Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
lirantzairi opened this issue Jul 21, 2024 · 4 comments
Open

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

lirantzairi opened this issue Jul 21, 2024 · 4 comments
Labels
needs repro info The issue is missing a reproduction sample and/or steps question Further information is requested waiting for response Waiting for follow up

Comments

@lirantzairi
Copy link

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
Copy link
Owner

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 👍

@felangel felangel added question Further information is requested waiting for response Waiting for follow up needs repro info The issue is missing a reproduction sample and/or steps labels Jul 21, 2024
@lirantzairi
Copy link
Author

Thanks for the response @felangel 🙂

I created this example, tried to simplify as much as possible:
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class CounterRepository {
  const CounterRepository();

  Stream<int> streamCount() {
    return Stream.periodic(const Duration(seconds: 1), (count) => count);
  }
}

class CounterCubit extends Cubit<int> {
  final CounterRepository repository;
  StreamSubscription<int>? _streamSubscription;

  CounterCubit({
    required this.repository,
  }) : super(0) {
    _streamSubscription = repository.streamCount().listen((count) {
      emit(count);
    });
  }

  @override
  Future<void> 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<CounterCubit, int>(
          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<CounterRepository>(),
            ),
            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.

@tenhobi
Copy link
Collaborator

tenhobi commented Oct 22, 2024

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.

The issue with this is that one Bloc can be used in multiple places across the application. Adding an event about such case (that you wanna stop smt) is the correct universal approach.

Also note that it's not necessary true that Blocs are part of presentation layer. They are more in between presentation and application, or however you wanna call them. Bloc don't even know anything about BuildContext etc., therefore it cannot listen to platform updates. The thing that brings Bloc to presentation is BlocProvider/BlocBuilder, but Bloc itself is unaware.

@felangel
Copy link
Owner

Thanks for the response @felangel 🙂

I created this example, tried to simplify as much as possible:

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class CounterRepository {
  const CounterRepository();

  Stream<int> streamCount() {
    return Stream.periodic(const Duration(seconds: 1), (count) => count);
  }
}

class CounterCubit extends Cubit<int> {
  final CounterRepository repository;
  StreamSubscription<int>? _streamSubscription;

  CounterCubit({
    required this.repository,
  }) : super(0) {
    _streamSubscription = repository.streamCount().listen((count) {
      emit(count);
    });
  }

  @override
  Future<void> 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<CounterCubit, int>(
          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<CounterRepository>(),
            ),
            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.

In this case, I'd recommend moving the BlocProvider lower in the widget tree (just scoped to the CounterScreen. That way when the CounterScreen is unmounted, the bloc will be closed and the underlying subscription will also be canceled. Hope that helps 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs repro info The issue is missing a reproduction sample and/or steps question Further information is requested waiting for response Waiting for follow up
Projects
None yet
Development

No branches or pull requests

3 participants