Blog Infos
Author
Published
Topics
Published
User Actions should take an important place in UDF implementation.

Disclaimer: This article was originally published as the second part of this articlebut after thinking about it, these two stories do not have much in common and are completly independent. So the title was changed and articles decoupled.

Unidirectional Data Flow

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 this article (read it when you get the time) I 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.

Rethinking and modeling User Actions

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 actions. Basically, One entry door and One exit door.

How do we do that?

First, we define the actions

sealed class MoviesAction {
data class SearchMovieAction(val query: String): MoviesAction()
data class AddToHistoryAction(val movieId: Int): MoviesAction()
object RefreshMoviesAction: MoviesAction()
}
view raw MoviesAction.kt hosted with ❤ by GitHub

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

class MoviesViewModel @Inject constructor(private val mapper: MoviesUIMapper) : ViewModel() {
private val _actionFlow =
MutableSharedFlow<MoviesAction>(extraBufferCapacity = 16)
init {
_actionFlow.process().launchIn(viewModelScope)
}
private fun Flow<MoviesAction>.process() = onEach {
when(it) {
is MoviesAction.SearchMovieAction -> onSearchMovie(it.query)
is MoviesAction.AddToHistoryAction -> onAddToHistory(it.movieId)
MoviesAction.RefreshMoviesAction -> onRefreshMovies()
}
}
/**
* This is the only function exposed to the view.
*/
fun processAction(action: MoviesAction) = _actionFlow.tryEmit(action)
}

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 Hoisting.
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:

@Composable
fun MoviesScreenContent(
uiState: TransferScreenState,
onSearchMovie: (String) -> Unit = { },
onCategoryChange: (String) -> Unit = { },
onFirstNameChange: (String) -> Unit = { },
onLastNameChange: (String) -> Unit = { },
onFavoriteChange: (String) -> Unit = { },
onDownloadClicked: (MovieUI) -> Unit = { },
onUploadClicked: () -> Unit = { },
onRetryClicked: () -> Unit = { }
) {
...
}
...
@Composable
private fun SearchField(
inputState: InputState,
enabled: Boolean,
onSearchQuerryChanged: (String) -> Unit
) {
...
}
@Composable
private fun DownloadButton(
onDownload: () -> Unit,
movie: MovieUI? = null
) {
...
}
@Composable
private fun UploadButton(
onUpload: () -> Unit,
uploadStatus: UploadStatus? = null
) {
...
}
@Composable
private fun RetryButton(
onRetry: () -> Unit
) {
...
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

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:

class MoviesActivity : ComponentActivity() {
private val viewModel by viewModels<MoviesViewModel>()
private val actionChannel = Channel<MoviesAction>()
private fun actions(): Flow<MoviesAction> = actionChannel.consumeAsFlow()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
actions().onEach(viewModel::processAction).launchIn(lifecycleScope)
setContent {
MoviesAppTheme {
Scaffold(
...
) {
MovieScreenContent(
uiState = viewModel.uiState,
actionChannel = actionChannel
)
}
}
}
}
}

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:

@Composable
fun MoviesScreenContent(
uiState: TransferScreenState,
actionChannel: ActionChannel
) {
...
}
...
@Composable
private fun SearchField(
inputState: InputState,
enabled: Boolean,
actionChannel: ActionChannel
) {
...
}
@Composable
private fun DownloadButton(
actionChannel: ActionChannel
movie: MovieUI? = null
) {
...
}
@Composable
private fun FavoriteField(
inputState: InputState,
enabled: Boolean,
actionChannel: ActionChannel
) {
...
}
@Composable
private fun RetryButton(
actionChannel: ActionChannel
) {
...
}

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

@Composable
private fun SearchField(
actionChannel: ActionChannel
) {
...
Button(
/*Send a new action in the actionChannel */
onClick = { actionChannel.trySend(MoviesAction.SearchMovieAction(query) },
...
) {
Text(text = "Search")
}
}

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 timeactionChannel.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 FlowBindings, and merge all your UI events flows + the actionChannel flow, into a single flow.

fun actions(): Flow<MoviesFlowAction> = merge(
actionChannel.consumeAsFlow(),
binding.refreshButton.clicks().map { MoviesAction.RefreshAction }
)

You merge them because you only want one flow for your actions.

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.

Conclusion

In this article, we have discussed the necessity of modeling your user actions just like you do with your UI state and argued how this could help to slightly improve your UI code and to set up a more clear Unidirectional Data Flow.

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.

This article was originally published on proandroiddev.com on April 29, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
This is the second article in an article series that will discuss the dependency…
READ MORE
blog
Let’s suppose that for some reason we are interested in doing some tests with…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu