MVI / MVVM - Examples

This is the accompanying examples post to MVI - Overview with Linked Examples. These should be read together starting with that post first.

IMPORTANT Note: These examples contain some known errors. I intend on fixing these tomorrow.

Our Examples

We will look at two examples of MVI to help explain the concepts.

For the purpose of these examples, I will avoid explaining all the layout details. Instead, I will just focus on the organization and how we connect the UI and data. You can always check the full code GitHub Gists to see exactly how everything fits together.

Example 1: MVI - Basic Color Selector

We have a very simple color selector.

App Animation GIF (Color Changes)
This example is too basic to understand the benefits of MVI, but it does clearly demonstrate how it works.

We have our data class and and action interface:

Basic Color Selector - Data

UiState tracks the state of the UI for our Composable to display the data.
data class UiState(
val color: Color = noColor,
)
UiAction is used to pass actions from the UI to the ViewModel.
interface UiAction {
object ClearColor: UiAction
data class SelectColor(val selectedColor: Color): UiAction
}
That's the core of MVI:
  • Single UI State
  • One way to pass actions

Basic Color Selector - UI

We can briefly look at how the code is used (formatting / title removed):
@Composable
private fun ExampleUi(viewModel: MinimalMviViewModel) {
val uiState by viewModel
.uiState.collectAsStateWithLifecycle()

ColorDisplayBox((uiState.color))
ColorControls(
color = uiState.color,
onAction = viewModel::onColorAction
)
}
This function takes in our viewModel and collects the uiState with lifecycle. The uiState is a StateFlow which means the composable will update whatever needs this information every time it changes.

We then have two functions to pass information to. We pass our color to the ColorDisplayBox and nothing else. It doesn't care about any other information. This makes ColorDisplayBox extremely easy to test.

For ColorControls, we pass both our color and tell it how to pass actions to our viewModel. Passing that single reference to onAction allows the UI to set new colors or clear the existing color with just one argument. As you can see, the code is extremely concise.
@Composable
private fun ColorDisplayBox(
color: Color,
) {...
ColorDisplayBox simply takes in the single color and displays the box. I have cut all the formatting here.
@Composable
private fun ColorControls(
color: Color,
onAction: (UiAction) -> Unit
) {
ColorSelectRow(color, onAction)
ColorClearButton(onAction)
}
ColorControls hosts our ColorSelectRow and ColorClearButton and only passes the information they need. 

Let's start with ColorClearButton as this is a little easier to understand. Very simply, we use the onAction lambda to pass our UiAction.ClearColor object to the ViewModel to tell it to update the UiState data class.
@Composable
private fun ColorClearButton(
onAction: (UiAction) -> Unit
) {
Button(
{ onAction(UiAction.ClearColor) }
) {
Text("Clear")
}
}
This one is a little more complex:
@Composable
private fun ColorSelectRow(
selectedColor: Color,
onAction: (UiAction) -> Unit
) {
Row() {
colors.forEach { color ->
val borderColor =
if(color == selectedColor) Color.LightGray
else Color.Transparent

Box(
modifier = Modifier
.size(46.dp)
.background(color)
.border(4.dp, borderColor)
.clickable {
onAction(
UiAction.SelectColor(color)
)
}
)
}
}
}
ColorSelectorRow uses our list of colors. For each color, it checks if it is the current selectedColor. If it is selected, it adjusts the border color to LightGray. Then it draws a small box of the color with the border and makes the box clickable.

The key point here, again, is the action. It takes in our UiAction.SelectColor data class and passes the current color as an argument. This then gets passed to the ViewModel to update the UiState data class.

Basic Color Selector - ViewModel

We're almost done with this MVI example. We just need to see the ViewModel
class MinimalMviViewModel: ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()

fun onColorAction(action: UiAction) {
when(action) {
UiAction.ClearColor -> { _uiState.update { UiState() } } // object
is UiAction.SelectColor -> { // class so needs `is`
_uiState.update {
it.copy(color = action.selectedColor)
}
}
}
}
}
Wow, that's actually really simple.

We have our uiState created privately as a MutableStateFlow and shared publicly as a StateFlow. In the UI code above, you saw how we could collect this using collectAsStateWithLifecycle.

After that, we deal with all of our actions in one place. This code is very simple so all updates are done immediately without needing to wait for anything (like calculations or data to be returned. This leads to a really clear ViewModel where you can quickly see all possible states.

Summary

We are using MVI with two points of data access. One is sharing our data with the UI and the other is passing any actions from the UI to the ViewModel to then process the requests.

This is obviously a very simple demonstration. If you are used to MVVM, you may wonder why this MVI pattern is useful? The MVVM code for this exact example isn't really that different. That's why we need a more complex example.

Example 2: MVI - Advanced Color Selector

App Animation Image (Circular Color Changes)

You know when the circles come out, things are getting serious!

Alright, so from this animation, you will notice a few changes that need to be made to the code. We now need to hold three color values. We also need to know if the sequence of colors needs inverting. Aside from that, the MVI elements are basically the same.

The code listing will be much shorter here as I won't need to explain a lot of what we have already covered in the first example.

Note: I wanted to add a color blend animation to this, but I left that out to keep things simple. I would recommend this as an extension activity if you are interested.

Advanced Color Selector - Data

data class UiState(
val primaryColor: Color = noColor,
val secondaryColor: Color = noColor,
val tertiaryColor: Color = noColor,
val invertOrder: Boolean = false
)
We now have our three colors and our invertOrder flag.
interface UiAction {
object ClearColors: UiAction
data class SelectColor(val selectedColor: Color): UiAction
object InvertOrder: UiAction
}
Our interface hardly changes. We just add an InvertOrder object.

By this point, I hope you can see how clear everything already looks. You might already start to appreciate the benefits of MVI.

Advanced Color Selector - UI

I won't show too much of the UI here; I'll focus on the keys points. 
@Composable
private fun ColorDisplayBox(
primaryColor: Color,
secondaryColor: Color,
tertiaryColor: Color
) {
Box(
modifier = Modifier
.padding(16.dp)
.size(256.dp)
.border(width = 48.dp, primaryColor, shape = CircleShape)
.border(width = 96.dp, secondaryColor, shape = CircleShape)
.background(tertiaryColor, shape = CircleShape)
)
}
I think ColorDisplayBox is really the only part we need to look at. It just takes in three arguments for the colors instead of one.

I will include a cut down version of ColorControls just for clarity:
@Composable
private fun ColorControls(
primaryColor: Color,
onAction: (UiAction) -> Unit
) {
Row()
{
InvertButton(onAction)
ColorClearButton(onAction)
}
}
We very simply have an InvertButton and have arranged them in a row. The key point is that nothing has changed with the incoming arguments. We simply have our color (admittedly renamed) and action. A lot of the UI layout code is missing here so do see the full example for that if needed.

Advanced Color Selector - ViewModel

Maybe you can already guess, but the ViewModel really doesn't get much more complicated (but I do cheat a little).
    class MinimalMviViewModel: ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()

fun onColorAction(action: UiAction) {
when(action) {
UiAction.ClearColors -> { _uiState.update { UiState() } }
is UiAction.SelectColor -> {
_uiState.update {
it.updatePrimary(action.selectedColor)
}
}
UiAction.InvertOrder -> { _uiState.update {
it.copy(invertOrder = ! it.invertOrder)
} }
}
}
}
The InvertOrder code is very simple.
fun UiState.updatePrimary(selectedColor: Color): UiState {
return copy(
primaryColor = selectedColor,
secondaryColor = primaryColor,
tertiaryColor = secondaryColor
)
}
The cheat we use for SelectColor is just an extension function for UiState. This reduces the length of the code in the ViewModel to make it faster and easier to understand. I'll leave the original code commented out in the Gist in case you want to see what that would have looked like.

Summary

And that's it. It's tiny! We've not really had to make many changes to get these new changes in and now the demonstration app really starts to demonstrate why MVI is the go-to pattern for Android Compose.

MVI Conclusion

Looking at the second example, the benefits become much more obvious.

Benefits

  • State: One source of truth. A single snapshot of the entire UI state. Far easier to quickly understand.
  • Actions / Intents: One way communication with the ViewModel. Much clearer communication.
  • Atomic: One recomposition per change. One update per change and no chance of broken hybrid states where some values haven't yet updated.
  • Readability: Intent over implementation detail. Forces cleaner organization and less boiler plate communicates ideas more clearly.
  • Testability: Predictable and pure. Clearer expected results and UI states for actions.

MVVM

For comparison, let's look at an MVVM version of the same code.

Comparison Example: MVVM - Advanced Color Selector

I must warn you, things are going to get ugly.

MVVM Comparison - Data

None! We don't have anything here. This is very clean! One point for MVVM!

MVVM Comparison - UI

Oh, this is where it starts to get bad... I almost don't want to show this.
Ewwwwww Gif
@Composable
private fun ExampleUi(viewModel: MinimalMviViewModel) {
val primaryColor by viewModel.colorPrimary.collectAsStateWithLifecycle()
val secondaryColor by viewModel.colorSecondary.collectAsStateWithLifecycle()
val tertiaryColor by viewModel.colorTertiary.collectAsStateWithLifecycle()

val invertOrder by viewModel.invertOrder.collectAsStateWithLifecycle()

val onClearColors: () -> Unit = { viewModel.onClearColors() }
val onInvertColors: () -> Unit = { viewModel.onInvertColors() }
val onSelectColor: (Color) -> Unit = { viewModel.onSelectColor(it) }

if(invertOrder) {
ColorDisplayBox(
primaryColor = tertiaryColor,
secondaryColor = secondaryColor,
tertiaryColor = primaryColor
)
} else {
ColorDisplayBox(
primaryColor,
secondaryColor,
tertiaryColor
)
}

ColorControls(
primaryColor,
onClearColors,
onInvertColors,
onSelectColor
)
}
As you can see, the code is far more verbose and error prone. We have to collect the state of four different flows here to pass down to the functions. Of course, we could pass the ViewModel but that makes things even worse in many cases.

As well as the states, we also need to create references to each of the functions to pass into the child composables. This code is very error prone as it is extremely easy to pass the wrong value if they are the same time - especially when the names are similar.
@Composable
private fun ColorControls(
primaryColor: Color,
onClearColors: () -> Unit,
onInvertColors: () -> Unit,
onSelectColor: (Color) -> Unit
) {
The rest of the code is fairly similar but we are passing more arguments around so I will omit posting that here.

MVVM Comparison - ViewModel

Is the ViewModel nicer, at least?
Starwars meme
class MinimalMviViewModel: ViewModel() {
private val _colorPrimary = MutableStateFlow(noColor)
val colorPrimary = _colorPrimary.asStateFlow()

private val _colorSecondary = MutableStateFlow(noColor)
val colorSecondary = _colorSecondary.asStateFlow()

private val _colorTertiary = MutableStateFlow(noColor)
val colorTertiary = _colorTertiary.asStateFlow()

private val _invertOrder = MutableStateFlow(false)
val invertOrder = _invertOrder.asStateFlow()

fun onSelectColor(selectedColor: Color) {
_colorTertiary.update { _colorSecondary.value }
_colorSecondary.update { _colorPrimary.value }
_colorPrimary.update { selectedColor }
}

fun onClearColors() {
_colorPrimary.update { noColor }
_colorSecondary.update { noColor }
_colorTertiary.update { noColor }
}

fun onInvertColors() {
_invertOrder.update { !_invertOrder.value }
}
}
Nope!

This is probably the worst part to deal with. Each State Flow requires two lines of codes. Here's another place where it is really easy to mix up references.

Working with this pattern, I would often have mistakes where the public accessor pointed to the wrong internal value (e.g. colorTertiary takes _colorSecondary). This leads to surprisingly sneaky UI bugs where you have to review all the UI and ViewModel code before spotting what went wrong.

Our onSelectColor and onClearColors functions are now much longer as well.

Inverting colors is actually very easy so I won't complain about that one.

MVVM Summary

MVVM is fine and often easier for smaller tasks and prototyping. I will generally use it first so I can often get it up and running very quickly. The issues really start to propagate once you start adding more complexity. At that point, I switch.

One thing I did find interesting was the fact that both the MVI and the MVVM versions of the code seem to run exactly the same. One of the advantages of MVI is the fact that the state gets updated atomically so we should get less recomposition.

In this small example, neither suffered from too much recomposition. When tested with the Layout Inspector, the MVVM had to do one extra draw (ColorDisplayBox) after clicking all the buttons in order (from top left to bottom right) so there may be some minor inefficiencies, but this was actually much better than expected.

Once you have carefully considered the examples, it's time to return to the main post.

Comments

Popular Posts