Immutable Business Logic in Flutter

David Leybovich
Flutter Community
Published in
4 min readApr 9, 2019

Disclaimer: I know that the described pattern has many names, and MVI is one of them. But one of the things to learn from this article is that you can name your components whatever until you do break the app logic.

Photo by Fatos Bytyqi on Unsplash

In mobile development, it is impossible to live without some mutable state: the device location changes, screen rotation, the battery is dying, and so on. Mutable state leads to undefined behavior much more often that we are ready to admit, and a good thing would be trying to make the app state immutable where possible.

As you see, I use the word state quite a lot here, and that is because the idea of immutable states highly relies on State Machines.

State

In OOP languages we use classes a lot. Languages like Kotlin allow creating classes that are variations of other classes — sealed class. That allows specifying that something is an edge case of something else. For a screen of Reddit posts, for instance, we will have a State and specific ProgressState DataState ErrorState.

Dart doesn’t have such functionality, but we still can use inheritance in a way that describes relations like this. Runtime type checking and casting can help with that.

Creating the state

Let’s unfold that Reddit posts screen idea in a bit more detail.

abstract class State {} // would be the basic classclass ProgressState implements State { } // would be the initial stateclass DataState implements State {
List<RedditPost> get posts;
}
class ErrorState implements State {
Exception get error;
}

As you can see we are only using getters and hopefully will just use List.unmodifiable for the DataScreen. That should make the states truly immutable.

Changing the state

As mentioned, we will use a State Machine to move from one state to another. Some might call a state machine like this a Reducer. To move from one state to another the Reducer has a method called reduce which receives the current State and an Action that needs to be applied for that state. If the State and Action are not related, we usually throw an Exception.

I find it useful to treat Actions the same way as we treat State — one class for each Action. Sometimes an enum might do the job, but we never know when some additional data will be needed for the state change.

There are different ways to bind the Action to a specific State. I will create the actions as classes and check in the Reducer if the action is valid.

abstract class Action {} // the base action to pass to the Reducerclass DataAction : Action { List<RedditPost> get posts;
}
class ErrorAction : Action { Exception get error;
}
class RetryAction : Action { }

And our Reducer would look something like this:

class Reducer {void reduce(State state, Action action) {
switch (state.runtimeType) { // a useful trick to switch by type
case ProgressState:
switch (action.runtimeType) { // please, don't nest switches in production. this is only for displaying purpose.
case DataAction:
return DataState(action.posts);
case ErrorAction:
return ErrorState(action.error);
}
break;
case ErrorState:
if (action.runtimeType == RetryAction)
return ProgressState();
break;
}
throw Exception("no suitable action found");
}
}

The code above is simplified for readability reasons, but hopefully, the idea is clear.

Testing

As you could imagine, testing code like this is pure joy. No one has to deal with callbacks, async operations or multithreading while testing the Reducer.

Testing the positive case would most likely look like this:

test("testing positive case", () {final error = Exception("");
final newState = reducer.reduce(ProgressState(), ErrorState(error));
expect(newState, TypeMatcher<ErrorState>());
expect(newState.error, error);
});

While testing a wrong action would look like this:

test("testing negative case", () {expect(() => reducer.reduce(ProgressState(), RetryAction()), throwsA(TypeMatcher<Exception>())); 
});

Like in the books: calling a method — checking if the result is correct.

Updating UI

Of course, in real life, we have to deal with async operations and callbacks, and putting any async code to the UI layer is not preferable. That’s why a good solution might create an additional layer between the UI and our Reducer.

This layer would hold the reducer, and it’s only responsibility would be to work with any async operations and notifying our Widgets.

class AsyncOperationHandler {final reducer = Reducer();

StateNotifier notifier;
State state;
void loadData() async {
state = ProgressState();
notifier(state);
try {final data = await loadDataFromServerFuture();
state = reducer.reduce(state, DataAction(data));
notifier(state);
}
catch (e) {
state = reducer.reduce(state, ErrorAction(e));
notifier(state);
}
}
}
typedef StateNotifier = void Function(State state)

I decided to store the current state in the AsyncOperationHandle, but different options are available. You could do this directly in the Reducer.

I find this approach highly usable and accessible to process, so free to try it in your project ;)

--

--