Stop Passing Event/UI-Action Callbacks in Jetpack Compose
ilyas ipek on 2024-03-16
Stop Passing Event/UI-Action Callbacks in Jetpack Compose
Why the UDF is a waste of time for most screens.
Important note: In this article, I assume you’re familiar with UDF (Unidirectional data flow) and the MVI architecture.
In Google Docs, Google advises us to use UDF to increase the testability and reusability of our components. For example…
@Composable fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) { TopAppBar( title = { Text(text = topAppBarText) }, navigationIcon = { IconButton(onClick = onBackPressed) {/*...*/} }, ) }
By providing the component with only the data it needs topAppBarText
and collecting ui-actions using lambdas onBackPressed
we make this component reusable and testable. Awesome right?
However, when you apply that principle to your whole screen you will quickly end up passing 20–30 event callbacks from the top component all the way to the bottom one. Lemme give you an example :)
@Composable fun HomeScreen( onItemClick: (String) -> Unit, onSearchClick: () -> Unit, /* More state and callbacks */ ) { Home( onItemClick = onItemClick, onSearchClick = onSearchClick, /* More state and callbacks */ ) } @Composable private fun Home( onItemClick: (String) -> Unit onSearchClick: () -> Unit, /* More state and callbacks */ ) { HomeContent( onItemClick = onItemClick, onSearchClick = onSearchClick, /* More state and callbacks */ ) } @Composable private fun HomeContent( onItemClick: (String) -> Unit, onSearchClick: () -> Unit, /* More state and callbacks */ ) { Row { Button(onClick = onSearchClick) { /***/ } } HomeList( onItemClick = onItemClick, /* More state and callbacks */ ) } @Composable private fun HomeList(onItemClick: (String) -> Unit) { items.forEach { item -> HomeListItem( onItemClick = onItemClick ) } } @Composable private fun HomeListItem(onItemClick: (String) -> Unit) { Button(onClick = { onItemClick(item) }) { /*...*/ } }
Here you notice that we needed to pass onItemClickon/onSearchClick
like 900 times, and this is just a small example, in a real project the number of the same callback passed down might make you think that going back to XML is not a bad idea :)
Also, check this example in Google sample, please follow the callbacks down annnd up (HomeRoute
& JetnewsNavGraph
) for more fun 🫠
The UDF is a really good approach for reusable/generic components but it’s not suitable for the whole screen for 3 reasons…
- Unless the same screen is reusable for the Create and Update operation, 99% of the screens are not reusable.
- Almost all of the screen’s subcomponents are specific to that screen and can’t be reused elsewhere.
- Since the devs are lazy, they will not extract components into smaller components to avoid the pain of passing all those callbacks around. As a result, the code will become less readable and harder to maintain.
The solution is simple, if you’re using MVI, you already have a sealed class for all your Intents/Actions with a onAction()/onIntent()/processIntents()
function that handles them in the ViewModel
. Just pass that one onAction()
function around.
Note: You can still use this solution even if you’re not using MVI.
// Optionally, create a typealias or just use (UiAction) -> Unit typealias OnAction = (UiAction) -> Unit @Composable fun HomeScreen( onAction: OnAction ) { Home(onAction = onAction) } @Composable private fun Home(onAction: OnAction) { HomeContent(onAction = onAction) } @Composable private fun HomeContent(onAction: OnAction) { Row { Button(onClick = { onAction(UiAction.OnSearchClick) }) { /*...*/ } } HomeList(onAction = onAction) } @Composable private fun HomeList(onAction: OnAction, /*...*/) { items.forEach { item -> HomeListItem( item = item, onAction = onAction, ) } } @Composable private fun HomeListItem( item: String, onAction: OnAction, ) { Button(onClick = { onAction(UiAction.OnItemClick(item)) }) { /*...*/ } }
Why is this better?
- Instead of 900 event callbacks passed all around, now it’s just one.
- Faster development time and happier developers.
- You will encourage your team to split big components into multiple subcomponents since it has no pain.
- Debugging becomes easier. It’s way faster to navigate where the Action is triggered rather than navigate to the Action and then go down the tree component by component till you forget what you were looking for :)
- Enhanced Code Readability
- Just passing the function reference, i.e.
viewModel::onAction
, instead of multiple lambdas will result in slightly better performance, especially when you use unstable classes likeviewModel
inside the lambda. Check out this video for more information.
That’s it and I hope you love the blog ❤️