Passive Views: keep your UI code simple and stupid part 2 | by Rygel Louv | Apr, 2022

We keep discussing some techniques to make our UI code clean, stupid, and simple. Let’s talk a bit more about UDF

Minimal image by Samantha Gades on Unsplash

This article was made in two parts. You can find part 1 here

This article is not necessarily about the details of the Passive View pattern but mostly about the idea of ​​putting together efforts to make a View passive with very less logic or complexity.

Unidirectional Data Flow is not a pattern we should introduce anymore. This concept did enter the Android world along with the MVI architecture pattern. The MVI architecture has grown in popularity and even today we can clearly see some of its concepts being widely adopted even by Google. Because MVI was the first architectural pattern to suggest the idea of ​​State management as we know it today in the Android world.
Unidirectional Data Flow exposes the idea that the flow of data should go in a single direction. Meaning we go from an initial state of the UI screen, the user interacts with the screen, the user’s actions are processed and a new UI state is produced.

Image from https://proandroiddev.com/android-unidirectional-state-flow-without-rx-596f2f7637bb

In the previous post we argued that to have a cleaner UI code, one should make sure they represent the UI as a single state object that can be mutated otherwise, it might end up in a mess.
Remember the goal is to have a View that is passive, to have a dumb and stupid UI code which does mostly displaying data and captures user input. So doing something like this 👇 is certainly more likely to add complexity and we should eventually avoid:

Source: Kaushik Gopal’s talk on UDF

But instead, we want our UI Views to be better organized and we want to have a clear idea of ​​how the data flows in the system.

Source: Kaushik Gopal’s talk on UDF

If you read Google’s documentation, some clear explanations and samples are provided about UDF but there is one thing that bothers me about it, and that is the way user Actions are represented.

Think about it, they say UI state should be properly represented. So we create data classes or sealed classes, objects to represent every possible mutation of the UI. But when it comes to user actions, most samples you will see will just let the UI call the ViewModel’s functions. I think User Actions (what some may call UI Events or Intents in MVI) should also be modeled using data classes, objects, and sealed classes, and I will argue that it helps to provide a much cleaner code.

First of all, we implement UDF because we want a more predictable code that can easily scale. In UDF, your ViewModel should expose only one object for state observation and I think likewise, the View should also call only one function to provide the user’s action. Basically, One entry door and One exit door.

How do we do that?

First, we define the actions

Then we can set up our ViewModel in a way that it consumes actions using a Kotlin Channel:

So we still have the functions but now, we can make them all private except for the processAction function which is now the only public function exposed by the ViewModel.

What’s the advantage of this?

As you can see, not much did actually change. But to explain how this can help the code to be cleaner, let’s take an example in Jetpack Compose

In Jetpack Compose, we are encouraged to make our Composable stateless by moving Composable’s state out to the caller. That’s what we call State Hosting.
So basically, pass in the state values ​​to the Composable along with the functions that will be executed when a specific UI event happens (That’s what I call Actions in this post).
The Android official doc has a brilliant example for it:

@Composable
fun HelloScreen() {
var name by rememberSaveable { mutableStateOf("") }

HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $name",
...
)
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") }
)
}
}

But, even though State Hoisting is definitely a very good and important concept, I think you may have already come across the kind of problem it can cause.
Let’s say we have a parent Composable that has multiple child Compasables and it takes UI event functions as parameters and passes them down to the different child Composables. Here is how it should be done following State Hoisting:

As you can see, the MoviesScreenContent Composable has a lot of parameters and this number will grow proportionally to the number of child Composables. If you have Detekt in place, there is a chance that you get a LongParameterList error.
And that’s the kind of problem the Actions and ViewModel previously set up, can help fix. What if we only passed one parameter around to dispatch an Action (UI Event) when it happens? To do that we will have to set up the Action system on the UI side:

You will notice that we create an actionChannel object that we will use to feed the actions to the ViewModel by calling viewModel.processAction(MoviesAction) and that’s all we need to do.
Now with this, we can just pass the actionChannel down to the child Composables instead of the lambda functions:

And how do we notify when the user performs an action? Easy, just put it in the channel.

With this refactoring, instead of passing ViewModel functions around as lambdas, you can now just pass the actionChannel around instead and provide your actions when needed. Thanks to the setup we did before, each time actionChannel.tySend(MoviesAction) is called, viewModel.processAction(MoviesAction) is automatically called which will trigger the right function in the ViewModel.
ViewModel.processAction() is our single entry door and ViewModel.state is our single exit door.

Works with the View system as well

Of course, you can use this refactoring in the view system too. You can take advantage of FlowBingsand merge all your UI events flows + the actionChannel flow, into a single flow.

How about single-shot events?

For single-shot events like those used to display a Snackbar, the official Android doc has an example for it where it is handled as part of the UI state and they recommended keeping track of the events fired with a clunky mechanism. I don’t quite like this solution even though it respects the UDF pattern, it still feels more like a workaround to me. I prefer having a separate SharedFlow event object that is used for single-shot events only. Or use the SideEffect elements that come with Compose. Up to you.

In this 2 part series, we have discussed in detail some of my ideas on how to slightly make your UI code less complex. Of course, there are so many other things you can do to keep your UI code clean and free of logic.
Remember, as a rule of thumb, if you have a lot of if-else statements, loops/iterations, and some amount of logic that is not specifically UI related, then your view is probably doing too much, it’s probably not passive.

Keep in mind that the ideas discussed here are very opinionated, you may have a different point of view and if that’s the case, I would love to hear them in the comments.
Thanks for reading.

Leave a Comment