Yet another Unidirectional State Flow architecture for Flutter

Suson Thapa
11 min readAug 14, 2021
Photo by chris robert on Unsplash

Being both Android and IOS developer I know the pain of working with both platforms. Implementing a feature both in Android and IOS is no fun with duplicated code and quadrupled headache. But this blog is not about why cross platform development is good or bad. So, let’s dive straight into topics.

After successfully (kind of 😄) implementing USF(Unidirectional State Flow) in android I wanted to do the same in flutter. I have learned state management is one of the key for developing maintainable application. Implementing USF in android was easy and fun as there was literally nothing built in android for managing state. But with flutter every developer is like 😜

Flutter out of the box provides state management with Stateful widgets and it’s fine. There are other libraries like Bloc, cubit, Riverpod for state management. I will be using Riverpod for state management.

Why use library?

If you have read my blog about USF in android you can see that it’s similar to Bloc(Except for Result and Effects). After implementing this pattern in one of my project I can tell you that this approach is not very efficient for indie developer (should have known before 😆). So I didn’t want that in flutter and decided to use Riverpod.

I could have implemented the architecture by myself (like I did in android which I shouldn’t have done, there were good libraries like MvRx) but one of the problem that I found out later with my android implementation was state conflict. What I mean is that If I set the state and then read it afterwards then there was no guarantee that the state has been set(Oops 😮). This problem leaked to domain and data layer (as I had to solve it and can’t just implement with other library overnight), so I decided to use library for state management in flutter. Enough about my mistakes, this blog is not going to my resume 😆.

Here is the link to the project for those who understand code more than the documentation. To run the app make .env file in your project’s root directory and add you OMDb api key.

API_KEY='your_key'

Prerequisite

These are the libraries that I have used in this project. If you are planning to read the source code I expect you to be familiar with these. But let’s be honest who comes to medium for reading code, we are here for the GIFs 😅.

  1. Riverpod: For state management
  2. Fimber: For logging
  3. Json_seaializable and Freezed: For creating model class with json serialization
  4. Rxdart: For implementing observable pattern in repository layer
  5. Get_it with Injectable: For dependency injection
  6. Retrofit with Dio: For network layer
  7. Mockito and test: For testing

Architecture

Architecture for flutter

I will be using MVVM pattern with a little taste of clean architecture. The architecture is pretty simple.

  1. View: Calls ViewModel’s function whenever something happens in the UI (event) and observers state exposed by the ViewModel.
  2. ViewModel: Manages the screen’s state and coordinates with Repository for fetching data.
  3. Repository: Provides single source of truth by managing different data sources and errors associated with them.

Development Flow

Most of us start our project with the presentation layer. First creating the Widget, then ViewModel then Repository and finally ApiService.

This way of development is intuitive as you first develop the UI which you can visualize. This gives us the feeling of building something and we spend most of the time pixel perfecting the UI as per the design. Then we move to ViewModel where we might discover some UI bugs then we iterate few times to fix the bugs. Then we move to Repository where we might discover some bugs related to UI and ViewModel and so on. When you reach the last layer

I don’t know what others call it, I call it BDD(Bug Driven Development) 😆.

But with USF architecture we can do that in reverse.

This way of building a feature might seem counter intuitive, let me explain. It’s like building a house. When you start from UI it’s like building your house from top. You build the top floor (just imagine) and then move down to base. Well you can see the problem right? If you find any issues in the base you have to adjust everything above it to fix the problem.

With USF you are first building the base then making it stable by adding test cases. After everything is stable we then build the UI and if you encounter any problems most of the time it’s related to Widget.

Let’s get to Business

We will be building a simple app that let’s you search movies from OMDb. You can add the movies to your history list and view movie details. Here is the demo.

So let’s start with the data layer. This will be our Api service.

Now for the repository.

What the hell is Lce(more info here)? Lce stands for Loading, Content and Error. It’s just a wrapper around the actual data (Kind of like Result in kotlin).

If you are not familiar with reactive programming then let me explain what the getMoviesFromServer() function does.

For those visual learners here is what it does 😉

First we are getting movies from the network which returns Stream<SearchResponse>. Then we use map to transform it to Lce. We check if the response is True and return Lce.Content otherwise we return Lce.Error. Then there is onErrorReturnWith() which handles any exceptions (like http 404 to any exception thrown in map function). We just catch the exception here and return our wrapper Lce.Error. Then there is startWith that just emits Lce.Loading whenever the function is called.

The main benefit of doing this is separation of concern between ViewModel and Repository. ViewModel is not concerned whether the error returned by the repository is error from network or any other exception. Like wise ViewModel doesn’t care whether the loading comes from actual network loading or just some long running computation process. These details are all encapsulated in Lce.

Presentation

Now let’s move to presentation layer starting with ViewModel. Create a abstract class BaseVM which will be extended by our ViewModels.

I think the comments in the code are self explanatory. I am extending ChangeNotifier instead of StateNotifier the reason is to get more control over setting state. With StateNotifier, it automatically notifies listeners when we set the state which will cause issues when we implement navigation.

One of my pain point when implementing USF in flutter was navigation. For navigation we need BuildContext one way or other and it is only available within build() function. So we need a way to encapsulate navigation within state unless you want to implement navigation like this in the widget itself.

This approach works fine for simple projects but when the application grows so does the navigation logic. Sooner or later your are going to have something like this.

It is hard to test and reason about when you have complex navigation logic. So we need to figure out a way to move this logic to ViewModel so that it can be unit tested. This also makes the View dumb which in my opinion is a good thing.

With flutter everything is a function of state so we need to incorporate navigation within our state in a maintainable and scalable way. After some research I came up with this type of state for home screen.

Dissecting the HomeState we have the following members

  1. searchResult: List of movies for our query.
  2. contentStatus: Wrapper just like Lce but for presentation that represents the state of data in a way that’s easy to parse for the view. Here is it’s implementation.

3. history: Represents a list of movies that we have added to history.

Let’s talk about the nav variable. This variable encapsulates the type of navigation and the arguments needed by that navigation. HomeNav is an enum that represents the type of navigation. Here we have none as default and details that represents the navigation to details screen. The args in HomeNavArgs represent any data that we might need to pass to the screen that we are showing. In case of details it would be the selected movie.

With navigation in place let’s implement our HomeViewModel.

In resetEffects we are setting only the state (without notifying the listener) with default nav value. This is necessary as we are using state for navigation and not resetting it will cause the navigation to trigger whenever there is state change shown by the bug below.

Here we navigated to details page that was added to state, and when we come back and then add movie to history it sets the state which triggers the navigation.

We can also use resetEffects() to reset other variables like showing Snackbar which should be shown only once. This function is the reason we are using ChangeNotifier instead of StateNotifier as we don’t want to trigger any state change when resetEffect() is called. We can use StateNotifer but calling setState in resetEffect() will cause the view to render and I think that’s waste of CPU (may be not, flutter might be smart enough to know no views has changed and ignore the state update, I don’t know 😉).

If you are still reading this blog, then you guys might be wondering where the hell is the UI. Well it’s here or is it?

Testing

One of the focus point of this architecture is Testing. So first we will write some test cases to verify that the ViewModel is working as expected. You can even use TDD if you like it.

Coming from Rxjava in android, they provided TestObserver which we can use to count, and verify specific items are emitted in specific order. Dart also provides testing support for Streams with StreamMatcher but I was hoping for something like TestObserver which I didn’t find. As a good developer I created an issue in RxDart github repository 😉. Streams are quite difficult to test without a proper testing framework as they are asynchronous code and might result in flaky tests due to synchronization issues. I spent my whole day trying to reproduce the testing that I used to have in android with Rxjava. With the power of copy and pasting codes from Riverpod and Bloc, I built some utilities to help with testing.

First let’s create some utilities for testing.

The function vmTest is just the copy of blocTest. The equalsValue just makes it easier to test with expected and actual parameters. I was looking for truth library like functionality but couldn’t find it. Let me know if you know anything like that.

Next we going to create TestChangeNotifier inspired (copied) from Rxjava’s TestObserver.

I believe the comments are self explanatory. Let’s look at the code for testing the ViewModel.

We are using mockito to mock the repository. The isSuccess variable is used to toggle the response of the repository. The name of the tests are self explanatory. One of the things that caused me pain was the asynchronous nature of Streams. Let’s look at this function.

When we call searchMovie() it won’t run immediately even if we have mocked the repository as this function is asynchronous. So the next line tester.emitsItemCount() fails as the searchMovie() hasn’t updated the state yet. Being new to flutter I was desperate to find a solution and finally after searching I found the solution in the Riverpod’s testing docs.

await Future<void>.value();

With this line added after any async function (remember TestChangeProvider.wait()) we wait for the function to complete and then only run the next line so our test passes.

This architecture is so powerful you don’t even need UI testing(Don’t blame me if your QA finds bug 😉). As you can see we are able to unit test the navigation as well as other cases when there is no result. The sky is the limit, you can write as many tests as you want to verify your ViewModel’s integrity.

Since this example is simple I haven’t included the unit tests for repository but you can easily test it following similar pattern. But most of the time repository layer just map the network response to domain model and there are very little things that can go wrong here.

Finally we can build our UI.

With these testcases if your UI doesn’t work like expected then you can be 99% sure it might be the problem with the UI itself (if you have good test coverage) as the ViewModel’s behavior is already tested. This reduces the scope of code for finding the bug, we just have to look in the Widgets (sorry for those unlucky 1% 😜). I’m not going to explain the UI in details as I’m still beginner in flutter. If you have come this far in this blog post then I assume you have good knowledge of flutter UI. Let me know if I have messed up somethings.

This is our home screen, it’s long so just read like you read any other documentation.

There are few things that I will like to highlight. Let’s look at _MovieListWidget .

I have used HookWidget so that we can use useProvider for diffing with select function. This causes the widget to only rebuild when searchResult changes. This might look like overkill but believe me when the application grows you scramble for every bit of optimization that you can do and if your architecture is bad from start then good luck with that 😅.

The other thing that eagle eyed reader have noticed is providerListenerAutoDispose. What the hell is this?

This is just a top level function that helps in navigation.

We are listening to state changes and resetting the effects afterwards. The routerVM is a Navigator 2.0 router. You can reference the source code for details on router, as well as theming. Enough flutter for this post. If you have any questions I may not be able to answer it so google it first (just kidding, although I’m a beginner in flutter I will still do my best to help you 😄). And if I have done any mistakes let me know, so that I can learn flutter.

Thank you everyone who stuck till the end 🙏. As a bonus here is the link to the repository so that you don’t have to scroll to top 😅.

--

--

Suson Thapa

Android | iOS | Flutter | ReactNative — Passionate Software Engineer