Flutter IV: MVP Architecture
Some Flutter User interface creation concepts have been reviewed, the following post will focus on building an application based in MVP (Model-View-Presenter) architecture pattern.
Tip: The whole code is available in this repository.
In this post, it will be explained how to develop an application following MVP pattern, it will be shown how to create an UI layer, a Presenter layer and another layer containing all the data repositories.
The UI layer will contain all the necessary widgets in order to keep the contacts list that was created in the previous post.
Presenters will be used to link the UI layer with all the data gathered from the repositories.
Finally, inside the data repositories layer there will be two data sources: The one that was already created and which could be used as mock data to test the application and another data source fetched from a service outside the application context. In this case, Random User API will be consumed in order to get users.
Let´s start creating the data sources clients. Create a new folder called data inside lib directory. Next step is creating contact_data.dart file inside data folder.
In this file the Contact class and the interface that will expose how to get contacts and also an exception that will notice in case anything went wrong during the data fetch will be defined.
So, the contacts list should be place in another file as a mock implementation.
Check the result:
As it was said, this file contains the Contact class, ContactRepository interface defining a fetch method which returns a Future<ContactList> and FetchDataException.
In order to use Future dart:async must be imported, Future allows working using promises in Dart. An example about this is below in this post.
Our first ContactRepository interface implementation will be a mocked one. Create another file called contact_data_mock.dart inside data folder. In this file a class that implements ContactRepository interface and that returns the contacts list from the last post through the fetch method will be created.
Import both dart:async and contact_data.dart in order to use Future. ContactRepository and Contact.
As seen, a class to implement ContactRepository is created and inside the fetch method a contact list is returned using a Future.
Random User Repository
Its second implementation will be using RandomUser service, this will cover how to fetch data calling a service using Dart.
Inside data folder, create a file called contact_data_impl.dart and add the following code.
In this case, the class implementing ContactRepository interface is RandomUserRepository.
Inside the fetch method, execute a get function to query an URL contained inside _kRandomUserUrl. In order to do this, first import package:flutter/http.dart. Using as operator makes the http variable containing the whole package.
In order to perform the query, use http´s get method. It returns a Future which is Dart´s way of using promises. Future´s then method should be called when the query is finished and all the information was already gathered.
The then method takes a lambda as a parameter in order to notify the reply. It returns body and a statusCode to check if everything is OK, otherwise FetchDataException will be thrown.
Last step is reading a Json, to do so, use dart:convert and JsonDecoder class which takes reply´s body as a parameter.
Let´s check how a Random User Json reply is:
Inside results, you can iterate every element using a Map and turn them into Contact and return the list using toList method.
To do so, a new Contact constructor that allows to create contacts from a Json must be defined.
In this constructor fullName and email attributes are extracted from map and assign by name.
In order to swap between both ContactRepository implementations, some dependency injection is needed. Create a new folder called injection inside lib, then create dependency_injection.dart file and paste the following code.
Flavor enumerator is used to identify both environments mock and production. Injector class is a singleton, it has a private constructor called _internal, a configure method to choose an environment, a factory to return the singleton once the Injector is created and a get method to return the ContactRepository implementation previously selected.
Inside main.dart file, before runApp instruction, execute configure method indicating which Flavor will be used. In this case PRO.
Once the repository layer is finished, let´s create the presenter. Create a two level directory called module/contacts inside lib folder. Then create contact_presenter.dart file.
First of all, define a Contract interface, this will help to communicate the presenter and the UI. Two methods are defined, one to notify when the contacts are already loaded and another to notify if any error happened during the download.
Next step is creating the presenter.
Import following files.
Inside the constructor, the view contract is assigned from and the Injector will select which ContactRepository to be used between MockContactRepository or RandomUserRepository.
Create a method called loadContacts, this is how the view will ask for contacts. Inside this method, get a List<Contact> Future using fetch method and use a lambda to be executed when the repository finishes fetching the data and mapping it into a contacts list.
By the time the data is available, lambda function should call onLoadContactsComplete method using the contacts list returned by the repository in order to pass the data to the view.
If any exception is raised, catchError method will be executed and it should print the error and communicate it to the view.
Use contact_view.dart file from the previous post and place it inside contacts folder, some modifications in the list are needed.
Now, ContactList is not a StatelessWidget anymore. It currently is a StatefulWidget, also a State for the ContactList was created.
ContactListState implements the Contract defined and both of its methods, onLoadContactsComplete and onLoadContactsError.
The presenter is created inside the constructor and initState method the presenter is asked to gather the data.
Inside the build method a CircularProgressIndicator is created to be shown to the user during the data fetch and then the list is generated. This progress is controlled using _isSearching variable, if true a CircularProgressIndicator is returned, if not a MaterialList.
When the presenter has the data, onLoadContactsComplete method is called, use a lambda to call setState and invalidate the widget to force the build method to be called again. This is how _isSearching is set to false and by the time the build method is called again it will return a MaterialList instead of a CircularProgressIndicator.
Error handling will be explained in following posts.
Check the results
Run the applications and see the results.
To sum up
Building an application following MVP pattern turns the application into a more maintainable one, it also helps with testing process among other things. This post shows how Flutter allows to build robust applications easily.
Next posts will focus on explain navigation in Flutter.
This posts is related to step3 inside the GitHub repository.