Category: Blog, Flutter, Development

Refining the Widgets Layer with Provider | How to Develop an App with Flutter – Part 6

Learn how to build your first app with Flutter. This time, we’re focusing on refining the widgets layer with the provider package.

How to develop your first app with Flutter widgets layer

This article is a part of the series “Flutter app development tutorial for beginners”. We’ve published the following posts so far:

  1. Introduction to your first Flutter app development 
  2. Flutter Project Setup
  3. Creating a Home Screen
  4. Styling the Home Screen
  5. Networking and connecting to API
  6. Refining Widgets’ Layer with Provider – you are reading this
  7. Internationalizing and Localizing your Flutter App

What’s more, we’ve also prepared a Roadmap that can be useful in your Flutter development journey:

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.

We will split the business logic from the application logic with the tool recommended by Google, which is the provider package made by rrousselGit.

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 Elements and 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 package!

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.

How does provider work?

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: ProviderModel

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 ChangeNotifier class.

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.

Provider‘s ChangeNotifierProvider and ProviderWidget

If we use ProviderModel, which extends ChangeNotifier, ourprovider should be able to handle such a subclass. That is why we need aprovider that will trigger tree rebuild when the model indicates change. ChangeNotifierProvider created by theprovider package should do the work. Here, I’ve placed a widget into ProviderWidget.

This class is also generic and it is initialized with a child and function that returns theprovider 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 provider.

PollutionProviderModelState

Let’s define a data object that will contain pollution data and will be handled by the implementation ofProviderModel. I have named it PollutionProviderModelState.

As we can see, it has fields that represent the results of requests defined in PollutionRestRepository. Every field is a type of extended result class, ProviderModelAsyncResult.

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.

PollutionProviderModel

The next class is the implementation of ProviderModel, which takes care of pollution data handling from PollutionRepository. It is PollutionProviderModel.

PollutionProviderModel extends ProviderModel with the PollutionProviderModelState type, which is an observable object of our ProviderModel. PollutionProviderModel contains methods that set particular fields of PollutionProviderModelState using data downloaded from object implementing PollutionRepository.

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 ChangeNotifier. Method 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. The HomePage is initialized in NavigationContainer. Let’s wrap HomePage with ProviderWidget of the type PollutionProviderModel.

In 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 initState method.

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.

The purpose

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.