After creating your first application in Flutter you have to decide which architectural pattern to use. When you first search for them, you will find a large number of possible options: MVP, MVVM, MVI, Scoped Model, Vanilla, BLoC, etc. A while ago, Google advised developers who cannot make a choice to use BLoC, which significantly simplifies managing the state of the widget and at the same time saves application resources. This pattern came to the liking of our team, so we will talk about it today.
In this article we will share some tips based on our experience in Flutter and it will be useful for developers who understand how to work with streams in Dart and the basics of OOP.
To understand the material presented below, the reader needs to thoroughly understand how the application works, which is created by default with the creation of a new project (incrementing a number using a button).
Where do I start?
To get started, create an application using Flutter create application name command. This command will create a working prototype for your application, which contains two texts in the center of the screen (the first with a description, the second with the number of clicks) and a button to increase the number of clicks. This is where we start. We will use one screen to explain the pattern (you can read about application navigation patterns here). Also, to simplify the process, we will not make a second button for the decrement, but rather focus on building a good architecture for the increment.
The first thing that should catch your eye is the call to the setState method. This method indicates to the widget that it needs to call the build method again, and therefore create new instances of the classes contained in the tree. Subsequently, when creating more complex screens, this process can become very expensive for the memory and processing power of the application and even lead to unpleasant and unwanted animation “jumps”. In addition, when clicking on the button, we want to change only the text of the counter, but at the same time, we rebuild the entire screen. This is no good and must be gotten rid of.
First of all, we will create another file, which we will call MainPageBloc. It is designed to store and manage the data that our screen will need. We will create two streams – one will send data to the screen, and the second will receive an increment event. For the second, it is also advisable to create an Enum, so we cannot be sure that the events for this screen will be limited to one increment (remember that it is likely that we will add a decrement in the future). As a result, the MainPageBloc code will look like this:
Now you need to establish communication with the screen. To do this, we will use StreamBuilder, which, when new data arrives, will update only the text, while not touching other widgets on the screen. To broadcast the increment event, we will use the stream we created earlier. An instance of our BLoC is identified in advance when creating a widget class. However, as you can see, now we only use the build method, without using either initState or setState, which means that we need to remake the class into StatelessWidget. The screen code after all the transformations looks like this:
Done! Now all the logic is in a separate file and our user interface does not depend on it in any way. We can increment the counter in any way, and handle the event as we please, for example, by sending a notification that the user has pressed a button to the backend, and the code of the screen itself will not change in any way!
Deepening the architecture and adding abstractions
Now the architecture of our application looks like this:
Application architecture after the first phase
However, you rarely have to make completely offline and local pages, and basically, your screens will rely on data that comes from the server, process it in some way, and display this data to the user. To do this, it is not enough just to call a request to the server from our BLoC, because, firstly, this will violate the principle of sole responsibility, and secondly, it will significantly reduce the possibility of the application’s extensibility, which may subsequently lead to the complete impossibility of introducing new functions into your code. Therefore, we will add a server call to our application. This article will not touch upon the topic of server communication, library selection, file generation, and the like, since this does not apply to the topic, so when we get to the moment of sending information to the server, we will write a simple stub. However, it is worth remembering that during the development of the application, some details may change (for example, we will communicate with the server via a socket, and not using REST) and we need to include the possibility of a simple extension or change in the application in advance.
As you can see, our small application can already send and receive a large amount of data and several options immediately come to mind:
1) Sending a push notification to the server;
2) Sending the total number of clicks to the server when exiting the application;
3) Receiving from the server the number of clicks made earlier;
4) Communication with the server via a socket, in which notifications and the current number of clicks will be sent.
For convenience, we will choose the first option, and as a result, the whole flow will look like this:
1) When we go to the screen, the number of clicks is 0;
2) After clicking the button, an asynchronous GET request occurs, which notifies the server about the click.
We will have the following request structure:
1) The user clicks on the button;
2) The event is sent to BLoC and processed there;
3) BLoC calls UseCase to notify the server;
4) UseCase calls the repository method;
5) The repository calls the client API method;
6) API client makes a request to the server and receives a response;
7) Following the same chain, only in reverse order, the answer is sent back to BLoC, which decides what to do with this answer.
A novice programmer may have a question: what is all this long chain for, why not call the API method directly from BLoC? The answer to this question in relation to each subject of the chain will be given below.
Server and UI work in tandem
It is more convenient to move from the screen to the API call when creating the architecture, so we will do that. All the classes created below will be Singleton for easy communication with them, however, for more advanced readers, I would advise looking towards GetIt.
First of all, we create the IncrementNotificationUseCase class, where we define the execute function, in which the repository call will be located. UseCase is useful for us to reuse the code, and if suddenly we need to call the same function in another screen, we will simply call the use case instead of implementing the entire chain. At this stage, the code looks like this:
After that, we create a repository. First, we create an abstract CounterRepository class in which we declare the incrementNotification function. The abstract class code is below:
We now create a CounterRestRepository class that will be responsible for passing data to a specific API. We will override the incrementNotification function in it, but we will not write anything into it yet. The code:
Now we create an abstract CounterApi class with one incrementNotification function inside (as in the previous case) and a CounterRestApi class in which we override this function. It will simply return true (at this point the GET request should have been called).
After that, we create an instance of the CounterRestApi class in the previously created CounterRestRepository class, and in the incrementNotification function we call the incrementNotification API class method and return its result.
Next, we create an instance of CounterRestRepository in the IncrementNotificationUseCase class and in the execute function we call the incrementNotification repository method and again return its result.
And now we can finally call IncrementNotificationUseCase () from our BLoC. Execute (), wait for it to complete, and, if it returns true, send a signal to the screen to increment the counter. It was a long way for our API call and finally, it got to the screen.
Now we should examine the result of our work. We have a use case that can be used on any screen with just one line. In addition, we made an abstract class of the repository, and now, if we suddenly need to send this request, not to our REST server, but, for example, to Firebase, we will write a new class for the repository and API, however, in the existing application code we will need change just one line – defining the repository in the use case, and everything, neither the screen nor BLoC, will even know that something has changed! And this significantly increases the extensibility of the application. The application architecture is shown below:
Complete application architecture
In this article we analyzed the BLoC architecture, and also showed how to add new classes and calls to server methods without any special difficulties and problems for future code, based on our experience in Flutter. I hope this article was interesting and understandable both for beginners and for already experienced programmers. Thank you for your attention.