Home > other >  How to structure BLoCs for screens with cross-dependent entities
How to structure BLoCs for screens with cross-dependent entities

Time:09-03

I am currently trying to learn the BLoC pattern and still struggle with figuring out the best architecture for an app where entities in the model have cross-dependencies that also have implications on screens.


I am building a very simple CRUD Flutter app that allows the user to manage and tag a movie database. The database is implemented using the SQlite plugin and Drift. So far, there are 4 main screens:

  • MoviesListScreen
  • MovieDetailsScreen
  • TagsListScreen
  • TagDetailsScreen

So obviously, the list screens list all movies / tags in the database, respectively, and allow you to add and delete entities. When you click on an entitiy on a list screen, you reach the details screen with all information on the respective movie / tag.

From what I have read, it is recommended to have one bloc per feature or screen. Following this tutorial, I created two blocs, one for movies and one for tags. Here is the tags state:

enum TagsStatus { initial, success, error, loading, selected }

extension TagsStatusX on TagsStatus {
  bool get isInitial => this == TagsStatus.initial;
  bool get isSuccess => this == TagsStatus.success;
  bool get isError => this == TagsStatus.error;
  bool get isLoading => this == TagsStatus.loading;
  bool get isSelected => this == TagsStatus.selected;
}

class TagsState extends Equatable {
  final TagsStatus status;
  final List<Tag> tags;
  final Tag selectedTag;
  final List<Movie> moviesWithSelectedTag;

  const TagsState(
      {this.status = TagsStatus.initial, List<Tag> tags, Tag selectedTag, List<Movie> moviesWithSelectedTag})
      : tags = tags ?? const [],
        selectedTag = selectedTag,
        moviesWithSelectedTag = moviesWithSelectedTag;

  @override
  List<Object> get props => [status, tags, selectedTag];

  TagsState copyWith({TagsStatus status, List<Tag> tags, Tag selectedTag, List<Movie> moviesWithSelectedTag}) {
    return TagsState(
        status: status ?? this.status,
        tags: tags ?? this.tags,
        selectedTag: selectedTag ?? this.selectedTag,
        moviesWithSelectedTag: moviesWithSelectedTag ?? this.moviesWithSelectedTag);
  }
}

And the corresponding bloc:

class TagsBloc extends Bloc<TagEvent, TagsState> {
  final Repository repository;

  TagsBloc({this.repository}) : super(const TagsState()) {
    on<GetTags>(_mapGetTagsEventToState);
    on<SelectTag>(_mapSelectTagEventToState);
  }

  void _mapGetTagsEventToState(GetTags event, Emitter<TagsState> emit) async {
    emit(state.copyWith(status: TagsStatus.loading));
    try {
      final tags = await repository.getTags();
      emit(
        state.copyWith(
          status: TagsStatus.success,
          tags: tags,
        ),
      );
    } catch (error, stacktrace) {
      print(stacktrace);
      emit(state.copyWith(status: TagsStatus.error));
    }
  }

  void _mapSelectTagEventToState(event, Emitter<TagsState> emit) async {
    emit(
      state.copyWith(
        status: TagsStatus.selected,
        selectedTag: event.selectedTag,
      ),
    );
  }
}

Now this works perfectly fine to manage the loading of the list screens. (Remark: I could create separate blocs for the details screens, because it feels a bit out of place to have the selectedTag bloc in the state that is used for the TagsListScreen, even though the selected tag will be displayed on a different screen. However, that would create additional boilerplate code, so I am unsure about it.)

What I really struggle with is how to access all tags in the MovieDetailsScreen where I am using the MoviesBloc. I need them there as well in order to display chips with tags that the user can add to the selected movie simply by clicking on them.

I thought about the following possibilities:

  1. Add all tags to the MoviesBloc - that would go against the point of having two separate blocs and I would have to make sure that both blocs stay in sync; moreover, a failure loading the tags would also cause a failure loading the movies, even in widgets that don't even use the tags
  2. Subscribing to the TagsBloc in MoviesBloc - seems error-prone to me, also same as in point 1
  3. Creating separate blocs for the list screens and details screens - lots of redundancy and additional boilerplate code
  4. Nesting two BlocBuilder components in the MovieDetailsScreen - BlocBuilder currently does not support more than one bloc, probably because this is an anti-pattern
  5. Using one single bloc that holds movies as well as tags and use it in all 4 screens - discouraged by the creators of the BLoC package; also I would need two status properties to manage the loading from the database separately for movies and tags, which I feel should be in separate blocs

What would be the recommended way to handle this kind of business logic with blocs?

CodePudding user response:

Subscribing to the TagsBloc in MoviesBloc - seems error-prone to me, also same as in point 1

This is the cleanest approach. I have been using it and it scales pretty well as number of blocs increases.

CodePudding user response:

Option 4

As you're no dought aware, there are no correct and incorrect answers here, it's a matter of design. And if you ask me, I wouldn't consider movies and tags as two separate features, but that highly depends on the project domain and one's definition of a feature, so let's not go there.

Answering your question I'd go with option 4. Nesting BlocBuilders is a quite common pattern in the bloc architecture, I've seen it many times. Also, in the same thread you've referenced, the author is recommending that idea, so I don't think it's an anti-pattern.

p.s. option 3 is also fine, and maybe you can avoid the redundancy by creating a class that contains the shared logic between the two cubits, thus, you can still maintain them together, while having the perks of two separate blocs.

CodePudding user response:

There is a recommendation from the author of flutter_bloc available on the flutter_bloc documentation page.

https://bloclibrary.dev/#/architecture?id=bloc-to-bloc-communication

Some key-lines from the page:

...it may be tempting to make a bloc which listens to another bloc. You should not do this.

...no bloc should know about any other bloc.

A bloc should only receive information through events and from injected repositories

So in short, something like your options 3 or 4. Perhaps with a touch of a BlocListener there to trigger events between the blocs.

There is no problem having multiple blocs per screen. Consider having a bloc controlling the state of a button based on some API calls or a Stream of data, while other parts of the screen are determined by another bloc.

It is not a bad thing to have an "outer" bloc determining if a part of a screen should be visible, but that inner part's state is handled by a separate bloc.

  • Related