Bloc: Inter-Bloc dependency

Created on 8 Dec 2019  ·  12Comments  ·  Source: felangel/bloc

Following the pattern used in the Todos tutorial, I'm unable to establish Interbloc dependencies through the listen method within another bloc. This method works fine within the widget tree.
Am I missing something here?

class myBloc extends Bloc<myBlocEvent, myBlocState> {
  final AnotherBloc anotherBloc;
  StreamSubscription _stateSubscription;
MyBloc({@required this.anotherBloc});
    _stateSubscription = anotherBloc.listen((state) {
      print('this is not being called');
      if (state is Paused) {
        add(MyBlocEvent());
      }
    });
}

Using version 2.1.1 if BlocLibrary.

Flutter (Channel stable, v1.9.1+hotfix.6, on Microsoft Windows [Version 10.0.18362.476], locale
en-US)
• Flutter version 1.9.1+hotfix.6 at C:\flutter
• Framework revision 68587a0916 (3 months ago), 2019-09-13 19:46:58 -0700
• Engine revision b863200c37
• Dart version 2.5.0

question

Most helpful comment

I made a mixin for this type of scenario:

mixin AutoResetLazySingleton<E, S> on Bloc<E, S> {
  @override
  Future<void> close() {
    if (locator.isRegistered<Bloc<E, S>>(instance: this)) {
      locator.resetLazySingleton<Bloc<E, S>>(instance: this);
    }
    return super.close();
  }
}

And the bloc:

@lazySingleton
class StoriesBloc extends Bloc<StoriesEvent, StoriesState>
    with AutoResetLazySingleton {
    ...
}

Blocs are registered as lazy singletons. This way you won't end up with multiple instances of your bloc when injecting it in other blocs, but also you'll ensure you'll be using the fresh instance everywhere. Hope it helps @ricpar11

All 12 comments

Hi @recreatingfitness 👋
Thanks for opening an issue!

Are you able to share a link to a sample app which illustrates the issue? Thanks!

Closing for now since I am missing information/sample app to reproduce the issue. Feel free to add additional information or link to a sample app and I'm happy to continue the conversation 👍

Appreciate the quick response Felix, sorry I haven't had time to construct a sample app and have just been using workarounds in my application.

If your flutter_todos example is having no issues it's safe to assume this has something to do with my set-up...the subscription in the initializer is only calling on app start (so only reacts to initial state) but the subscription is still showing as active. Thinking this may have to do with how I am calling the Bloc using a service locator but I am unsure. If I find anything or have some time to reproduce in a demo app I will let you know.

I've managed to tweak the flutter_todos app to demonstrates the issue i'm having within my application. repo here

The issue can be triaged to using dependency injection (get_it package). All that I've changed is how bloc dependencies are injected

Expected behavior: Blocs update in response to blocs they depend on changing state.

Observed behavior: Bloc initializers are only being called once...in the case of listening to another bloc only the seeded value is being emitted. In the sample app above, all blocs dependent on the Todos bloc are stuck in Loading

@felangel hoping you can take a look and potentially re-open this. thanks

Will take a look as soon as I can 👍

Update: I've tested a few other DI solutions with the same results inject.dart and kiwi. It doesn't make any difference using a factory or singleton pattern.

A confirmed workaround is using a ProxyProvider to inject independent blocs into dependent blocs (example from my application below). However at this point I'm just circumventing the service locator in favor of Provider. This can be quite a bit more verbose but I'm willing to accept this a solution provided that the following should not be a concern:

  1. Performance and bloc disposal
  2. Dependency Inversion and the ability to develop against abstractions

Appreciate any insight into this.

        MultiProvider(
          providers: [
            BlocProvider(
                create: (BuildContext context) =>
                    locator<WorkoutDataBloc>()..add(WorkoutStarted(config))),
            ProxyProvider<WorkoutDataBloc, LayoutBloc>(
              create: (context) => locator<LayoutBloc>(),
              update: (context, dataBloc, _) =>
                  LayoutBloc(workoutDataBloc: dataBloc),
            ),

          ],
          child: const TrackerScreen(),
        ),

@recreatingfitness I was able to make it work by refactoring service_locator.dart to

import 'blocs/blocs.dart';
import 'package:get_it/get_it.dart';

import 'blocs/todos/todos_bloc.dart';
import 'package:path_provider/path_provider.dart';

import 'package:todos_repository_simple/todos_repository_simple.dart';

GetIt locator = GetIt.instance;

void setupLocator() {
  setupBlocs();
}

void setupBlocs() {
  final TodosBloc todosBloc = TodosBloc(
    todosRepository: const TodosRepositoryFlutter(
      fileStorage: const FileStorage(
        '__flutter_bloc_app__',
        getApplicationDocumentsDirectory,
      ),
    ),
  );
  locator.registerFactory(() => TabBloc());
  locator.registerFactory(() => todosBloc);
  locator.registerFactory(() => FilteredTodosBloc(todosBloc: todosBloc));
  locator.registerFactory(() => StatsBloc(todosBloc: todosBloc));
}

I expected the following to work but it didn't:

import 'blocs/blocs.dart';
import 'package:get_it/get_it.dart';

import 'blocs/todos/todos_bloc.dart';
import 'package:path_provider/path_provider.dart';

import 'package:todos_repository_simple/todos_repository_simple.dart';

GetIt locator = GetIt.instance;

void setupLocator() {
  setupBlocs();
}

void setupBlocs() {
  locator.registerFactory<TabBloc>(() => TabBloc());
  locator.registerFactory<TodosBloc>(() => TodosBloc(
        todosRepository: const TodosRepositoryFlutter(
          fileStorage: const FileStorage(
            '__flutter_bloc_app__',
            getApplicationDocumentsDirectory,
          ),
        ),
      ));
  locator.registerFactory<FilteredTodosBloc>(
      () => FilteredTodosBloc(todosBloc: locator<TodosBloc>()));
  locator.registerFactory<StatsBloc>(
      () => StatsBloc(todosBloc: locator<TodosBloc>()));
}

Seems like there is a problem resolving registered instances within other factories. I would recommend opening an issue for this on get_it since this doesn't appear to be an issue with the bloc library.

Hope that helps 👍

@recreatingfitness I was able to make it work by refactoring service_locator.dart to

import 'blocs/blocs.dart';
import 'package:get_it/get_it.dart';

import 'blocs/todos/todos_bloc.dart';
import 'package:path_provider/path_provider.dart';

import 'package:todos_repository_simple/todos_repository_simple.dart';

GetIt locator = GetIt.instance;

void setupLocator() {
  setupBlocs();
}

void setupBlocs() {
  final TodosBloc todosBloc = TodosBloc(
    todosRepository: const TodosRepositoryFlutter(
      fileStorage: const FileStorage(
        '__flutter_bloc_app__',
        getApplicationDocumentsDirectory,
      ),
    ),
  );
  locator.registerFactory(() => TabBloc());
  locator.registerFactory(() => todosBloc);
  locator.registerFactory(() => FilteredTodosBloc(todosBloc: todosBloc));
  locator.registerFactory(() => StatsBloc(todosBloc: todosBloc));
}

I expected the following to work but it didn't:

import 'blocs/blocs.dart';
import 'package:get_it/get_it.dart';

import 'blocs/todos/todos_bloc.dart';
import 'package:path_provider/path_provider.dart';

import 'package:todos_repository_simple/todos_repository_simple.dart';

GetIt locator = GetIt.instance;

void setupLocator() {
  setupBlocs();
}

void setupBlocs() {
  locator.registerFactory<TabBloc>(() => TabBloc());
  locator.registerFactory<TodosBloc>(() => TodosBloc(
        todosRepository: const TodosRepositoryFlutter(
          fileStorage: const FileStorage(
            '__flutter_bloc_app__',
            getApplicationDocumentsDirectory,
          ),
        ),
      ));
  locator.registerFactory<FilteredTodosBloc>(
      () => FilteredTodosBloc(todosBloc: locator<TodosBloc>()));
  locator.registerFactory<StatsBloc>(
      () => StatsBloc(todosBloc: locator<TodosBloc>()));
}

Seems like there is a problem resolving registered instances within other factories. I would recommend opening an issue for this on get_it since this doesn't appear to be an issue with the bloc library.

Hope that helps 👍

Hi @felangel  👋
So starting from the "expected code to work":
Even though in the beginning I thought the same as you, I think I realize that if that code worked it still will not worked as expected. I mean, in this case we are trying to achieve inter-bloc dependency so we expect that if we want to access from a bloc blocB and the blocC to the blocA, we would want blocA to be the same instance, and that would not be the case because we registered blocA with a factory register

Each time you call get() you will get a new instance returned (from https://github.com/fluttercommunity/get_it)

So, are we talking about blocA being a singleton? Well, yes and no. Yes because we want to access to the same instance form blocB and blocC, and no because at some time the bloc can be closed and we couldn’t recreate an instance of it because it is a singleton indeed.

Now we can talk about the proposed workaround:
So yes, it work, but... using the same example blocs mentioned before, what if blocA gets closed?
Even though the factory would always returns a "new" instance when asking for a blocA object, the same closed bloc would be returned because of the final variable assigned in the body of the register function of A, B and C.

So, as a workaround we could create a factory constructor for our BlocA class and some static properties that give us the "state" of the "unique" valid instance.

class BlocA extends Bloc<BlocAEvent, BlocAState>
{
 SomeDependency someDependency;

...
static BlocA lastCreatedBlocA; 
static bool isClosed; // (would be great if we have a method lastCreatedBlocA.isClosed, so we could remove this variable)

  BlocA._internal({@required  SomeDependency someDependency})
      : assert(someDependency != null),
        this.someDependency = someDependency;

  factory BlocA({@required SomeDependency someDependency}) {
    BlocA blocAToReturn;

    if (BlocA.isClosed == null || BlocA.isClosed) {
      blocAToReturn = BlocA._internal(someDependency: someDependency);
    } else {
      blocAToReturn = lastCreatedBlocA;
    }

    BlocA.isClosed = false;
    BlocA.lastCreatedBlocA = blocAToReturn;
    return blocAToReturn;
  }
...
}

and inside of the blocA provided widget onDispose() method, we shoud set BlocA.isClosed to true

  @override
  void dispose() {
    BlocA.isClosed = true;
    super.dispose();
  }

finally, inside our injection container file, we change this:

  final blocA = BlocA(
        someDepency: sl.get<SomeDepency>(),
      );

with something like this:

  final blocA = () => BlocA(
        someDepency: sl.get<SomeDepency>(),
      );

and excute that funciton inside of the factories where blocA is present:

  sl.registerFactory<BlocA>(() => blocA());
  sl.registerFactory<BlocB>(() => BlocB(blocA: blocA()));
  sl.registerFactory<BlocC>(() => BlocC(blocA: blocA()));

I know it's not the cleanest workaround but for the moment I think it works, would be nice to have your opinion about this.

I made a mixin for this type of scenario:

mixin AutoResetLazySingleton<E, S> on Bloc<E, S> {
  @override
  Future<void> close() {
    if (locator.isRegistered<Bloc<E, S>>(instance: this)) {
      locator.resetLazySingleton<Bloc<E, S>>(instance: this);
    }
    return super.close();
  }
}

And the bloc:

@lazySingleton
class StoriesBloc extends Bloc<StoriesEvent, StoriesState>
    with AutoResetLazySingleton {
    ...
}

Blocs are registered as lazy singletons. This way you won't end up with multiple instances of your bloc when injecting it in other blocs, but also you'll ensure you'll be using the fresh instance everywhere. Hope it helps @ricpar11

Thats definitely a nice cleaner workaround. At first I thought it would still have the get it issue when trying to get the bloc instance, but apparently it only occurs when registering with factory functions.

Thank you @RollyPeres

Was this page helpful?
0 / 5 - 0 ratings

Related issues

hivesey picture hivesey  ·  3Comments

rsnider19 picture rsnider19  ·  3Comments

ricktotec picture ricktotec  ·  3Comments

shawnchan2014 picture shawnchan2014  ·  3Comments

nerder picture nerder  ·  3Comments