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 / freezed packages (all by the same author. This guy is awesome…) .
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 by date in ascending or descending order.
・The completion flag can be made permanent by pressing the completion button.
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, Client, 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.
Client is a class that pulls data, referring to API clients, Firestore, SharedPreferences, etc. An implementation example in SharedPreferences is below. If you want to use it at production, you might want to include error handling and so on. I won’t explain this class in detail because it’s not the main purpose of this article. The important point is the line defining the Provider. This will allow you to access it from ProviderReference.
Entity is a container of data that corresponds to the schema of the database. It is sometimes called something else, but you can convert it to something else as you see fit. Entity is used everywhere from displaying the data to fetching the data, using the Freezed package to get the copyWith function and fromJson/toJson, see here for the Freezed package.
You can write the process of CRUD data in the Repository. If you define the process in the abstract class (so-called interface), it is convenient to add a dummy implementation or connect to an external DB later. In the actual implementation, we fetch the client via ref.read and CRUD the data.
State is a place to store the state in memory. We also define information such as Getter, which is calculated from the values of other states. This time we have 3 states, which are defined as StateProviders so that the View and ref.watch are automatically notified when the state variable is changed. Since _todos and _sortOrder are not directly accessible from View, we define them as private variables.
1. Todo list value (_todos)
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 (_sortOrder)
define sort order as enum
3. Sorted Todo list value (sortedTodo)
The calculation is based on the values of _todos and _sortOrder. If they are changed, they are automatically calculated again.
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.
Finally, let’s take a look at the View, and following is the full code for the View, which extends HookWidget and allows for functions to access a Provider, such as useProvider. The reason we’re using _currentTodo and not passing any arguments to the TodoTile is so that the TodoTile can be generated as a const, which is expected to be more efficient for drawing. The useEffect is a bit trickier to explain, so we’ll follow.
Basically, in the build, we just use useProvider to access State, and in the onPressed, we just use context.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.
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.
<<Summary of this architecture>>
✅ Taking full advantage of Riverpod.
✅ 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 if you don’t need to use StateNotifier.
✅ 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 site: https://github.com/ttlg/riverpod_todo
If you have any improvements or questions, please leave some reaction😄