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
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
likeInfoobject that holds whether the item was liked or unliked
Similarly, the bloc has 2 events:
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
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
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
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 (
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
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 —
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.
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.