How to Build an App with Flutter – Part 6. Refining the Widgets Layer with Provider
Learn how to build your first app with Flutter. This time, we’re focusing on refining the widgets layer with the provider package.
Want to build an app with Flutter? Follow our series “Flutter app development tutorial for beginners”. We’ve published the following posts so far:
- How to Build an App with Flutter – Part 1. Introduction
- How to Build an App with Flutter – Part 2. Project Setup
- How to Build an App with Flutter – Part 3. Home Screen
- How to Build an App with Flutter – Part 4. Styling the Home Screen
- How to Build an App with Flutter – Part 5. Networking and connecting to API
- How to Build an App with Flutter – Part 6. Refining the Widgets Layer with Provider – you are reading this
What’s more, we’ve also prepared a Roadmap that can be useful in your Flutter development journey:
- Roadmap for Flutter developers in 2020 – feel invited to contribute!
We hope that, whether you want to be a freelancer or work at a Flutter development company, our series will help you to become a Flutter developer and build your first Flutter app.
Problem and solution, right away!
As we know, Flutter’s UI is declarative. We go down the widget tree and we get to know the view’s structure. This includes the parameters of particular widgets, the declared business logic, and even application logic! The basic problem in Flutter app development I’ve dealt with is that business logic tends to mix with application logic. It makes widgets hard to read in its structure and has too many responsibilities.
Because all of this, it’s also hard to test the application. That’s why, at the beginning of every project as well as when we’re getting to know about the project’s functional requirements (Product Discovery), it is the first problem that we have to deal with. In this part of our series, we will show one of the possible solutions.
What is business logic and application logic? Let’s explain briefly!
At first, it will be worth briefly reminding ourselves what business logic is, as well as application logic, and why we should have as little application logic as possible contained in our widgets.
Business logic defines tasks that need to be done but doesn’t present exact implementation.
Application logic is the implementation.
One of the simplest examples of business logic can be Dart’s abstract class. This class defines WHAT will be achieved by implementing this abstract. The code within the designated methods and variables is indeed application logic, which specifies HOW the task will be done.
The widgets layer as a neat interface for developers
And now we can answer why widgets should contain as little application logic as possible. Widgets are the highest layer in Flutter application, while down below we have
RenderObjects. On a daily basis, a Flutter developer will mostly deal with the widgets layer, which is an easy to read interface that tells us what could be displayed on-screen and which components we can interact with.
In the widgets layer, we will not see how Flutter’s framework plans its next animation frames, how it will refresh the
Elements’ tree or even how
RenderObjects will be drawn on the screen. Widgets because of which layers are placed in should be only blueprints of the application flow, which don’t define concrete implementation, so they need to only implement business logic.
Of course, achieving such a complete extraction of the application logic in the widgets layer is hard and expensive. However, we should always try to reach this state. With this brief explanation finished, we can move to the main part of the article.
Let’s introduce the
provider is a package created by rrousselGit. It is recommended by Google as a component for simple app state management used in Flutter. Previously, they’ve been recommending
BLoC pattern. Here is the most popular package made by felangel.
Provideris a convenient wrapper on
InheritedWidget, which significantly improves work with the data that we transfer through widgets’ trees.
In my opinion, the most important feature of this component is the unified initialization and the ability to retrieve data from
InheritedWidget. Every developer should take into account that what he has written can be used by another developer and, hence, the code should be at some level written in commonly used and respected standards.
Clean code is also a well-known code. That is why we create our projects with already battle-tested architectures, as well as design patterns, and stick to code standards such as the Effective Dart style guide.
Now I will describe the
provider‘s flow. If you prefer documentation, just get in there buddy and you can skip this paragraph. We place
provider(as a subclass of InheritedWidget) in the widget tree and declare an object that will be provided. In the next step, we just retrieve the provided object from the context with a few context extension methods defined by the package. With these methods, we can refresh the view only when the desired field of observable object changes.
Let’s get to the code!
As we already know how to use the
provider and what it is, we can introduce it to our Smoge application.
I’ve previously mentioned adhering to standards. Besides the standards that are present in many projects, we also create those that we keep internally. The development team creates such rules. The
provider package establishes initialization and data retrieving policies. We, as developers, need to figure out a way to integrate
providers and observable models with our application.
We need building blocks, first:
Let’s begin with the model that needs to be injected into the
provider. As the PollutionRestRepository launches requests to GIOŚ (Chief Inspectorate of Environmental Protection) API, our
provider needs to inform it about the
result of a particular request. This is why we need a model that is capable of indicating a change in the value that we observe. In this case, our model needs to extend the
The class’s name is
ProviderModel. This is a generic class containing a data object. The constructor initializes this data object with the initial value.
If we use
ProviderModel, which extends ChangeNotifier, our
provider should be able to handle such a subclass. That is why we need a
provider that will trigger tree rebuild when the model indicates change.
ChangeNotifierProvider created by the
provider package should do the work. Here, I’ve placed a widget into
This class is also generic and it is initialized with a child and function that returns the
provider model of the type declared in
ProviderWidget class. In the
build function, we return the mentioned ChangeNotifierProvider. Even though the lazy parameter is set to true by default, I just wanted to show this feature of
provider, which initializes only when it is needed.
Because the BuildContext parameter in the build function returns the context of the previous widget, we have to be sure that the injected
provider will be available from the child level. We’ve wrapped the provider’s child with Builder. Thanks to Builder, the child will be able to get the
provider from context because the
provider will already be below the created tree. Now we can easily wrap widgets that need
Let’s define a data object that will contain pollution data and will be handled by the implementation of
ProviderModel. I have named it
As we can see, it has fields that represent the
results of requests defined in
PollutionRestRepository. Every field is a type of extended
This class will allow us to control the asynchronous status of setting the nested
result field and it will inform the
provider about the change of a particular field from
PollutionProviderModelState. Everything will be handled by a
set() method that gets a Future object. It contains the
Result to be handled and callback of data change.
The next class is the implementation of
ProviderModel, which takes care of pollution data handling from PollutionRepository. It is
ProviderModel with the
PollutionProviderModelState type, which is an observable object of our
PollutionProviderModel contains methods that set particular fields of
PollutionProviderModelState using data downloaded from object implementing
Each method invokes
set() method of particular
ProviderModelAsyncResult variables in
PollutionProviderModelState. We get
result from the repository and the callback is the
notifyListeners method that originates from
notifyListeners calls all registered listeners about the possible change and can result in rebuilding the widgets’ tree.
Putting them together!
As we already know all of the components, we can finally add
provider to the
HomePage is initialized in
NavigationContainer. Let’s wrap
ProviderWidget of the type
create function, we return a
build() constructor of
PollutionProviderModel that injects
PollutionRestRepository, which was previously placed directly in the
HomePage. Now, after retrieving the
provider from the context, we will be able to interact with it. I’ve changed the
_buildStationName method and now it looks like this.
In the beginning, we use the
BuildContext extension method, selected in order to observe changes of a particular field of
ProviderModel. The widget tree gets rebuilt if an observable variable changes. Thanks to this, we can control the whole asynchronous process of downloading data from
PollutionRestRepository. Observable value is of the
ProviderModelAsyncResult<PollutionStation> type. If the
result doesn’t contain any data, we display
CircularProgressIndicator. After retrieving the data, we basically unwrap the
results and display a station name or error description.
Now we have the last thing to do. We have to trigger the station name request. Flutter documentation recommends invoking any of
InheritedWidgets’ methods in didChangeDependencies function, as this is the first method invoked after
initState. Just for reminder’s sake, we cannot invoke
BuildContext.dependOnInheritedWidgetOfExactType within the
As you can see, we’re making sure that our injected
provider will be invoked only once.
Finally, results … kind of!
Let’s run the app. As you see, nothing changed. Maybe end users will not be amazed by our changes but the developer will surely be happy to see downloading pollution data logic extracted from the widget layer. The
HomePage only gets information about the operation’s
result and doesn’t know about any required dependencies to accomplish this task. The widget only knows what to display.
Separating business logic from application logic is a common practice in programming, especially in commercial development. The more complex a project is, the more we benefit from this approach.
In the Smoge app, we could of course ignore these practices and make it the easiest way because we will not release this app and will not get profit from it. However, the main purpose of this blog post series is to show you how to write commercial apps that have to be easy to maintain and readable for the whole app development team.
Stay tuned for the next article in this series, it will be about writing automated tests. In the meantime, check out in10 – RSVP & ETA Tracking App which we made with Flutter.