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 State
s:
To change between them we have the following Event
s which represent user actions performed when using the application:
We can also visually describe the relationship between State
s and Event
s by a diagram:

Finally, our state machine can also produce SideEffect
s 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+.
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 SideEffect
s 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!? State
s 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.