Avoid return statements in Jetpack Compose!

Adrian Tache
Android Ideas
Published in
3 min readOct 2, 2023

If there’s one thing that has created the weirdest errors I’ve seen so far, it’s return statements in composable functions.

AI image generation is fun. This one’s a “mobile magician”.

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.

--

--