Lilly
04/14/2021, 1:02 PMwhen
construct in a composable function:
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?patrick
04/14/2021, 1:14 PMLaunchedEffect
:
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?Javier
04/14/2021, 1:18 PMJavier
04/14/2021, 1:19 PMJavier
04/14/2021, 1:20 PMZach Klippenstein (he/him) [MOD]
04/14/2021, 2:19 PMstate =
- what is that doing?Lilly
04/14/2021, 2:56 PMLaunchedEffect
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 fineMy bad, makes sense.
I'm doing exactly this:- what is that doing?state =
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.Zach Klippenstein (he/him) [MOD]
04/14/2021, 3:16 PMLilly
04/14/2021, 3:22 PMLaunchedEffect
and what perfectly works for me is:
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:
@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 calledZach Klippenstein (he/him) [MOD]
04/14/2021, 3:35 PMZach Klippenstein (he/him) [MOD]
04/14/2021, 3:36 PMZach Klippenstein (he/him) [MOD]
04/14/2021, 3:38 PMLilly
04/14/2021, 3:50 PMLaunchedEffect
? 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..
Zach Klippenstein (he/him) [MOD]
04/14/2021, 3:50 PMLazyColumn
is a composable function, you can only call it from a composableLilly
04/14/2021, 4:13 PMwhen (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?Zach Klippenstein (he/him) [MOD]
04/14/2021, 4:15 PMZach Klippenstein (he/him) [MOD]
04/14/2021, 4:16 PMDisposableEffect
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 readableZach Klippenstein (he/him) [MOD]
04/14/2021, 4:16 PMLilly
04/14/2021, 4:17 PMLilly
04/14/2021, 4:17 PMZach Klippenstein (he/him) [MOD]
04/14/2021, 4:17 PMShimmerList
, StatusListComponent
, and ShowErrorMsg
are all composables, noLilly
04/14/2021, 4:18 PMZach Klippenstein (he/him) [MOD]
04/14/2021, 4:22 PMToast.makeText(context, message, Toast.LENGTH_LONG).show()
Lilly
04/14/2021, 4:25 PMShowErrorMsg(state.message)
is such a callZach Klippenstein (he/him) [MOD]
04/14/2021, 4:34 PM@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 effectZach Klippenstein (he/him) [MOD]
04/14/2021, 4:35 PMZach Klippenstein (he/him) [MOD]
04/14/2021, 4:37 PMMutableState
). Effects are generally needed any time you’re interacting with other APIs that don’t know about compose (e.g. toasts, IO, analytics, etc).Lilly
04/14/2021, 4:38 PMmakeText
which isn't compose-aware. Is this a problem here?Zach Klippenstein (he/him) [MOD]
04/14/2021, 4:39 PM@Composable fun ErrorMessageToast(message: String) {
val context = LocalContext.current
LaunchedEffect(message) {
Toast.makeText(context, message).show()
}
}
Lilly
04/14/2021, 4:40 PMZach Klippenstein (he/him) [MOD]
04/14/2021, 4:40 PMToast
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.Zach Klippenstein (he/him) [MOD]
04/14/2021, 4:41 PMLilly
04/14/2021, 4:42 PMZach Klippenstein (he/him) [MOD]
04/14/2021, 4:42 PMLilly
04/14/2021, 4:47 PMContext
and this should be avoided in viewmodel I also have never seen this before?Zach Klippenstein (he/him) [MOD]
04/14/2021, 5:32 PMIan Lake
04/14/2021, 5:42 PMLilly
04/14/2021, 5:57 PMval 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 aBut the snackbar is called twice. Also).MutableState
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?Javier
04/14/2021, 6:02 PMJavier
04/14/2021, 6:03 PMZach Klippenstein (he/him) [MOD]
04/14/2021, 6:05 PMis 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
.Zach Klippenstein (he/him) [MOD]
04/14/2021, 6:05 PMscope.launch
is a “compose-unaware side effect”, so it should not be called from a composable directly (that’s why LaunchedEffect
exists)Lilly
04/14/2021, 6:14 PMconnectionState
? Isn't this a problem too:
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)Zach Klippenstein (he/him) [MOD]
04/14/2021, 6:35 PMconnectionState
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.Sean McQuillan [G]
04/14/2021, 7:20 PMLaunchedEffect
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.Lilly
04/14/2021, 8:40 PM