Improving UI State management by using a Finite State Machine and MVI architecture
In this story, I’ll talk about why I believe we can strongly improve the UI State management between the View and ViewModel by using a Model-View-Intent architecture with the help of a Finite State Machine. It provides more structure to the communication, plus, it improves code legibility, maintainability, and testability.
Before we start, I’ll assume the reader has basic knowledge about Model-View-Intent and also about Model-View-ViewModel.
Who’s Next?!
To explain this idea, I’ll use a personal project called Who’s Next?!, which consists of a simple timer application that lets you know when it’s time to change the goalkeeper in a football game with friends.
Finite State Machine (FSM)
A state machine is a behaviour model. It’s called a finite state machine because it consists of a finite number of states. Based on the current state the machine relies on user input or in-state calculation to determine the next state. I prefer to use deterministic FSMs because they’re easier to reason about, the same input will always result in the same output. When implementing a FSM the syntax to keep in mind is: From a current State on a specific Event we transit to another State and that may produce a SideEffect.
In Who’s Next?!, we can receive one of the following States:
To change between them we have the following Events which represent user actions performed when using the application:
We can also visually describe the relationship between States and Events by a diagram:
Finally, our state machine can also produce SideEffects when transitioning between states:
Later, I’ll use these side effects as “state transition callbacks” to trigger UI State changes but I’ll cover that in the next section.
Implementation
I’ve used Tinder’s State Machine, a Kotlin and Swift Domain Specific Language (DSL) for FSM, and thanks to it the implementation is pretty straightforward:
Let’s recall our diagram and write the FSM code to alternate between both Idle and SettingTimer states:
FROM -> TO by [EVENT]Idle -> SettingTimer by [OnSetTimer]
SettingTimer -> Idle by [OnTimerSet]
And the result is:
Pretty simple. Now that we know how to setup a state machine and how it behaves, let’s see how can we use its side effects to emit UI State updates.
Model-View-Intent (MVI)
It’s out of scope a deep dive explanation about this architecture - there are plenty out there and very good ones -, but briefly, it provides the following:
- Immutable model (state) to achieve the single source of truth principle
- Unidirectional and cyclical data flow
- Thread safe when implemented with functional reactive programming
To take advantage of this architecture I’ve used Orbit Multiplatform library because I believe it offers three no-brainers:
- A very lightweight learning curve
- Composition over inheritance approach (incremental adoption 🎉)
- MVVM architecture update (with KMM support)
And thus, like the authors, I consider it to be a MVVM+.
Get Guilherme Delgado’s stories in your inbox
Join Medium for free to get updates from this writer.
While this library has the same notion of State and Side Effects we use them in different ways. The first represents the UI State at a given point in time, the second is used to perform one-off events like Toasts, Navigation, etc. We won’t be using the latter in this example.
In the heart of the Orbit Multiplatform system we have a Container which is responsible to retain the State and expose two channels to listen for data updates (I’ll describe them shortly). There’s also a ContainerHost interface that allow us to use MVI verbs, we call them operators.
To handle user interaction we have at our disposal three operators: intent, reduce and postSideEffects. Within the first - intent - we can invoke the other two to run business operations to transform data. With - reduce - we atomically create the new UI States by applying transformations to the current one. These changes will be sent automatically to the stateFlow observed by the View. Finally, we have the postSideEffects that sends one-off events to the sideEffectFlow (also observed by the View).
Implementation
Let’s go back to Who’s Next?! application - where we’ve implemented a FSM to alternate between Idle and SettingTimer -, to add UI State update logic.
It’s represented by:
Before we adopt the MVI architecture our ViewModel and View looked like:
With Orbit Multiplatform we change it to:
Almost done, the only piece missing is connecting with the FSM. Let’s first remember its syntax: from a current State on a specific Event we transit to another State and that may produce a SideEffect. I’ve also said that I would use these SideEffects to trigger TimerUiState changes.
Thus, our FSM+MVI becomes:
Have you noticed the only public methods represent an intent to change the state, but they all get double checked by the FSM before using the MVI operations to do so? Pretty cool! 🍒
Conclusions
The following animation show us all Who’s Next!? States available as well as the FSM console output:
Final thoughts
I found it very efficient the use of both this two architectures, even if by that it means we need to add some extra lines of code for the State Machine. It will pay off in terms of legibility, testability and robustness.
The FSM helps us fully specify and validate all the states and transitions available. We can create it even before having UI or business logic.
Regarding Orbit Multiplatform system, we have a very clean and simple MVI (MVVM+) architecture out of the box. It abstract us from the complexity that others libraries don’t, and the fact that it’s built upon a composition methodology it makes it possible to adopt/migrate at our own pace. Fun fact: you don’t even have to use it with ViewModels if you want to.
Also, I believe the single source of truth principle is doubly assured, because with MVI we get that by default, but with FSM we make sure that the only public methods from the ViewModel are the ones that will trigger known State transitions which will produce SideEffects to change atomically the UI State. The FSM acts as an extra layer of security.
Next steps
Wouldn’t it be nice if we could share this architecture between mobile platforms? Go no further:
As always, I hope you find this article useful, thanks for reading and Matthew Dolan for his review.

