Tic-tac-toe: from MVP to Jetpack Compose
Tags: android, kotlin, compose, fore
Published: 2022-02-01 19:45:36

There are two fundamental ways of thinking about data that drives a UI.

We can think of our data as **state** or as **events/messages**. Here's what I mean:

`I am 3 years old`

That's a **"state-y"** way to represent someone's age, instead you could put it like this:

`I was born
I had a birthday
I had a birthday
I had a birthday`

That's an **"event-y"** way to represent the same thing.

Notice how the events tend to occur at the state change boundaries (a birthday event happens when your age state changes from 2 to 3). It's almost like we have two parallel worlds representing the same thing, the state world and the event world.

**Erm... okay. Why are you telling me this?** MVP is a very event-y style architecture (yes I did just make that word up), whereas Jetpack Compose does really well with state-y style data. That's what makes migrating from MVP to Jetpack Compose a little tricky. But if we recognise this state vs event difference first, migrating can be relatively pain free.

---

## Event-y

I'm not going to go into the details of how MVP is typically set up on android. There has been a long slow march away from MVP in android for almost 10 years, but there are still a lot of MVP apps around and chances are, if you're reading this article, you probably already have one in mind.

![tic tac toe game in progress](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/exzw9j61i2qmul71kuqi.png)

Let's just imagine the view interface / contract we might have with an MVP version of our tic-tac-toe app. Maybe something like this:

``` kotlin
fun showLoadingSpinner()
fun hideLoadingSpinner()
fun populateTile(xPos: Int, yPos: Int, mark: Player)
fun showError()
fun hideError()
fun showNextPlayer(player: Player)
fun showWinner(player: Player)
fun hideWinner()
fun resetGameView()
```

This is why I say most MVP apps are event-y, they tell the view what to **do** or what happened (not what **is**).

Take a look at your own MVP app, you'll probably find the same thing.

---

## State-y

So how would we re-write this in a state-y way? We just need to come up with a state that covers every possible situation our view will need to display. Kotlin data classes are pretty good for this, but by no means mandatory. This should do the trick:

``` kotlin
data class GameViewState {
   val isLoading: Boolean,
   val error: Error?,
   val nextPlayer: Player,
   val winner: Player,
   val board: List<List<Player>>
}

sealed class Player {
    object X : Player()
    object O : Player()
    object Nobody : Player()
}
```

This happens to be an immutable state, so it's easy to guarantee that it won't change while the view is updating the screen.

It doesn't have to be immutable though, we could also manage this state inside a regular class. The view just needs to be told to sync itself whenever any of the state changes (you have to be aware of threading issues with this one, but it also works fine):

``` kotlin
class Game {

   ...

   fun isLoading(): Boolean {...}
   fun error(): Error? {...}
   fun winner(): Player {...}
   fun tile(xPos: Int, yPos: Int): Player {...}
}
```

The point is, with state-y driven code we are telling the view what **is** (not what to **do** or what happened).

How we calculate this state (the actual logic of the game) is kept safely away from the view layer. The logic of a tic-tac-toe game doesn't need to know anything about android activities or fragments. It's plain logic, and it would be the same logic if it was driving an iOS tic-tac-toe app (it would be a good contender for keeping in a kotlin only domain module if we were writing a clean architecture app).

---
## Imperative vs Declaritive

Take a moment to notice that event-y style code sounds a lot like **imperative** programming - telling your view what to **do**. And state-y style code sounds a lot like **declaritive** programming - telling your view what **is**

----
## Drawing the Non-Compose UI

Back to our pre-compose tic-tac-toe app for the moment. We've changed the type of data that is driving our view, so now we also need to change how the view is drawn.

Let's take the loading spinner, our event-y MVP code looks like this:

``` kotlin
fun showLoadingSpinner() {
   spinner.visibility = View.VISIBLE
}

fun hideLoadingSpinner() {
   spinner.visibility = View.GONE
}
```

it's fairly easy to write a more state-y version like this:

``` kotlin
If (viewState.isLoading) {
   spinner.visibility = View.VISIBLE
} else {
   spinner.visibility = View.GONE
}
```

But the secret is to put the **focus on the UI attribute** itself, so we can instead write:

``` kotlin
spinner.visibility = if (viewState.isLoading) VISIBLE else GONE
```

If you do this correctly you can have a single line for each UI attribute (and typically that means far fewer lines of code than the event-y version)

We've actually seen this trick before when we re-wrote the [android architecture blue prints todo sample app](https://dev.to/erdo/tutorial-android-architecture-blueprints-full-todo-app-mvo-edition-259o). In that case the event-y view code went from over 100 lines to an almost embarrassing 8 lines of state-y code. If you haven't read that article, you probably find that hard to believe - but it's not an exaggeration.

---
## Reactivity

The final piece of the puzzle is that we need some way of letting the view know that it's time to redraw itself. There are loads of ways to do this, basically some kind of observer implementation: RxJava, Flow, LiveData, etc. I'm going to use [fore](https://erdo.github.io/android-fore/) because **a)** I wrote it 😬 and **b)** it's impossible to do it with less boiler plate.

``` kotlin
class TicTacToeActivity : FragmentActivity(R.layout.activity_tictactoe), SyncableView {

  //models that we want to observe
  private val gameModel: GameModel = OG[GameModel::class.java]

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    //setup observers
    lifecycle.addObserver(LifecycleObserver(this, gameModel))

    //setup click listeners
    viewResetBtn.setOnClickListener { gameModel.reset() }
    ...

  }

  //called on the UI thread, whenever the gameModel state changes
  //you could instead observe some LiveData state, or collect a Flow here
  override fun syncView() {
    gameModel.state.apply {
      viewResetBtn.enabled = !loading
      viewIsBusyProg.showOrInvisible(loading)
      viewWinnerText.text = winner
      viewWinnerText.showOrInvisible(winner != Player.Nobody)
      
      // more ui elements
      ...
    }
  }
}
```

Congratulations 🎉 you now have "state-y" data driving your UI 😬 It's performant, supports rotation, and doesn't have any memory leaks - so this is a good place to be already. Access your GameModel via a ViewModel, and you'll have a basic MVVM app.

Now we're driving our UI with state, the entire view layer of our app (the Activity class in this case) takes up around 70 lines of code including imports. If we had started out with something like MVI rather than MVP, we would probably be a lot closer to this level already.

----

## Replacing the view layer with Compose

If you get to this point in your migration, you can now focus just on Compose itself. The game model won't need to change, and the rest of the app won't even be aware that it's now driving a compose view layer.

The whole of Jetpack Compose is driven by state, and state is what we already have - but first we need to convert our state to an instance of Google's State&lt;&gt; class.

Most flavours of observable have an extension function that will do the conversion for you.

``` kotlin
// Fore
val gameVS by gameModel.observeAsState { gameModel.state }
// Flow
val gameVS by gameModel.gameFlow.collectAsState(GameViewState())
// LiveData
val gameVS by gameModel.gameLiveData.observeAsState(GameViewState())
```

(While writing the extension function for fore I used a demo app to investigate the behaviour of the LiveData and Flow implementations, it's [here](https://github.com/erdo/compose-observe-as-state-explorer) in case you find it useful.)

Here's (most of) the Compose UI code for an equivalent view. It's a lot of code, but we'll discuss that in a sec:

``` kotlin
@Composable
fun GameScreen(
    game: Game,
    modifier: Modifier = Modifier,
    switchToNonComposeUI: () -> Unit
) {

    val viewState by game.observeAsState { game.state }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .padding(dimensionResource(id = R.dimen.common_space_small))
            .then(modifier),
    ) {

        HeaderLayout(viewState, retry = { game.retryAutoPlayer() }, Modifier.align(TopCenter))

        BoardLayout(
            viewState.isLoading,
            viewState.error,
            viewState.board,
            Modifier.align(Center)
        ) { x, y ->
            game.play(x, y)
        }

        Column(
            modifier = Modifier
                .wrapContentSize()
                .align(BottomCenter),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Button(
                modifier = Modifier
                    .wrapContentSize(),
                onClick = { game.newGame() },
                enabled = !viewState.isLoading
            ) {
                Text(stringResource(id = R.string.reset))
            }
            Spacer(modifier = Modifier.width(dimensionResource(id = R.dimen.common_space_small)))
            Button(
                modifier = Modifier
                    .wrapContentSize(),
                onClick = { switchToNonComposeUI() },
            ) {
                Text(stringResource(id = R.string.non_compose_ui))
            }
        }
    }
}

@Composable
fun HeaderLayout(
    viewState: GameState,
    retry: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        modifier = Modifier
            .wrapContentSize()
            .then(modifier),
        verticalArrangement = Arrangement.Top,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {

        if (viewState.gameFinished()) {
            Text(
                text = stringResource(viewState.winner.winMessageRes),
                color = colorResource(id = R.color.colorTextTitle),
                fontSize = fontSizeResource(id = R.dimen.common_textsize_extra_large),
                fontWeight = Bold
            )
        } else {
            Text(
                text = "${viewState.whoseTurn().label} ${stringResource(id = R.string.to_play)}",
                color = colorResource(id = R.color.colorTextTitle),
                fontSize = fontSizeResource(id = R.dimen.common_textsize_extra_large)
            )
        }

        if (viewState.isLoading) {
            CircularProgressIndicator(
                modifier = Modifier
                    .wrapContentSize()
                    .padding(dimensionResource(id = R.dimen.common_space_small)),
            )
        }

        if (viewState.error != ErrorMsg.NoError) {
            Text(
                modifier = Modifier.wrapContentSize(),
                text = stringResource(viewState.error.msgRes),
                color = colorResource(R.color.colorWarning),
                fontSize = fontSizeResource(id = R.dimen.common_textsize_extra_large)
            )
            Button(
                modifier = Modifier.wrapContentSize(),
                onClick = { retry() }
            ) {
                Text(stringResource(id = R.string.retry_autoplayer))
            }
        }
    }
}

@Composable
fun BoardLayout(
    isLoading: Boolean,
    error: ErrorMsg?,
    board: List<List<Player>>,
    modifier: Modifier = Modifier,
    tileClick: (x: Int, y: Int) -> Unit
) {
    Column(
        modifier = Modifier
            .wrapContentSize()
            .then(modifier)
    ) {
        var colorIndex = 0
        for (y in board.size - 1 downTo 0) {
            Row {
                for (x in board.indices) {
                    if (isLoading || error != ErrorMsg.NoError) {
                        GameSquare(colorPastels[colorIndex++ % colorPastels.size], board[x][y]) { }
                    } else {
                        GameSquare(colorPastels[colorIndex++ % colorPastels.size], board[x][y]) {
                            tileClick(x, y)
                        }
                    }
                }
            }
        }
    }
}
```

## Compose Yourself!

Wait a second... that legacy state-y view layer code looked _clearer_ than the Compose version 🤔 It was also smaller and slightly more performant, so what's going on here?

While that might be true (even if we allow for the fact that the compose code did away with the XML layouts), I believe that's somewhat missing the point of Compose.

## Dynamic UIs are the future

Compose is looking to a future where devices are _more_ performant, not less. The UIs of tomorrow are likely to be much more dynamic and animated. And **animating changes are the achilles heel of state-y driven UIs**.

Let me explain that. If you think about when you might want to animate something on screen, it's typically at the state _change_ boundaries. When someone goes from being 2 years old to 3 years old, you might wish to run a "happy 3rd birthday" animation. But that's a one time thing, if you ever get in to a situation where you're wishing someone happy birthday twice in the same day, well... that's weird.

That's something event-y driven architectures excel at because they're already event based. When the UI layer receives the "3rd birthday" event -> it runs the "happy 3rd birthday" animation. Easy.

**State-y driven architectures have a problem here**. When our UI layer receives the latest state, it knows that person is 3 years old (age=3). But did they become 3 just now?, have they been 3 for 6 months already? Did the UI already wish them happy birthday, and then they just left the room and came back (i.e. rotated the screen)?. The UI layer doesn't know without resorting to storing the latest state and looking for changes. There are [many](https://www.reddit.com/r/androiddev/comments/g6kgfn/android_databinding_with_livedata_holds_old_values/foabqm0/), [many](https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150), [many](https://github.com/android/architecture-components-samples/issues/63#issuecomment-310422475) ways to handle this of course (and [fore](https://erdo.github.io/android-fore/01-views.html#synctrigger) has its own solution too). It's a universal, if not very often discussed, problem.

Well this is where Compose really shines, that whole **looking-for-changes-to-state** thing is what Compose does for us (Compose is actually two systems, but that's outside the scope of this article), we just provide it with our latest state whenever we have it, and compose handles the rest for us. As the number of animations in a typical UI increases, you can probably appreciate how this could start to be the _more_ scalable solution from a complexity point of view.

## A few more thoughts

- Composables are functions, and are very well suited for reuse provided they're written with that in mind. Sure we can do that with XML based layouts too, but it's not pretty.

- A lot of state driven UIs have a function where all the UI states are set. In the example above we have **syncView()**, in an MVI app you might have a **render()** function. If this function is written [correctly](https://erdo.github.io/android-fore/01-views.html#syncview), most UI inconsistencies can be quickly narrowed down to a single line of code in a 10-20 line function. It's an incredibly powerful way to debug UIs, but I don't see a way that would be possible with Compose at the moment.

But Compose is pretty new! I'm interested to know what you think about it in the comments

---

Thanks for reading, hope you get a chance to play a few rounds 😂

Note: if you do, the auto player makes a real network connection to mocky.io so it will take up to 30 seconds to wake up on first play

Also, the auto player is not a particularly smart opponent, but know that when ~~playing~~ testing this game, I still lost to it several times 🤦

Clone the app on [github](https://github.com/erdo/fore-mvp-compose-tutorial)