How would you handle this `when` construct in a co...
# compose
l
How would you handle this
when
construct in a composable function:
Copy code
when (val state = presenter.uiState) {
        UiState.Success -> {
           CallComponent() // composable code
        }
        is UiState.Failure -> {
           CallAnotherComponent() // composable code
           state = "setState" // non-composable code
        }
    }
presenter.uiState
is a mutableStateOf. Optimally this code should only be called when presenter.uiState has changed but instead it's called on every recompose. What's the right way to handle this?
p
Sounds like you are looking for
LaunchedEffect
:
Copy code
LaunchedEffect(presenter.uiState){ 
    if(presenter.uiState is UiState.Failure) {
        <http://presenter.xyz|presenter.xyz>()
    }
}
the LaunchedEffect block should only be called when
presenter.uiState
changes now. Does that help?
j
are you remembering it?
If you use mutableStateOf without also using remember, then the state is reinitialized
z
Your code shouldn’t depend on how many times it’s recomposed. Usually if that’s not the case, you’re doing some side effect directly in the composition that should be in a side effect (like Patrick suggested). Calling compose code is fine, so the only side effect it looks like you’re doing is that
state =
- what is that doing?
l
@patrick @Zach Klippenstein (he/him) [MOD] I had
LaunchedEffect
in mind because it can also call composable code but since it starts a coroutine + it re-launches the coroutine when the key changes I wasn't sure if this fits my needs. In the first place it seemed a bit overpowered for this case. For example I have another screen with the same construct...when UiState.Loading, I call a composable function that shows a loading component and when UiState.Success I show a LazyColumn filled with data which is passed with Success. Should this be in a coroutine and cancelled on state change? I'm not sure.. 😕
Calling compose code is fine
My bad, makes sense.
state =
 - what is that doing?
I'm doing exactly this:
Copy code
var connectionState: BluetoothConnectionState by remember {
        mutableStateOf(BluetoothConnectionState.Initial)
    }

        when (val state = presenter.uiState) {
            UiState.Success -> {
                Timber.tag("In screen").d("device connected.")
                connectionState = BluetoothConnectionState.Connected
            }
            is UiState.Failure -> {
                connectionState = BluetoothConnectionState.Disconnected
                showErrorMessage(state.message)
            }
        }
@Javier You are right. I'm remembering it, I should have put the missing piece of code into my initial post.
z
What does showErrorMessage do?
l
I had a look into
LaunchedEffect
and what perfectly works for me is:
Copy code
remember(presenter.uiState) {
   ..
}
The only problem I have with this solution is that
remember
returns T and since my only code in this block is the when statement it is handled as expression and forces me to be exhaustive + remember complains about "remember calls must not return Unit" @Zach Klippenstein (he/him) [MOD] showErrorMessage can be one of two:
Copy code
@Composable
fun showErrorMessage(message: String?) {
    val context = LocalContext.current
    Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}

fun showErrorMessage(ctx: Context, message: String?) {
    Toast.makeText(ctx, message, Toast.LENGTH_LONG).show()
}
Depends on from where it is called
z
Ah, didn’t realize it was composable. The composable one should start with an uppercase letter, as per the naming guidelines.
But inside that composable, makeText is a side effect and should be done from an effect function, not directly.
That error message you’re getting about remember returning unit is probably there to prevent people from trying to use remember to do side effects, because it is not an effect function. Effect functions do more than just extend lifetime beyond a single composition pass, they also defer execution until the composition is successfully applied, as well as ensure their code is ran on the main thread
l
Thanks for looking. So remember(key) is not the right way? Should I stick with
LaunchedEffect
? And what do you think about my considerations:
In the first place it seemed a bit overpowered for this case. For example I have another screen with the same construct...when UiState.Loading, I call a composable function that shows a loading component and when UiState.Success I show a LazyColumn filled with data which is passed with Success. Should this be in a coroutine and cancelled on state change? I'm not sure..
z
I’m not sure how you’d put those in a coroutine –
LazyColumn
is a composable function, you can only call it from a composable
l
Sorry I didn't explain myself properly. I have another screen where I have this:
Copy code
when (val state = presenter.uiState) {
        is UiState.Loading -> {
            ShimmerList()
        }
        is UiState.Data<*> -> {
            StatusListComponent(
                model = state.model as StatusParameterPresenterModel,
                innerPadding = innerPadding
            )
        }
        is UiState.Failure -> {
           ShowErrorMsg(state.message)
        }
    }
Wouldn't be
LaunchedEffect
with its coroutine under the hood too much? Everytime
presenter.uiState
changes, the coroutine is cancelled and re-launched. This would happen 3 times and I don't even need a coroutine or what do you think? Is there a effect function which handles composable and non-composable code in main thread but without launching a coroutine?
z
There is no effect function that allows you to call composable code, because composables aren’t side effects.
DisposableEffect
creates an effect that lives as long as it’s in the composition without launching a coroutine, but requires you to return an
onDispose{}
handler. Practically I don’t think there’s much difference in overhead, and it’s better to use whatever makes your code more readable
I don’t see anything wrong with that last snippet you posted – you’re calling different composables based on some state, that’s pretty common
l
ok so i dont need a side effect function?
for the last snippet?
z
Assuming
ShimmerList
,
StatusListComponent
, and
ShowErrorMsg
are all composables, no
l
ok then it's clear to me now. Thanks for your patience ^^ ❤️
z
The only thing you’d need an effect for would be calls to non-composable-aware functions, like
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
l
ok then I would need the LaunchedEffect because
ShowErrorMsg(state.message)
is such a call
z
Earlier you posted two functions:
@Composable fun showErrorMsg
(which should be named
ShowErrorMsg
, because it’s composable), and
fun showErrorMsg
(correctly named with lower pascale case, because it’s not composable). If you are calling the composable
ShowErrorMsg
, you don’t need (and can’t use, the compiler will complain) an effect
You can’t call composables from effects. You don’t need effects to call composables. Composables must be called from other composables.
Effects are for code that is not aware of Compose (ie. mutates program state that is not stored in a
MutableState
). Effects are generally needed any time you’re interacting with other APIs that don’t know about compose (e.g. toasts, IO, analytics, etc).
l
Yeah sorry I goth this. But even ShowErrorMsg is a composable function and dont need an effect function, it calls
makeText
which isn't compose-aware. Is this a problem here?
z
This would be valid code (although the API is a bit weird, since toasts are transient):
Copy code
@Composable fun ErrorMessageToast(message: String) {
  val context = LocalContext.current
  LaunchedEffect(message) {
    Toast.makeText(context, message).show()
  }
}
l
ah ok. What do you mean with transient. I don't have to do it like this. Do you have a better solution to show a toast which I can use?
z
1. Composables that return unit should be named as nouns, using upper-pascale-case. Not verbs. They represent entities, not actions. 2. The
Toast
API doesn’t know anything about compose, so it must be called from an effect. You could use
LaunchedEffect
or
DisposableEffect
here. 3.
LocalContext.current
is a composable property, so it must be called from the composable, and not the effect.
I would recommend using the compose Snackbar API, which is designed to be used from compose. The android toast API is just kind of weird unfortunately
l
Thank you so much Zach. I'm really sorry for the inconvenience 🙏
z
Another way to design this code, which is a bigger change, would be to not make your composable responsible for showing the toast at all. Instead, your view model can just show the toast itself as soon as it gets the failure response/event.
l
I thought toasts shouldn't be called from viewmodel because it uses
Context
and this should be avoided in viewmodel I also have never seen this before?
z
Fair point. I think there is just no good way to use the toasts api 😛 (i’m sure many disagree)
i
You should not use Toasts at all, tbh. Use snackbars that are a proper part of your UI hierarchy
l
Yeah thanks Ian, I found a post from Adam where he says the same and Zach mentioned it too. I won't use Toast anymore. @Zach Klippenstein (he/him) [MOD] I have done it now like this:
Copy code
val scaffoldState = rememberScaffoldState()

    val scope = rememberCoroutineScope()

    var connectionState: BluetoothConnectionState by remember {
        mutableStateOf(BluetoothConnectionState.Initial)
    }

    when (val state = presenter.uiState) {
        UiState.Success -> {
            Timber.tag("DataCollect").d("In success.")
            connectionState = BluetoothConnectionState.Connected
        }
        is UiState.Failure -> {
            Timber.tag("DataCollect").d("In failure.")
            connectionState = BluetoothConnectionState.Disconnected
            state.message?.let { msg ->
                scope.launch {
                    scaffoldState.snackbarHostState.showSnackbar(msg)
                }
            }
        }
    }
You said
Effects are for code that is not aware of Compose (ie. mutates program state that is not stored in a 
MutableState
).
But the snackbar is called twice. Also
connectionState
is set twice. I'm just wondering how this matches with your statement because connectionState is mutablestate. Can you explain this case please? I guess the state should not called twice even it is mutablestate, right?
j
If you treat snackbars as state, you will have to check when it is gone to change the state to idle or something so. Another approach is using events for errors
z
I think here’s where you want something like:
Copy code
is UiState.Failure -> {
  LaunchedEffect(state.message) {
    scaffoldState.snackbarHostState.showSnackbar(msg)
  }
}
This way the lifetime of the snackbar itself will be tied to the value of your state being Failure. If the message changes, the snackbar will be reshown. If the state changes to Success, the snackbar will be hidden. And you don’t need the
rememberCoroutineScope
.
scope.launch
is a “compose-unaware side effect”, so it should not be called from a composable directly (that’s why
LaunchedEffect
exists)
l
ahh ok. And what about setting
connectionState
? Isn't this a problem too:
Copy code
is UiState.Failure -> {
            connectionState = BluetoothConnectionState.Disconnected
            LaunchedEffect(state.message) {
                scaffoldState.snackbarHostState.showSnackbar(state.message!!)
            }
        }
Lets say
connectionState
is set to Connected further below in code. The functions is recomposed and the state is set to Disconnected again, I would end up with incorrect state 🤔 Currently success and failure are called twice (have logged it)
z
yea, this seems backwards.
connectionState
looks like a higher-level state that should live in your view model or higher, not in your composable. And then it should be set somewhere upstream, probably by the same logic that sets your
uiState
itself.
🙏 1
s
I scrolled through most of the thread; but I wanted to add for future searchers: Remember blocks should not have side effects, and will run when a composition does not end up getting committed. A big difference between
LaunchedEffect
etc and
remember
is the lifecycle: 1.
remember
runs in line with recomposition, to allow values to be available immediately (e.g.
remember { someColor }
is available right after the line) 2.
*Effect
run when the composition is successful, which doesn't always happen.
l
Thanks for participating Sean 🙂