Implementing Redux architecture with Flutter

Max Ermakov
ProAndroidDev
Published in
6 min readOct 28, 2018

--

I recently got a chance to use Flutter in a commercial project. Having tried a variety of architectures before, I decided to combine Flutter with Redux, because it’s challenging and fun. To document the development and share this experience with you, I selected a simple and usual case: the Welcome Sign-In Sign-Up flow.

There are two major prerequisites:

  • Theoretical understanding of the Flux pattern and the Redux framework. Namely, you should know what Store is, and why it’s the Single Source of Truth, what Reducers are, and why they’re Vanilla functions, and so on.
  • At least a basic understanding of the redux and flutter_redux plugins that the Flutter framework uses to implement Redux architecture.

There’s an excellent article by Android developer Paulina Szklarska to help you cover both: Flutter + Redux — How to make Shopping List App?

Proceed when you’re ready :)

I have three screens:

  • a Welcome screen with a logo in the middle, where we check for an authorization token;
  • an animated Sign-In screen, to which we transfer if there is no authorization token;
  • a Home screen, to which we transfer if there is an authorization token (this screen is mocked);
  • a Sign-Up screen, to which you can get by pressing the dedicated button.

Here’s how all of it should look like:

I started this project by describing AppState, AppReducer and Store entities. As you know, these entities are global, but State is also immutable, which means that you have to recreate it whenever making any changes. In my particular implementation, AppState serves as host for the States of screen modules:

This is also true for the AppReducer:

The SignInState composed in the AppState contains a respectable chunk of business logic:

Take a look at SignInState copyWith(….) , which creates a new state by substituting properties in the previous one.

I used combineReducers<SignInState>() as a reducer for SignInState. Operating on a simple concept, it takes as an argument an array of TypedReducers: it’s basically a reducer that consists of many separate reducers, which is very comfortable for structuring the project.

Here’s how my SignInReducer looks like:

Each TypedReducer’s second argument is Actions. Actions are entities that change the AppState. There are likely to be many of them, as only Actions can trigger State changes. Usually, Actions are simple data classes that may or may not contain data. Here are some of the Actions I created for this case:

I used a similar approach to create SignUpState and SignUpReducer. You can find their code in the project repository, the link to which I left at the bottom of the article.

It doesn’t take much time and effort to create Store:

As you can see, Store takes the main AppReducer, AppState instance, and the Middleware array as its constructor arguments.

Middleware are entities responsible for side-effects and business logic-related work (often asynchronous). Actions go through dedicated Middleware, which either declines or lets them through to Reducers. Middleware can also dispatch new Actions as necessary. For instance, here’s Middleware that deals with field validation:

At this point, we’re done with all Dart-based parts of the project. Now let’s see how to implement this flow in terms of Flutter.

First, we create a Store entity in the main function. Then we wrap the App widget in StoreProvider. Here’s how this looks like in code:

I also added a NavigatorKey as a static instance of GlobalKey, and moved it to a separate file — we’ll now use it for navigation from Middleware.

Let’s discuss StoreConnector, an integral part of Redux in Flutter.

StoreConnector is a widget that connects the ViewModel (which we’ll get to in a bit) of your Stateful Widget with the entities of AppState and Store. It’s usually located closer to the root of your screen widget hierarchy (right after Scaffold and SafeArea in my case), so that StoreConnector wraps the entire contents of that screen. Take a look:

StoreConnector has two required properties, converter and builder:

  • converter is a function that accepts Store as an argument, then creates and returns ViewModel;
  • builder renders UI based on the newly created ViewModel.

Apart from that, StoreConnector has a number of useful property callbacks, such as onInit, onWillChange, onDidChange, etc. You’ll see my implementation of onInit after we deal with ViewModel, and the rest we’ll use in an imaginary case shortly after.

Callbacks are great, but ViewModel is the most important artefact that StoreConnector has to offer. ViewModel is the main entity that we work with in terms of a single screen.

It’s a shame that most Redux tutorials skip such an important concept, using something like an item list instead of ViewModel. Here’s my ViewModel for the SignIn screen:

As you might have already figured out, the first part of class fields initialize widget state. Meanwhile, functions are triggered with either events or user actions.

Functions in ViewModel can only dispatch new Actions — otherwise, the Single Source of Truth principle would be disturbed. It’s quite easy to use ViewModel to initialize UI, take a look for yourself:

Basically, we’re doing exactly the same as we were doing before with the Stateful Widget’s state to initialize presentation and dispatch Actions based on events, which recreates AppState, which changes ViewModel, and so forth. Let’s take an example of email input to see this process in more detail:

Each inputted symbol invokes ValidateEmailAction, and forwards it the input string. The action string (a.k.a. inputted email) is then validated based on a regular expression in ValidationMiddleware. If the email is valid, ValidationMiddleware returns an EmailErrorAction with an empty string. Otherwise, it returns an error message, which changes AppState. ViewModel then contains that error message, but doesn’t display it yet.

Based on the Login button press, ViewModel sends a ValidateLoginFields action that goes to ValidationMiddleware. If there are any error messages, ValidationMiddleware changes AppState and ViewModel status property to LoadingState.error, and displays all non-empty error strings to the user. If the EmailErrorAction contains no errors, ValidationMiddleware sends a SignInAction, which goes to RestMiddlevare. If the request returns 200, RestMiddlevare sends a NavigateToHomeAction, which then uses NavigationMiddleware to transfer to a different screen that contains its own State and ViewModel:

Although this might sound complicated and as a bit of an overhead, the coding process is rather simple. And if you use additional tools like redux_logging and dev_tools, the code becomes self-explanatory.

Now, let’s get back to the StoreConnector callbacks, as the article would be alarmingly incomplete without them. A simple case: whenever getting to the Welcome screen, we need to check for an authorization token — if there is one, we get inside the app, and if there isn’t — we go to the Sign-In screen.

StoreConnector’s onInit callback is perfect for this. OnInit offers us a Store instance, which means that we can send Actions. When we send a CheckTokenAction, it goes to LocalStorageMiddleware, and checks for the token in SharedPreferences. Here, you can take to major paths.

A simpler one: send two callback functions inside the CheckTokenAction: noToken, which launches a transformation animation of the SignIn screen, and hasToken, which should transfer to a different screen. I trigger these callbacks from LocalStorageMiddleware — because side-effects are still happening within Middleware, I decided that this wouldn’t be an architecture violation:

A cleaner one: send a separate event targeted at changing this module’s ViewModel. We could, for instance, sign up for StoreConnector’s onDidChange callback, which would be triggered whenever the ViewModel changes, check that the changed parameter is exactly the one we need (as ViewModel changes constantly), and then either launch the animation or transfer to a different screen:

The only major drawback of the second approach is that you would have to write more code. All in all, they both achieve the same goal, and the choice is up to you.

Project repository here.

I just started figuring out the proper ways of Redux, and a lot can change in my understanding of it by the time you read this. If you have any tips or thoughts on how I could improve any of the abovementioned code, please share them, I would much appreciate it. That’s it for me for now, I wish you all the luck with Flutter, and will see you later in another post.

--

--