Business Logic Component [3/4]
In the third part of a series of articles about the bloc, we will analyze successful and unsuccessful decisions, typical mistakes, and misconceptions when designing a Business Logic Component.
You can consider this a checklist, check your coding style, or pick up new ideas.
Helpful hints, design tips, and tricks
π€ If possible, use the freezed package for events and states, especially its feature with Union and map, maybeMap, and mapOrNull methods. But the usage of when with a bloc is best to be avoided.
You shouldn't use when because if you change a state or event (add, rename or remove a field), it will affect the whole app, and you will need to rewrite many parts of your program, even if nothing depends on the changed fields.
π€ Do not add variables to the bloc where you pass data between states.
The BLoC already has such a variable, and it is called a state. Just copy data from one state to another, even if it hasn't changed.
In practice, a BLoC may have the following private variables: Repositories and StreamSubscribtions
π€ The most versatile way to write handlers and BLoC generators is to alternate states with Processing, Error, Success, and Idle.
But this does not mean you cannot practice other approaches, including those in the same BLoC.
π€ Use snippets for everything.
No need to write by hand what is perfectly automated. For example, here is mine, which significantly speeds up development in many ways.
π€ BLoC Observer. One observer receives callbacks from all BLoCs at once, which makes it an ideal place to log metrics, catch all errors and send them to Sentry or Crashlytics.
And, of course, always rethrow exceptions in the bloc if you donβt know what exactly happens and log it inside BLoC Observer.
π€ Because blocs expose streams, making a bloc that listens to another bloc may be tempting. You should not do this. it creates a dependency between two blocs.
If one of your blocs needs to depend on the states of another, you have two ways to go about it.
- You can subscribe to the states of one in the widget and add events to the other based on them (e.g., BlocListener or just subscribe in initState)
- You can call repository methods in one bloc, while the second bloc will subscribe to a method or getter that returns the repository stream.
Two blocs can listen to a stream from a repository and update their states independently of each other whenever the repository data changes (e.g., FirebaseAuthentication or Database triggers or Web Sockets). Using reactive repositories to keep the state synchronized is common in large-scale enterprise applications.
Passing one bloc to the constructor of another would be an error since components of the same level should not be dependent. Generally, sibling dependencies between two entities in the same architectural layer should be avoided at all costs, as it creates tight coupling, which is hard to maintain. Since blocs reside in the business logic architectural layer, no bloc should know about any other bloc.
π€ Try to reduce the number of states. Most BLoCs need 3-4 states, which are usually common for all events. It would be a mistake to create separate states for separate specific events.
Also, states should not contain information about the events.
π€ It's a good idea to try measuring bloc transitions, e.g., with tools like Firebase Performance Monitor and Sentry. They have the tools to trace transactions.
π€ Set and override event transformers. The current version of the bloc package (8.x.x.) uses the awful default transformer, which practically nullifies all the advantages of the bloc. You must redefine it. Look more at bloc_concurrency if you use the bloc package.
π€ BLoC should only receive information through events and from injected repositories (i.e., repositories given to the bloc in its constructor).
π€ If you use a BLoC, don't think that in the widget layer, or you are now prohibited from using a StreamController or ChangeNotifier or ValueNotifier or setState. Or even synchronously check the form text field for completion. A bloc is a good solution if the processing is asynchronous and unexpected exceptions are likely.
But there is no need to overcomplicate what is easily solved.
You are engineers, not fanatics. Your goal is to solve problems, not blindly follow a cult.
Without fanaticism, please, at all.
π€ We must use the repositories interface inside the bloc and the data providerβs interface inside a repository. Dependency Inversion principle.
Speaking of principles, I would like to mention the OCP. Do not remove methods and classes, and do not make significant changes to existing methods. Please try to use the @Deprecated(...) annotation for this everywhere. And try to remove such things only after a couple of releases.
This will significantly simplify the work of medium and large teams. And working with git and merge will become much easier.
π€ Try to encapsulate logic inside a repository (or something like use cases), do not try to write everything inside bloc implementation.
π€ If you are using the bloc package, then every next state must not be equal previous.
This is another one of many package design mistakes.
The package author tries to filter the states, believing that by doing so, he gets rid of duplicates in the widgets.
Though, of course, it's not.
E.g.
- You emit int states in order: [1, 1, 2, 1]
- bloc package distinct him, and now it [1, 2, 1]
- In BlocBuilder or StreamBuilder, your filter with buildWhen or where gets only even numbers.
- In build, you get [1, 1]
What is the point of this? Itβs a problem. If the authorβs bloc package needed something like this, he should have done something like the BlocBuilder.distinct factory.
Just keep in mind this behavior and a design error that has been around for several years. Try to make all states not equal to the previous one.
π€ Use debugFillProperties in widgets. It helps you track current states and data inside Flutter Inspector
π€ If you create an instance in initState or didChangeDependencies, you can inject it in Element Tree with BlocProvider.value (not a BlocProvider)
π€ Better to use a "Scopes" approach instead of providers. Also, InheritedWidgets are much more convenient and beautiful to use than providers, but this requires some skill.
What would you prefer?
import auth_scope;
final user = AuthenticationScope.userOf(context);
AuthenticationScope.logOut(context);
AuthenticationScope.authenticateOr(context, callback);
or
import flutter_bloc;
import user;
import auth_event;
import auth_state;
import auth_bloc;
final user = BlocProvider.of<AuthenticationBLoC>(context).state.user;
BlocProvider.of<AuthenticationBLoC>(context).add(LogOut());
BlocProvider.of<AuthenticationBLoC>(context).user is AuthenticatedUser)
? callback(BlocProvider.of<AuthenticationBLoC>(context).user as AuthenticatedUser)
: BlocProvider.of<AuthenticationBLoC>(context).add(SignIn());
I think the idea of encapsulating the bloc and its capabilities inside the scope is clear.
There are comments and a list of methods. Itβs not difficult to understand how it works. You donβt need to write a bunch of imports and deal with events and states.
Pitfalls
π If you consider that you will create a BLoC in initState or didChangeDependencies, and add events to it in the same place, then they can be quickly processed, and the results states will fall into the first build StreamBuilder or BlocBuilder.No. The event loop doesn't work that way. Until the first build, events will not even begin to be processed, let alone release states based on them. And in the case of StreamBuilder - the first snapshot will always be what you set it in initialData.
π If you start writing logic directly in mapEventToState or the BLoC constructor, it will quickly turn into an unreadable huge piece of code, and you will complain about the boilerplate.
If you prepare the BLoC correctly, the boilerplate does not smell.
Events + states + bloc fit all together on 1-2 screens.
Everything is incredibly readable. You don't need to create separate files for events and states.
π Mutable states - no and no, should all be marked @immutable, and all data classes should contain only final fields.
Payload/Load/State data is also affected.
It is advisable not to blink and wrap the lists in an UnmodifiableListView, including list (very often mistake), or donβt mutate them without coping.
If for good, this also applies to events, but the price of a mistake there is not so high.
π Create a repository directly in the BLoC, and it's even worse to deliver it inside via service locator (e.g., get_it) or singleton.
The repository can be here only through the constructor. That's all, no options.
π Try to create your own from scratch because a BLoC is a pattern, not a package.
Of course, if you are not an experienced architect with many resources and have time for tests, documentation, and support.
And also have a huge community of contributors ready to help you in this task.
Umm... Well then, what are you doing here?
The main problem is that with seeming simplicity, you will likely make mistakes that future generations will have to pay for. And documentation, tests, and support will not be expected.
π BLoC should not have additional public methods, getters, setters, or variables.
If you can't do something through pub/sub (add/listen), you are doing it wrong.
π Please don't use a cubit. The main reason for using a BLoC is scheduling and protecting against race conditions. Using a cubit is no different from using a change notifier, stream controller, or something like that (except for observer and error handling).
This is not a publisher-subscriber pattern but an observer pattern.
Since there is no processing order, you can get inconsistent states.
There are two exceptions to the rule if you want to:
- Your qubit has no public methods. Its states result from a stream of changes on the part of logic. For example, web sockets, location, connection, or battery status.
- You are new and still not comfortable with the concept of streaming. It's okay to start with a simpler concept at the expense of app quality.
π Do not register multiple "on" in the constructor.
This point is identical to the previous one.
You can get inconsistent states even in the simplest logic, like working with a list.
This is a huge mistake in the design of the bloc package, which was made to simplify interaction with event transformers.
Try to organize the routing and handling of events using only one transformer.
Violating this rule, it is quite difficult to maintain the code and to predict the behavior of handlers. And the risk of inconsistent conditions and race conditions is seriously increased.
Break this rule only if you understand what you are doing. The picture shows an example of routing using one transformer and a matching pattern from freezed. Try to do the same.
π Do not respect the uniqueness of the states:
1) Instead of creating a new state object, you pass an existing instance by reference
2) Move the object from the previous state to the new one by reference
3) Forget about the overridden equality of the state and its payload
4) Took a list from the previous state by reference, changed it, and put it in a new state
In each case, you run the risk of being unpleasantly surprised.
And do not be afraid to create new instances of data classes. The garbage collector is optimized for this. You create hundreds and thousands of widgets for each rebuild in a flutter.
π You forget about the order of events.
Be sure to keep in mind that the order in which events are processed is very important!
π You don't REthrow unknown errors. This isn't nice.
The BLoC will not pass errors to Widgets. Please, don't catch or rethrow the exception if you don't know exactly what happened. Errors of all BLoCs will be in the bloc observer, from where, from one place, you will already send them to Crashlitics or Sentry.
π Starting from the bloc (including), there should be no flutter imports.
The exception, perhaps, is a few entities from flutter/foundation and dart:ui, which can be replaced by third-party universal packages that do not depend on the flutter SDK (annotations, isolates & method channels).
There shouldn't be widgets or colors or something like that. Of course, there shouldn't be BuildContext (Element).
π Do you think that BLoC is a presentation layer? Of course not, Business LOGIC Component it's not a presentation layer at all.
Just as it is not a widget (they are configuration layers).
If you are tormented by the name of what to call it - do not torment yourself. Name the directory and the layer: widget, bloc, and data.
Nevertheless, remember that BLoC is not a presentation layer and certainly not related to widgets or a flutter. The main reason BLoC exists is to be separate from Flutter and widgets.
π Using the hydrated_bloc package. No one should use it.
Huge problems with the very concept of a saving state. You don't need to save the states processed & successfully, and so on. Since their restoration will not make any sense.
And the usage of the hive package as a database turns meaningless into extremely dangerous.
hive is one of the most deceitful packages in the pub.
Its documentation is silent on many things and is outright lying in some places. And believe me, you will often change the state structure, while the hive does not even come close to supporting migrations.
Try to avoid these packages as much as possible, as you avoid GetX and get_storage.
If you need to save and restore the state - implement it through the repository manually, I suggest taking SQLite (e.g., drift package) or shared_preference as storage.
π Blocs cannot be extended with additional public methods and getters. Just don't do it. And try to avoid it as much as possible.