The Flutter BLoC pattern is a popular and efficient way of managing a state in Flutter apps. It allows us to separate business logic from the user interface, making managing applications' dynamic and complex states easier. However, as with every similar tool, it has challenges and can sometimes be difficult to use in specific scenarios. Today, we’re going to focus on handling one-time events, such as displaying a confirmation dialog or a snack bar. In general, any event that should only be shown to the user once without affecting the state falls into this category.
In this post, all examples use the Bloc library, particularly the bloc
and flutter_bloc
packages.
Typical use of one-time events in blocs
Let's demonstrate a simple app that loads items and lets users like them. The app has two events. The first one relates to loading the items. You simply tap on the load-more button, and if the process succeeds, an updated list with new items is emitted. If an error occurs, a red snack bar should appear to inform the user. The second event is a functionality that allows users to like an item. Here, you tap the heart icon next to each item. Liking an item turns it red and makes the snack bar appear. Users can remove the like by tapping the heart icon again.
Target app example (NetGlade)
As the app is very simple, it is driven by a single ItemsBloc
bloc with single state ItemsState
with variables:
a list of items where each item contains its name and a liked flag
a bool determining if the loading failed
an optional
likeInfo
object that holds whether the item was liked or unliked
Similarly, the bloc has 2 events: LoadItems
and LikeItem
. An event to like an item has an order (index) of the item in the list.
Once the user taps a load-more button, an event LoadItems
is sent to the bloc. In response, the bloc sends the request to the API. If the call is successful, the list should be loaded or updated. If some error occurred, we should see a red snack bar with information about that.
When a user taps a like or an unlike icon, an event LikeItem
is sent. In response, the bloc emits the state with set likeInfo
parameter, reflecting whether the like or unlike happened, and sets the item's name to the corresponding string.
Take a look at the implementation of loading items:
And here’s one for liking an item:
To show the snack bars, we use BuildConsumer
, which allows us to add a listener
builder that is only called once, and we can execute non-build code there. It would look like this:
What’s wrong with it?
As we know, displaying a list of items is a piece of cake. The problems might come later when we want to add additional actions, mainly when producing effects other than displaying them on a screen.
Because we are using copyWith
on ItemsState
rather than manually creating a new state each time, managing the internal states of these actions or one-time parameters is quite tricky. And we often cannot emit entirely new states because each event only needs to affect a particular part of the state. In the example, we use two action parameters for the state, bool loadingFailed
and LikeInfo? likeInfo
.
As you can see on the gif below, the error snack bar is not shown when the error occurs the second time because the internal state (items, boolean variable) does not change. That is because we want to use Equatable
, which makes the new state equal by overriding ==
and hashCode
. Generally, you want to use Equatable
for their perks, as bloc only rebuilds widgets when new states are emitted, not similar. In Flutter, UI is built based on a state, which makes this option quite reasonable. And when the state is the same, there is no need to rebuild. Similarly, if the loading of items fails (and a red snack bar is displayed) and the like status changes for one item afterwards, the loading failed snack bar is shown again. This is happening because the loading failed boolean variable has not been reset.
Example with issues due to a snack bar controlled by bloc states generated by copyWith (NetGlade)
Potential solutions of one-time events problematics
The problems mentioned above have several potential solutions, each with their advantages and disadvantages. Here are some of them:
1. Reset all the action parameters at the end of every bloc action
Whenever a question is liked, or an answer is selected, we can simply emit an additional ItemsState
state with all the action parameters set to their default values (null
or false
). Unfortunately, the reset leads to an unnecessary UI rebuild, and we need to remember to reset the state each time the action is performed.
2. Use a separate bloc for every action
In our sample app, we could split it into 2 blocks. A bloc for loading list of items and a bloc for handling likes for each item. Therefore for 10 items in a list, we would need 1 bloc for loading items and 1 instance for each item, meaning 11 in total. This solution is nice and clean as it decouples the logic. However, in the case of a minor feature, it may be unnecessarily complex, and it adds a lot of boilerplate code. Decoupling the logic might also be more complicated than it might seem sometimes.
3. Use a modified copyWith
method to create new ItemsState
states when performing any action
This can be done in two ways. The first is to create a separate copy-with-based method for each action parameter, like copyWithLikedItem
, that sets the likeInfo
parameter and resets all the other action parameters to default value. One drawback of this solution is that new actions require creating a separate method to generate a new ItemsState
state. The other approach is to create some universal method like copyWithReset
that would set other one-time parameters to their default value every time, except for arguments passed to the method. For this approach, we cannot control individual parameters efficiently, and if we want to keep something on every new emit, we are in trouble. As the number of actions and types of loaded data can increase, this approach can become difficult to manage.
4. Take advantage of additional one-time events stream
Sometimes one-time events are also called side effects, presentation events, etcetera. This stream is separated from the main bloc's state stream and is handled by a dedicated listener widget. We will take a close look at this approach in the following chapters.
Illustration of side effects (NetGlade)
Handling one-time events
As previously mentioned, we can extend the bloc and add a stream to it. That can be achieved by either a subclass or a mixin. And to produce a one-time event, we simply add it to this stream. Similar to listening to standard bloc state stream, we can create a particular listener widget that listens to the new stream events and reflect them accordingly. This approach is particularly well-suited for one-time actions, like showing dialogs or snack bars, as it does not impact the bloc's state. In the case of Bloc, it is best to work around extending the BlocBase
using a mixin, so you can work with both blocs and cubits. Extending the Bloc
and Cubit
classes is possible, but you must duplicate the code.
By deploying this concept, we can solve our one-time events problems by creating 2 new one-time events corresponding with the app actions — ItemLikedEffect
and LoadingFailedEffect
. Instead of emitting new states, we produce one-time events in the bloc handler.
Here’s an implementation of item loading changes only in the catch block:
The implementation of liking is further simplified by removing likeInfo
arguments from the copyWith
method and producing the side effect afterwards. The emit
must remain, so we can display a different like icon colour and shape.
Then we use the listener for side effects above the builder for states, move the listener from bloc consumer there, and modify to check for the classes:
Using one-time events or side effects, you name it, we do not have to modify our bloc states and keep the logic pretty simple while avoiding a lot of boilerplates. There is no risk of data loss or undesired showings of snack bars or dialogs.
Several libraries already implement the concept of one-time events, for example, bloc_presentation, side_effect_bloc, or bloc_effects.
Final thoughts
One might ask which is the better option: split blocs into multiple small ones for each action or use side effects? Well, as usual with coding, the answer isn’t so clear-cut.
Essentially, it's all about trade-offs. Both solutions might work, but you should consider if displaying one snack bar or dialog is worth the effort of separating a bloc into two, three, or even more. And while using side effects couples the bloc with UI more than it should, it might be an easy and transparent way to go.
Try both approaches for yourself and play with them. Then you can decide your preference. We suggest not enforcing one way over the other in the whole project. In some places, it might be better to use side effects, while others will fare better with splitting the bloc.
Sources
Recipes: Show SnackBar with BlocListener | bloclibrary.dev
Add the concept of side effects | felangel - Github
Side-effects (events) without overriding state | felangel - Github