Avoid return statements in Jetpack Compose!
If there’s one thing that has created the weirdest errors I’ve seen so far, it’s return statements in composable functions.
Since composable functions are, after all, functions, you might be tempted to write code like this:
@Composable
fun SomeView() {
val state = someFlow.collectAsState(null).value ?: return
NavHost(...) {
...
}
}
It’s not the cleanest code, but it works fine in general, and it saves you from dealing with that annoying null
initial value for the state variable, right? Well, not so fast!
I’ve seen cases where code like this can result in that flow never getting new states, so, after that initial return, we never get a proper value to actually draw the screen we want. Even worse, after a configuration change, if anything tries to interact with the navController
and that return triggers beforehand, it may lead to a crash, since its navGraph
never got set by the NavHost
.
Now, let’s move to a more subtle situation where I had issues with returns:
@Composable
fun SomeView() {
// Assume this gets set by a LaunchedEffect.
var error: Exception? by remember { mutableStateOf(null) }
val state by someFlow.collectAsState(null)
if (state.condition) {
val data = state.data ?: return
SomeOtherView(data)
} else {
Loader()
}
if (error != null) {
AlertDialog(error)
}
}
For some reason, you notice that your error dialog isn’t showing up in this screen, and scanning the code, it’s not obvious what’s wrong. You debug the alert dialog, the error setting mechanism, but everything seems fine, right?
Wrong! The fact that we’re returning if the data is null
means that everything after that return doesn’t get to run, even though it’s not related to it, so to speak. It’s certainly not a mindblowing conclusion to draw, but definitely something that I’ve seen happen a number of times so far.
So, of course, the correct code would become:
@Composable
fun SomeView() {
// Assume this gets set by a LaunchedEffect.
var error: Exception? by remember { mutableStateOf(null) }
val state by someFlow.collectAsState(null)
if (state.condition) {
state.data?.let { data ->
SomeOtherView(data)
}
} else {
Loader()
}
if (error != null) {
AlertDialog(error)
}
}
My point here isn’t that devs don’t know what return
does, but since composables are functions but behave more like classes, it’s easy to make mistakes with early returns, especially as the code inevitably changes over time.
This is why my recommendation is to simply always avoid using return
statements in composable functions, and just get used to letting the composition run on its own all the way through.
And, if you do want to write code that relies on this kind of logic, it’s better to extract it to a separate composable, just know that you’ll run the same risk if that component changes over time. It would look something like this:
@Composable
fun SomeView() {
// Assume this gets set by a LaunchedEffect.
var error: Exception? by remember { mutableStateOf(null) }
val state by someFlow.collectAsState(null)
ConditionBlock(state)
if (error != null) {
AlertDialog(error)
}
}
@Composable
fun ConditionBlock(state: Something) {
if (state.condition) {
val data = state.data ?: return
SomeOtherView(data)
} else {
Loader()
}
}
Hope this helps, I know I had a few of issues with this approach!
Thanks for reading this article. You can connect with me on LinkedIn.
If you liked this article, please hit the clap icon 👏 to show your support.