felangel / bloc

A predictable state management library that helps implement the BLoC design pattern
https://bloclibrary.dev
MIT License
11.69k stars 3.38k forks source link

feat: API notifying when any Bloc widgets are active #4208

Open lirantzairi opened 1 month ago

lirantzairi commented 1 month ago

Description

In my Bloc I have a stream subscription which listens to a Firebase Firestore's document real time updates. I want to maintain the state even when the user moves to a different screen in the app so I keep the Bloc alive. However when the user is in a different screen I want to pause the Firestore listener so I need to know once the relevant BlocBuilder is disposed of. Also I need to know once any new BlocBuilder is listening again to this Bloc so I can resume the Firestore listener.

Desired Solution

Add two overridable methods to BlocBase:

void Function()? onStreamListen();
void Function()? onStreamCancel();

onStreamListen will be invoked once the first Bloc widget (BlocBuilder, BlocSelector, BlocConsumer etc) is attached. onStreamCancel will be invoked once all the Bloc widgets attached to this Bloc are disposed of.

An intuitive solution would be to assign these functions to the creation of late final _stateController = StreamController<State>.broadcast(); But it's problematic when using BlocProvider because this provider also attaches a listener of its own to the Bloc.

felangel commented 1 month ago

Hi @lirantzairi 👋 Thanks for opening an issue!

It’s hard to say without a link to a sample app which illustrates the setup and problem you’re facing but it sounds like you should be able to notify the bloc (add an event) when navigating away (then the bloc can pause the subscription) and similarly notify the bloc when you navigate back to the page (then the bloc can resume the subscription).

Let me know if that helps. If you’re still facing issues, then please share a link to a minimal reproduction sample and I’m happy to take a closer look 👍

lirantzairi commented 1 month ago

Thanks for the response @felangel 🙂

I created this example, tried to simplify as much as possible: ```dart import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class CounterRepository { const CounterRepository(); Stream streamCount() { return Stream.periodic(const Duration(seconds: 1), (count) => count); } } class CounterCubit extends Cubit { final CounterRepository repository; StreamSubscription? _streamSubscription; CounterCubit({ required this.repository, }) : super(0) { _streamSubscription = repository.streamCount().listen((count) { emit(count); }); } @override Future close() { _streamSubscription?.cancel(); return super.close(); } } class CounterScreen extends StatelessWidget { const CounterScreen(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(runtimeType.toString())), body: Center( child: BlocBuilder( builder: (context, count) { return Text(count.toString()); }, ), ), ); } } class HomeScreen extends StatelessWidget { const HomeScreen(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(runtimeType.toString())), body: Center( child: TextButton( onPressed: () { Navigator.of(context).push( MaterialPageRoute(builder: (_) => const CounterScreen()), ); }, child: const Text('Go to CounterScreen'), ), ), ); } } void main() { runApp( MaterialApp( builder: (context, child) { return RepositoryProvider( create: (_) => const CounterRepository(), child: BlocProvider( create: (context) => CounterCubit( repository: context.read(), ), child: child, ), ); }, home: const HomeScreen(), ), ); } ```

Think that CounterRepository can listen to a Firebase document, so to reduce costs we want to have it on only when necessary. Now, when you launch the app and you're in HomeScreen, the counter isn't running because the bloc is lazy (that's good). It starts running only when you go to CounterScreen. The problem is that when you go back to home it will continue running. I want to be able to cancel _streamSubscription when navigating back to HomeScreen and set it again when navigating again to CounterScreen.

Technically you're right that it's possible to notify the bloc when navigating away and back. But this will cause the presentation layer to send events to the bloc layer just for the purpose of the bloc maintaining itself. Which in my opinion contradicts one of the purposes of this architecture. From your documentation:

The presentation layer’s responsibility is to figure out how to render itself based on one or more bloc states. In addition, it should handle user input and application lifecycle events.

It makes more sense that the bloc has its own API to notify when its stream is (or is stopped) being listened to. Hope I managed to make more sense.