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

docs: Discussion - Bloc-to-Bloc communication recommendations #3816

Open
wujek-srujek opened this issue May 10, 2023 · 3 comments
Open

docs: Discussion - Bloc-to-Bloc communication recommendations #3816

wujek-srujek opened this issue May 10, 2023 · 3 comments
Labels
documentation Documentation requested

Comments

@wujek-srujek
Copy link

wujek-srujek commented May 10, 2023

Hi Felix (@felangel).

First off, I'm sorry if this is not the right place for discussion, it used to work fine here but I've been away for a long time so please forgive me.

After a many months hiatus, I'm back to Flutter with a vengeance, and back to my favourite state management library. I decided to see what changed and was very surprised to read the Bloc-to-Bloc Communication section. It seems to be making statements that many people (at least in my surroundings) don't agree with, and I would like to ask you for your opinion on an approach we have been employing:

  1. You essentially officially recommend against Bloc-to-Bloc communication, and I would consider the main reason controversial:

    Generally, sibling dependencies between two entities in the same architectural layer should be avoided at all costs

    Where does this rule come from? If you take a look at e.g. onion or port-adapters (hexagonal) architectures, nowhere do they prohibit components in the same layer ('siblings') from talking to each other. The only 'bad'/wrong dependencies are pointing 'outside' (e.g. a repository knowing a Bloc). I have a lot of backend experience and it is also day-to-day business to call a component with business logic by another component in the same layer.

  2. The official recommendation basically kills Bloc composition - you recommend against it - while if you consider other libraries, like provider or riverpod (to name the most written-about ones recently, apart from Bloc), they actively promote it (with ProxyProvider and Ref.watch(someOtherProvider), accordingly).

  3. The recommended solutions are not ideal:

    1. Connecting the blocs in the widget - IMHO this puts too much logic into the widget, and will also result in nesting and more complicated code in the build method. For example, when the UI depends on the state of BlocB, which in turn is triggered by a change in BlocA, you need a BlocListener<BlocA> and a BlocBuilder<BlocB>, where the listener is nested within the builder. (And this is just one use case, there are others.) For these reasons our team decided to completely reject this approach.
    2. Connecting the Blocs by using the same repository - but this either puts too much logic into the repository (see below for a use case, where I try to explain this point) which should only be concerned with fetching the data, or forces logic duplication in the Blocs, or requires creation of an 'aggregated repository' (or some kind of 'service') that keeps the common logic, and both Blocs use it. But I would like to put my logic in the Bloc (literally 'business logic component').

My example use case is the following (silly and contrived): there is a WeatherBloc that gives me weather forecast (it changes as the day goes by, and the Bloc periodically calls the repository to get weather data) and ActivityBloc which suggests what I could do now. ActivityBloc gets favourite activities for the current user from a repository, but what it suggests takes weather forecast into account - it will not suggest taking a walk if the forecast is a downpour. As you can see, ActivityBloc could really use some help from WeatherBloc.

(BTW, I keep saying Bloc this, Bloc that, but in reality I always start off with a Cubit; all that I write here still applies.)

  1. I don't think this belongs to just a single Bloc, the tasks are different enough and I would like to split them. Imagine also that other features (with other Blocs) that do something completely different than suggesting activities, also depend on the weather - I don't want to put all that into WeatherBloc, I want to split all of this.
  2. I could use the same WeatherRepository in both Blocs, but ActivityBloc would pretty much redo a lot of logic that WeatherBloc already does - the polling in this case.
  3. If I choose to implement the logic in a repository of some kind, does it really belong there? I would argue that it doesn't. If it is an 'aggregated' repository (a repository using other repositories internally), it will also violate your rule (as there would be a dependency on sibling repositories).
  4. I could add some kind of a service that uses WeatherRepository and implements the polling, and both Blocs could use it. But this would introduce yet another layer which is not really talked about in any Flutter-related documents. It would also work only on one level of dependencies - ActivityBloc needs forecast information - but what if some other components need to work with activity suggestions as their input? This could get pretty complex.

Now the solution that I have been very successful with in a few projects - Bloc-to-Bloc communication using abstractions:

  1. WeatherBloc get its weather data from a WeatherRepository and issues WeatherStates - pretty normal stuff.
  2. ActivitiyBloc gets the list the user's favorite activities from ActivityRepository, AND ALSO WeatherState from WeatherBloc, intersect these two pieces of information and provide ActivitySuggestions based on that.
  3. ActivityBloc doesn't use WeatherBloc directly, nor vice-versa. ActivityBloc gets a Stream<WeatherState> and when the stream emits a new event, it triggers emitting new activity suggestions. Bloc just so happens to be able to give me a Stream, which is really neat.
  4. The wiring is done by someone else, e.g. (simplified):
    final weatherBloc = WeatherBloc(weatherRepository);
    final activityBloc = ActivityBloc(
      activityRepository,
      Stream.value(weatherBloc.state).concatWith([weatherBloc.stream]),
    );

(I think Bloc used to immediately give the current state on subscription way back in the past, then it changed, that's why I 'prefix' the stream with the current state. In my app I have an extension for it.)

This approach been working wonderfully for me:

  1. A change in a Bloc that other Blocs depend on triggers a cascade of changes, and the UI updates itself accordingly. (A cascade because we could imagine ActivitySuggestions triggering a change in some other Blocs/features.)
  2. I consider this to be a clean approach ('details (concrete implementations) depend on abstractions' etc.). Except for one: ActivityBloc knows about WeatherState - but is this bad? It does need the forecast data in some form, and it makes it possible without introducing extra code for the sake of abstractions, or more layers.
  3. I can test it nicely - in a test I can just create a Bloc with a faked stream dependency using a StreamController and trigger changes and test reactions.

I have never had to wire the Blocs the other way round: continuing from my example above, I never needed WeatherBloc to know ActivityBloc to trigger a change. In my head this would be backwards (as WeatherBloc would need to know all of the Blocs that depend on it). I could make it work by just giving an abstract Sink<WeatherState> to WeatherBloc, and the Sink would be a 'multicast sink' that would in turn push the new state to other Blocs, and all that would be wired when the application is constructed. But I have never needed it this way, it does sound much more complex, and wrong.

I would be interested in your thoughts about our approach described in this essay. Is it a violation of some principles that don't occur to me?

Cheers.

@wujek-srujek wujek-srujek added the documentation Documentation requested label May 10, 2023
@WieFel
Copy link

WieFel commented Sep 28, 2023

I would also like to describe our use case (CC @Stev3nsen): we were trying to use the Connecting Blocs through Presentation approach, but encountered some problems with it.

The use case looks as follows (simplified):
On one page of our app (page 1) we have a list of entries. The same entries are displayed on a map (Google map) on a separate page (page 2).
Untitled-2022-10-25-1855

Now we implemented a filter mechanism which allows to filter the entries by categories. The filter mechanism appears in both pages. The state of the filter is held in a bloc FilterBloc. The list's state is held in the ListBloc, the map's state is held in the MapBloc.
Untitled-2022-10-25-1855(1)

Both pages should use the same filter state. I.e. if I select "filter 1" on page 1, then also page 2 should apply "filter 1" automatically. The FilterBloc is already used on both pages, thus the state of the filter bar itself is already synchronised between them.
However, the single states of ListBloc and MapBloc still need to be updated, once the filters in the FilterBloc change.
Therefore, what we do in the two pages' build functions is the following:
List page:

BlocListener<FilterBloc, FilterState>(
  listener: (context, state) =>
      context.read<ListBloc>().add(ListFilterChanged(state.filter)),
  child: BlocBuilder<ListBloc, ListState>(
    builder: (context, state) {
      // build list with filtered items
    },
  ),
)

Map page:

BlocListener<FilterBloc, FilterState>(
  listener: (context, state) =>
      context.read<MapBloc>().add(MapFilterChanged(state.filter)),
  child: BlocBuilder<MapBloc, MapState>(
    builder: (context, state) {
      // build map with filtered marker items
    },
  ),
)

Which is exactly like in the example from the docs.

The problem we encounter here is that the synchronisation of the Blocs now depends on the UI.
E.g. if we are on the list page and change the filters, the map page is not active and thus the state of the MapBloc will not get updated accordingly and will be inconsistent. When we later switch to the map, it's items are incorrect.
The same happens the other way round: if we are currently on the map page, modify some filter and then switch to the list page, the filter state there will be different (wrong).

We ended up sticking to an approach similar to the one mentioned by @wujek-srujek, which connects the Blocs directly, without the involvement of the UI.

@Maxim-Filimonov
Copy link

Found this article which explains connection via domain on specific example:
https://medium.com/flutter-community/blocs-with-reactive-repository-5fd440d3b1dc might be helpful

@blackchineykh
Copy link

I would also like to describe our use case (CC @Stev3nsen): we were trying to use the Connecting Blocs through Presentation approach, but encountered some problems with it.

The use case looks as follows (simplified): On one page of our app (page 1) we have a list of entries. The same entries are displayed on a map (Google map) on a separate page (page 2). Untitled-2022-10-25-1855

Now we implemented a filter mechanism which allows to filter the entries by categories. The filter mechanism appears in both pages. The state of the filter is held in a bloc FilterBloc. The list's state is held in the ListBloc, the map's state is held in the MapBloc. Untitled-2022-10-25-1855(1)

Both pages should use the same filter state. I.e. if I select "filter 1" on page 1, then also page 2 should apply "filter 1" automatically. The FilterBloc is already used on both pages, thus the state of the filter bar itself is already synchronised between them. However, the single states of ListBloc and MapBloc still need to be updated, once the filters in the FilterBloc change. Therefore, what we do in the two pages' build functions is the following: List page:

BlocListener<FilterBloc, FilterState>(
  listener: (context, state) =>
      context.read<ListBloc>().add(ListFilterChanged(state.filter)),
  child: BlocBuilder<ListBloc, ListState>(
    builder: (context, state) {
      // build list with filtered items
    },
  ),
)

Map page:

BlocListener<FilterBloc, FilterState>(
  listener: (context, state) =>
      context.read<MapBloc>().add(MapFilterChanged(state.filter)),
  child: BlocBuilder<MapBloc, MapState>(
    builder: (context, state) {
      // build map with filtered marker items
    },
  ),
)

Which is exactly like in the example from the docs.

The problem we encounter here is that the synchronisation of the Blocs now depends on the UI. E.g. if we are on the list page and change the filters, the map page is not active and thus the state of the MapBloc will not get updated accordingly and will be inconsistent. When we later switch to the map, it's items are incorrect. The same happens the other way round: if we are currently on the map page, modify some filter and then switch to the list page, the filter state there will be different (wrong).

We ended up sticking to an approach similar to the one mentioned by @wujek-srujek, which connects the Blocs directly, without the involvement of the UI.

Is there a reason you couldn't use a ItemsBloc instead that is providing the items to the map UI and the list UI and have the bloc provided further up the widget tree to be available to both UI?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Documentation requested
Projects
None yet
Development

No branches or pull requests

4 participants