Flutter | Pragmatic Architecture using Riverpod

I’ve come up with a pragmatic Flutter architecture using Riverpod, so I want to share it in this article. If you’re wondering what Riverpod is, check it out first. It’s a new Flutter state management package. This architecture mainly use hooks_riverpod / flutter_hooks packages

日本語版はこちら

What we will build

We are going to build a minimum todo list. Because it is boring just to save data in memory, I will make it persistent save with SharedPreferences. If you make this external DB, the contents of this time can be diverted easily.

What the app can do is the following.
・The text that was input can be persisted as a Todo item.
・You can view persistent todo lists.
・You can sort the list by date in ascending or descending order.
・The completion flag can be made permanent by pressing the completion button.

Architecture Diagram

Here’s an illustration of the architecture that we’re going to look at. The arrows represent the flow of data. The characters are Entity, Repository, State, ViewController, and View. I wanted to keep the structure minimal, so I have narrowed it down to a few.

If your app is becoming more complex, you can improve it by introducing things like Usecase between the Repository and ViewController.
Entity here refers to a container of data corresponding to DB.
All items except View and Entity are declared based on Riverpod’s Provider.
Let’s look at each of them in detail.

The dependencies

Entity

Entity is a container of data corresponding to the schema of the database. It may be referred to in other ways, but you can convert it as you see fit.
Entities are used everywhere, from displaying data to retrieving data. This Entity can be made immutable to make it easier to handle, and the merits of immutability are summarized by Remi, the author of Riverpod:

However, the cost of making it immutable is that it cannot be modified, and you will need to use functions such as copyWith instead. There is a useful package called freezed that generates these functions for you, but it requires you to generate the code, which tends to confuse you during development. For this reason, I do not use freezed now, but instead use IntelliJ plugin called Dart Data Class to create functions such as copyWith / fromMap / toMap.

Repository

In Repository, we write the process to CRUD data. It is convenient to define it in an abstract class (so-called interface) so that you can easily add dummy implementations or connect to an external DB later. In this implementation, we will get the SharedPreferences and CRUD the data. If you are going to use it, you might want to add some error handling. I won’t explain the details of SharedPreferences since it’s not the main focus of this article. The important point is the line that defines the todoRepository. This will allow you to access it from ProviderReference.

State

State is where the state is stored in memory. It is also where you define information such as Getter that is calculated from the values of other States. In this project, we will prepare three states, and by defining them as StateProvider, View and ref.watch will be automatically notified when the state variable inside is changed. Of these, _todoListState and _sortOrderState are defined as private variables since they do not need to be accessed directly by View.

1. Todo list value (_todoListState)
The reason the initial value of the Todo List is null instead of an empty array is to determine if the value was retrieved and is empty, or if it hasn’t been retrieved yet.

2. Sort order value (_sortOrderState)
define sort order as enum

3. Sorted Todo list value (sortedTodoListState)

The calculation is based on the values of _todoListState and _sortOrderState. If they are changed, they are automatically calculated again.

ViewController

The ViewController takes commands from the View, such as user input, and writes processes that reflect them to the Repository and State. The modified State is reflected in the View.

View

Finally, let’s take a look at the View, and following is the full code for the View, which extends HookConsumerWidget and allows for functions to access a Provider, such as ref.watch. The useEffect is a bit trickier to explain, so we’ll follow.

Basically, in the build, we just use ref.watch to access State, and in the onPressed, we just use ref.read to access the ViewController.

The useEffect called within the build method serves as an alternative to the initState and dispose of StatefulWidget. useEffect’s first argument calls a function that causes a side-effect in the body, and the return value is specifying the functions that will be called when the widget is destroyed, similar to StatefulWidget. Here, we’re calling initState and dispose, which are defined in the ViewController, similar to StatefulWidget (we’re actually using .autoDispose, so we don’t need the dispose function). Then, we can pass an array of keys as the second argument. If the key is changed, the first argument is executed again. Since we didn’t pass anything here, it won’t be called again unless the widget is destroyed.

And finally, don’t forget to define a ProviderScope in main.dart.

Test

One of the merits of building an architecture with Riverpod is that the tests are very easy to write and read. This is where the benefit of defining the TodoRepository in an interface comes in: you can override the Provider’s content by using the Provider’s overrideWithProvider function (here, the TodoRepositoryImpl is set to overridden by _TodoRepositoryImplDummy).
To access each Provider from the test code, the ProviderContainer is defined. It is thought that it is enough to confirm that the State is changed by executing ViewController for the contents to be tested. The following is the code for testing all of the ViewController functions created this time. It’s minimal, but you can see that it can be written neatly.

Finally

<<Summary of this architecture>>
✅ Taking full advantage of Riverpod
✅ Entity is corresponding to DB scheme
✅ Repository fetch the data
✅ Never write any logic in the View
✅ Make data flow intuitive
✅ Repository is declared as a interface
✅ If it is fine by private then declare as private
✅ Use StateProvider to store the data
✅ Declare the state as the smallest unit
✅ Having a page-by-page ViewController
✅ Basically, use .autoDispose
✅ Use Hooks such as useTextEditingController
✅ Use useEffect to initialize the state
✅ Only State and ViewController communicate with the View

I hope you found this article helpful.
There is no right answer to the architecture, so please use it as a reference and optimize it for your own project.
A working example is on the git: https://github.com/ttlg/riverpod_todo
If you have any improvements or questions, please leave some reaction😄
Thanks!

Flutter