Effective Bloc-to-Bloc Communication Strategies in the Domain Layer

Effective Bloc-to-Bloc Communication Strategies in the Domain Layer

Optimizing Bloc-to-Bloc Communication with rxdart's reactive programming features

·

4 min read

Introduction

Numerous guides, including those from the bloclibrary, address the intricacies of bloc-to-bloc communication and over there, you can find this warning:

Generally, sibling dependencies between two entities in the same architectural layer should be avoided at all costs, as it creates tight-coupling which is hard to maintain. Since blocs reside in the business logic architectural layer, no bloc should know about any other bloc.

So blocs should be connected either through the presentation in the application layer or the domain layer.

Connecting Blocs through Presentation

class MyWidget extends StatelessWidget {
  const MyWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocListener<FirstBloc, FirstState>(
      listener: (context, state) {
        // When the first bloc's state changes, this will be called.
        //
        // Now we can add an event to the second bloc without it having
        // to know about the first bloc.
        BlocProvider.of<SecondBloc>(context).add(SecondEvent());
      },
      child: TextButton(
        child: const Text('Hello'),
        onPressed: () {
          BlocProvider.of<FirstBloc>(context).add(FirstEvent());
        },
      ),
    );
  }
}

The code above encourages loose coupling, but there's a potential issue if MyWidget is not mounted when FirstBloc's state changes. In such cases, the BlocListener may not be triggered. To address this, you might consider converting MyWidget to a StatefulWidget and handling events in the didChangeDependencies method. However, note that this approach could lead to unintended consequences, such as the SecondEvent being added whenever didChangeDependencies is called.

The didChangeDependencies method will be called in these situations.

  • After the initState method.

  • When an inherited widget changes, eg Theme or Locale.

  • When the BuildContext changes.

  • When callingNavigator.of(context).

  • When the keyboard appears or disappears.

  • When the Mediaquery changes, eg rotation changes, size changes etc

As you can see, that might not be a good place either to listen to bloc updates if you want to get the first update as well as the subsequent ones. What about;

Connecting Blocs through Domain

From the bloclibrary:

Two blocs can listen to a stream from a repository and update their states independent of each other whenever the repository data changes.

So we can create a repository that emits a data stream and the same repository can be injected into each bloc that needs to react to new data updates. Like this example in the bloclibrary.

  Stream<String> productIdeas() async* {
    while (true) {
      yield _ideas[_currentAppIdea++ % _ideas.length];
      await Future<void>.delayed(const Duration(minutes: 1));
    }
  }
await emit.forEach(
 _appIdeasRepo.productIdeas(),
 onData: (String idea) => AppIdeaRankingIdeaState(idea: idea),
);

While the above code may seem like a solution, it introduces new challenges. Dart streams are single-subscriber by default, meaning they can only be listened to once. Converting the stream to a broadcast stream doesn't solve the issue either; it won't pause emitting events when there are no listeners, even before Blocs are created.

If a listener subscribes to the productIdeas stream after it has started emitting items, that listener won't receive the items emitted before its subscription. Passing lazy: false to BlocProvider for eager initialization might work inconsistently.

To address these problems, solutions can be found in the rxdart package.

BehaviorSubject and ReplaySubject

From the documentation:

ReplaySubject: A special StreamController that captures all of the items that have been added to the controller, and emits those as the first items to any new listener.

BehaviorSubject: A special StreamController that captures the latest item that has been added to the controller, and emits that as the first item to any new listener.

So with these two subjects, you can have your blocs be up to date the moment they're created, whether lazily or not. Also, you probably don't need the ReplaySubject since it's overkill but it's good to keep it in mind.

Now that we know of these, we can do something like this:

  final BehaviorSubject<FirstState> _firstStateSubject =
      BehaviorSubject<FirstState>.seeded(const FirstState());

  Stream<FirstState> get firstStateStream {
    return _firstStateSubject.stream;
  }

  FirstState get currentFirstState => _firstStateSubject.value;

  void updateFirstState(FirstState firstState) {
    _firstStateSubject.add(firstState);
  }

This code could be part of a FirstStateRepository class that caches the state of FirstBloc and the FirstBloc can call updateFirstState in it's onChange method. Now any bloc like the SecondBloc in the project can listen to the firstStateStream and get its latest data or just query the latest value.

If you have an existing stream that you can't manipulate easily, you can do something like this:

UserChangesService

import "package:firebase_auth/firebase_auth.dart";
import "package:rxdart/rxdart.dart";

class UserChangesService {
  UserChangesService({required FirebaseAuth firebaseAuth})
      : _firebaseAuth = firebaseAuth {
    _firebaseAuth.userChanges().listen(_userChangesSubject.add);
  }

  final FirebaseAuth _firebaseAuth;
  final _userChangesSubject = BehaviorSubject<User?>.seeded(null);

  Stream<User?> get userChanges => _userChangesSubject.stream;

  User? get currentUser => _userChangesSubject.value;
}

Inside the constructor, the code starts listening to the userChanges stream provided by the FirebaseAuth instance. Whenever the user state changes (for example, when the user logs in or out), the new User object is added to _userChangesSubject.

The repository then exposes these changes as a stream that other parts of the application can subscribe to and you can rest assured that any bloc won't miss the latest value no matter how late they're created.

I've created a new brick to aid in creating these reactive repositories, right here.

That's all for now, subscribe to my newsletter if you found this valuable.

Did you find this article valuable?

Support Henry's blog by becoming a sponsor. Any amount is appreciated!