Finite State Machines + Android + Kotlin = Good Times

Alex Sullivan

I was recently listening to a fantastic episode of the Fragmented pocast where the guest, Ray Ryan, mentioned that they make heavy use of finite state machines at Square. Inspired by that podcast and a Github gist detailing the use of composable finite state machines in Swift, I decided to try and utilize a similar concept to build an Android application, with the goal being to come up with an architecture that both explicitly manages UI state and allows screen complexity to grow in a maintanable manner.

What is a finite state machine?

If you’re not familiar with the concept of a finite state machine, you can think of them as a set of `States` with pre-defined edges between them. An `Action` is fed into the finite state machine, at which point the finite state machine produces a new `State`. They’re typically drawn as circles representing states with lines connecting them representing transitions:

So in the above example of a turnstile finite state machine we have two `States` - `Locked` and `Un-locked`. We also have two possible `Actions` -`Push` and `Coin`, which represents depositing a coin into the turnstile. All finite state machines follow the following simple formula we alluded to above:

State + Action = new State.

So in the turnstile example above, we can model our finite state machine with the following formulas:

`Locked` + `Push` = `Locked` - A locked turnstile remains locked when pushed

`Locked` + `Coin` = `Unlocked` - A locked turnstile unlocks when money is inserted

`Unlocked` + `Push` = `Locked` - An unlocked turnstile locks after its used

`Unlocked` + `Coin` = `Unlocked` - An unlocked turnstile remains unlocked if you insert more money

How does this fit into Android development?

When building up a screen in an Android app, we often find ourselves wrestling with growing UI state. A view can be loading, populated, scrolling, animating, pending and so on. As our views grow, the complexity grows exponentially and we often find ourselves struggling to find a way to properly organize and navigate the increasingly convoluted landscape. Finite state machines offer a unique answer to this problem - they convert your previously unmanaged implicit state into managed explicit state.

Designing our application

For this demo we’ll build a minimal single `Activity` app focused on managing a users love of sandwiches (side note - it turns out I had no idea how to spell sandwich before I wrote this blog post). Our app will have 3 screens:

1. A listing screen, where a user can see their favorite sandwiches and initiate a flow to add a new sandwich:

2. A sandwich selection screen, where the user can either select a predefined sandwich or select a category for their own, new sandwich: (Note that I only have three images of sandwiches, so please excuse the duplicate deliciousness!)

3. And finally, a sandwich naming screen, where the user can name the new sandwich they started to build on screen #2

You’re probably assuming that I’ve won multiple design awards for the layout of this app. Shockingly I have not.

Defining our finite state machine

It’s worth taking a moment to think through what problem we’re hoping our finite state machine will solve. Currently, our activity has three distinct states that need some degree of coordination - we need to figure out when to hide the sandwich list screen and show the add sandwich screen and so on. Our finite state machine should be the engine that makes these decisions.

Now that we know what problem we’re trying to solve, we can move on to building our finite state machine.

Defining our States

First, we need to decide on a set of `States` for the app.

Since we want our finite state machine to drive the UI, it makes sense to create a `State` in our state machine for each of the screens listed above. Our states will thus be `SandwichList` for the listing screen, `AddSandwich` for the sandwich selection screen, and `NameSandwich` for the sandwich naming screen.

So our set of states is: `SandwichList`, `AddSandwich`, and `NameSandwich`.

Defining our Actions

Next we need to figure out what our `Actions` are.

Since our finite state machine is the engine driving our view, it makes sense that interactions with that view would affect the finite state machine. So we can model user interactions as `Actions` being fed into our finite state machine!

On the first screen, the only action the user can take is clicking the “Add Sandwich” button - so we’ll want an `AddSandwichClicked` `Action`. On the sandwich selection screen, the user can either select a predefined sandwich or select a sandwich category. So we’ll want two more actions - `SandwichTypeSelected` and `PredefinedSandwichSelected`. Finally, on the sandwich naming screen, the only action the user can take is clicking the “Submit” button, so we’ll need a `SubmitSandwichClicked` action.

Our set of `Actions` doesn’t necessarily need to be limited to user interaction; if we received a notification or something along those lines that could be an `Action` for the finite state machine as well.

So our set of actions is: `AddSandwichClicked`, `SandwichTypeSelected`, `PredefinedSandwichSelected`, and `SubmitSandwichClicked`.

Putting everything together

The final step in constructing our finite state machine is figuring out the set of transitions that we have - we want to setup a list of equations for our sandwich app in the same way we set them up for the turnstile above.

`SandwichList` + `AddSandwichClicked` = `AddSandwich` - After the user clicks “add sandwich” we want to transition to the `AddSandwich` state.

`AddSandwich` + `PredefinedSandwichSelected` = `SandwichList` - If the user selects a predefined sandwich, we want to take them right back to the `SandwichList` state with their newly selected sandwich added to the list.

`AddSandwich` + `SandwichTypeSelected` = `NameSandwich` - After the user selects a sandwich type, we want to take them to the `NameSandwich` state to choose a name for their new sandwich.

`NameSandwich` + `SubmitSandwichClicked` = `SandwichList` - After the user submits their sandwich name, we’ll combine that data with the sandwich type they selected before to create their new sandwich and add it to the list.

The following equations lead us to this finite state machine diagram:

Time to write some code

Now that we’ve constructed the theoretical underpinnings of our app, let’s actually write some code.

The first thing we’ll create is an `interface` for a `State` in our state machine:

``````interface SandwichState {
fun consumeAction(action: Action): SandwichState
}
``````

This interface models our equation above - `State` + `Action` = new `State`. Any `State` that implements this interface will have to supply a new `SandwichState` when provided with an `Action`.

Next up is to define our `Action`. We’ll model our `Actions` as a `sealed class` to utilize Kotlins exhaustive `when` statements later on:

``````sealed class Action {
class SandwichTypeSelected(val type: SandwichType): Action()
class PredefinedSandwichSelected(val sandwich: Sandwich): Action()
class SubmitSandwichClicked(val sandwichName: String): Action()
}
``````

Since we’re using a `sealed class` instead of an `enum` we can pass data through our `Actions`.

Next up we’ll define a few simple model objects:

``````data class Sandwich(val name: String, val type: SandwichType)

enum class SandwichType {
GRINDER,
WRAP
}
``````

A sandwich object consists of a name and a `SandwichType`, which is either `GRINDER` or `WRAP`.

Implementing our States

Now we can work on actually defining our `SandwichState` implementations.

First up we’ll create a new `SandwichList` class that implements `SandwichState`. This class will represent the `SandwichList` `State` we reasoned about earlier.

``````class SandwichList(private val sandwiches: List<Sandwich>): SandwichState {
override fun consumeAction(action: Action): SandwichState {
TODO("Fill in the details")
}
}
``````

The `List<Sandwich>` that we pass in represents the sandwiches the user has already added to their list of favorite sandwiches. We’re going to be building this list up as we flow through our finite state machine.

We can refer to the equations we wrote above to figure out how to handle our `Actions`. The only formula relevant to the `SandwichList` state is the following:

`SandwichList` + `AddSandwichClicked` = `AddSandwich`

The only `Action` we’re going to handle in the `SandwichList` state is the `AddSandwichClicked` `Action`. When we encounter it we want to proceed to the `AddSandwich` state, which we’ll define shortly:

``````class SandwichList(private val sandwiches: List<Sandwich>): SandwichState {
override fun consumeAction(action: Action): SandwichState {
return when(action) {
else -> throw IllegalStateException("Invalid action \$action passed to state \$this")
}
}
}
``````

If we receive a different `Action` than the one we’re expecting we know something has gone wrong and we’ll throw our hands up. An alternative approach could be to just return `this` current state.

Next up we’ll implement the `AddSandwich` `State`:

``````class AddSandwich(private val sandwiches: List<Sandwich>): SandwichState {
override fun consumeAction(action: Action): SandwichState {
TODO("Fill in the details")
}
}
``````

Again, we can utilize the relevant formulas we wrote to fill in the `consumeAction` method:

`AddSandwich` + `PredefinedSandwichSelected` = `SandwichList`

`AddSandwich` + `SandwichTypeSelected` = `NameSandwich`

``````class AddSandwich(private val sandwiches: List<Sandwich>): SandwichState {
override fun consumeAction(action: Action): SandwichState {
return when (action) {
is Action.SandwichTypeSelected -> NameSandwich(sandwiches, action.type)
is Action.PredefinedSandwichSelected -> {
SandwichList(sandwiches + action.sandwich)
}
else -> throw IllegalStateException("Invalid action \$action passed to state \$this")
}
}
}
``````

This `State` is slightly more complicated than `SandwichList` was. The logic breaks down as follows:

1. When the `SandwichTypeSelected` `Action` is provided, we’re going to transition to the `NameSandwich` `State`, passing in the `SandwichType` that the user selected so the `NameSandwich` class can actually construct a new `Sandwich`

2. When the `PredefinedSandwichSelected` `Action` is provided, we want to add the `Sandwich` the user selected to our existing list of `Sandwiches`. Since the user selected a fully formed sandwich rather than opting for creating their own, we don’t need to transition to the `NameSandwich` `State`. Instead, we just loop back to the `SandwichList` `State`.

Last but not least we’ll define the `NameSandwich` `State`. Again, we’ll use the formulas defined above to inform our implementation:

``````class NameSandwich(private val sandwiches: List<Sandwich>,
private val newSandwichType: SandwichType): SandwichState {
override fun consumeAction(action: Action): SandwichState {
return when (action) {
is Action.SubmitSandwichClicked -> {
val newSandwich = Sandwich(action.sandwichName, newSandwichType)
SandwichList(sandwiches + newSandwich)
}
}
}
}
``````

This state is fairly straightforward - once the user clicks submit, we use the `SandwichType` they selected in the previous screen and the sandwich name they entered to create a new `Sandwich` and add it to our list of existing sandwiches. We’ll then transition to our new state, `SandwichList`, with the updated list of sandwiches.

Building our Activity

We now have a fully functional finite state machine that builds up lists of sandwiches. The last thing we need to do is connect our finite state machine to an `Activity` to actually drive our UI.

``````class SandwichActivity : AppCompatActivity() {
var currentState by Delegates.observable<SandwichState>(SandwichList(emptyList()), { _, old, new ->
renderViewState(new, old)
})

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sandwhich)
}

private fun renderViewState(newState: SandwichState, oldState: SandwichState) {
when (newState) {
is SandwichList -> showSandwichList(newState.sandwiches)
is NameSandwich -> showSandwichInputFields()
}

when (oldState) {
is NameSandwich -> hideSandwichList()
}
}
}
``````

We’ve defined a new `SandwichActivity` that holds onto the current state of our state machine via an observable delegate. That means that whenever we update the value of `currentState`, we’ll call into `renderViewState` with both the old and new values of our state machine. The `renderViewState` method will update our view depending on what states we transitioned to and from. Let’s take a look at the `showSandwichList` method:

``````private fun showSandwichList(sandwiches: List<Sandwich>) {
sandwich_list_container.visibility = View.VISIBLE
}
}
``````

Most of this method is normal Android code. The most interesting piece is the click listener added to the `add_sandwich_button`:

`currentState = currentState.consumeAction(AddSandwichClicked())`

This is where we’re passing an `Action` to our finite state machine. Specifically, when the user clicks the `add_sandwich_button`, we pass the `AddSandwichClicked` `Action` into our state machine and update our `currentState` to be whatever the new state is. Since we’re using an `observable` delegate, we’ll be notified in our `renderViewState` method about both the old value of the state machine and the new one!

The rest of the methods in our `Activity` look very similar:

``````private fun hideSandwichList() {
sandwich_list_container.visibility = View.GONE
}

wrap_button.setOnClickListener {
currentState = currentState.consumeAction(SandwichTypeSelected(WRAP))
}
grinder_button.setOnClickListener {
currentState = currentState.consumeAction(SandwichTypeSelected(GRINDER))
}
currentState = currentState.consumeAction(PredefinedSandwichSelected(it))
})
}

}

private fun showSandwichInputFields() {
sandwich_inputs.visibility = View.VISIBLE
submit_button.setOnClickListener {
val sandwichName = sandwich_name.text.toString()
currentState = currentState.consumeAction(SubmitSandwichClicked(sandwichName))
}
}

private fun hideSandwichInputFields() {
sandwich_inputs.visibility = View.GONE
}
``````

Now our view is properly forwarding `Actions` to our finite state machine, and the shifting of our `States` in the state machine are being reflected in the view!

For those that are using the MVP architecture pattern, instead of passing the `Actions` in through the `Activity` you could delegate that responsibility to the `Presenter`, preserving testability.

We’ve now built a functioning application whose logic is primarily driven by a finite state machine! But there’s still improvements/concerns to be addressed. How do we handle mutations/logic that needs to be addressed outside of the finite state machine? Is there a cleaner, more type safe way to translate our states into views? How well does this approach scale?

This blog post has already gotten quite long, so we’ll address those questions and more in a future post expanding on using finite state machines in Android development!