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:
- 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 - Subscribing to the
TagsBloc
inMoviesBloc
- seems error-prone to me, also same as in point 1 - Creating separate blocs for the list screens and details screens - lots of redundancy and additional boilerplate code
- Nesting two
BlocBuilder
components in theMovieDetailsScreen
-BlocBuilder
currently does not support more than one bloc, probably because this is an anti-pattern - 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 BlocBuilder
s 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.