One Off the Slack: Is It OK to Have Stateful Widgets?

Ryan Simon asked:

What’s the community’s take on stateful Composables like a favorite button? It seems logical that we want to show the user the filled state of Favoriting something right away, and then update our internal ViewModel state once our network request goes through.

@Composable
fun FavoriteButton(
    onClick: (Boolean) -> Unit,
    isFavorited: Boolean,
    modifier: Modifier = Modifier
) {
    val fill = remember { mutableStateOf(isFavorited) }

    IconButton(onClick = {
        fill.value = !fill.value
        onClick.invoke(isFavorited)
    }, modifier = modifier) {
        Icon(
            imageVector = if (fill.value) Icons.Default.Favorite else Icons.Outlined.FavoriteBorder,
            contentDescription = stringResource(
                id = if (fill.value) R.string.remove_favorite_button else R.string.favorite_button
            ),
            tint = Theme.colors.onBackground,
        )
    }
}

In that sample, FavoriteButton() takes isFavorited as the initial state. The actual widget state is fill. It also has a bug, passing isFavorited to onClick() instead of fill, so the onClick() handler never gets the actual state.

Nathan Schulzke was not a fan:

It seems like this way of doing it leaves you no good way to reset the icon state if the network request fails for any reason. If you need to force a reset of the visual state, you’d need to set isFavorited to true and then switch it back to false, which is rather clunky and somewhat defeats the purpose (avoiding altering the ViewModel until it’s actually worked).

If instead you just update the ViewModel state right away but have the ViewModel roll the state back if the network request fails, it would work fine right out of the box.

In general, I have a strong preference for using a single source of truth where possible. I’ve seen far too many UIs that duplicate their state in one way or another, and it always comes back to bite you later by getting out of sync in hard-to-debug ways.

Ryan agreed:

You know, i hadn’t fully considered the disconnect between the ViewModel’s state during the network request and the button’s state.

You’re right lol. I think it’s best to make it stateless.

My entire architecture is built around state being managed by the ViewModel as the source of truth.

Google’s Zach Klippenstein had a slightly different take, with the repository being the single source of truth, not the viewmodel:

I think this could be handled by your repository layer as well, and might even argue that’s the best place to put it, since the whole job of the repository is to reconcile local and network state.


Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!