https://kotlinlang.org logo
#compose
Title
# compose
l

Lukasz Kalnik

02/16/2022, 4:56 PM
How do I reinitialize from outside the value which is `remember`ed in a composable? I have a dialog where you can enter a name. Every time the dialog is shown I want to set initially a specific name from the ViewModel (source in thread)
Copy code
@Composable
fun EnterNameDialog(
    uiState: EnterNameDialogState
) {
    val currentText = remember { mutableStateOf(uiState.name) }

    AlertDialog(
        // ....
        text = {
            TextField(
                value = currentText.value
                onValueChange = { currentText.value = it }
    // ...
}

data class EnterNameDialogState(
    val show: Boolean,
    val name: String,
)
Currently when I pass a string like
"test"
inside of
uiState.name
, when setting a breakpoint it gets immediately set to empty string in
currentText.value
. It is also empty in the UI obviously.
I only set the string to
""
when the user dismisses/confirms the dialog. Is it possible that the
remember
remembers this old state from previous composition?
a

Adam Powell

02/16/2022, 5:06 PM
do you have a stack trace from where
currentText.value
is set to
""
?
generally this kind of shape is discouraged though, usually you would use
uiState.name
directly and report new values up either via callback or method on
uiState
so that they can be applied to
uiState
l

Lukasz Kalnik

02/16/2022, 5:13 PM
Yes, that's what we had before (
onValueChange
was directly calling up to ViewModel callback which was only in charge of the
name
etc.). However we have changed that after adding a trailing icon to clear the text. It was convenient to keep the editing of the text internally via
remember
and only send the text up to ViewModel when the user confirmed the dialog.
But I might get back to the previous architecture where the whole state was only kept and updated in ViewModel as it seems cleaner and less error prone.
a

Adam Powell

02/16/2022, 5:17 PM
what does the call site look like here?
l

Lukasz Kalnik

02/16/2022, 5:17 PM
Where I set
currentText = remember // ...
?
a

Adam Powell

02/16/2022, 5:17 PM
the place where
EnterNameDialog
is called
l

Lukasz Kalnik

02/16/2022, 5:49 PM
Ok, I removed the
val currentText = remember { }
line and added the callback to viewModel for
onValueChange
and everything works perfectly.
I guess it's indeed not a good idea to mix keeping the state in ViewModel with using
remember
.
I found out the cause why
currentText.value
was always reset to
""
.
I misunderstood how
remember { mutableStateOf(value) }
works. I thought you can kind of reinitialize it from outside, by passing a new
value
.
I forgot that
remember
makes the computation inside lazy, and only calls it the first time, initializing it with
value
.
We passed the
value
as
""
indeed at the first call site of
EnterNameDialog
, when user entered the screen. So we couldn't change it later because of
remember
.
Update: actually it's much simpler than that. You can mix having state in a ViewModel and in
remember
. You just need to use
remember
with an additional parameter as key. This way the remembered value will get recalculated every time the key changes:
Copy code
val currentText = remember(uiState.name) { mutableStateOf(uiState.name) }
That updates
currentText
every time
uiState.name
changes.
Another update in case someone finds this thread while having a similar problem. Actually it's a bit more complex. If you couple your
remember
to
uiState.name
, it will only recalculate its value if it receives a new
uiState.name
but with a different value than the previous
uiState.name
it was initialized with. So the approach from the message above doesn't work in the following scenario: 1. App shows the dialog, initializing it with an empty string as
uiState.name
2. User types something like
"test"
3. User clicks the "OK" button 4. Dialog is dismissed 5. User opens the dialog again 6. App code again initializes the dialog with an empty string as
uiState.name
7.
remember
looks at
uiState.name
and compares it with its previous value 8. Both values are empty strings, so
remember
determines the key didn't change 9. Because of that,
remember
decides there's no need to reinitialize
currentText
so it provides the cached value 10. But the cached
currentText
value was last updated by the user typing
"test"
the previous time the dialog was opened 11. So dialog now opens with the old
"test"
text instead of the empty string from
uiState.name
The solution? You can only use as
key
for
remember
reinitialization something that is guaranteed to change every time you want to reinitialize
currentText
. In my case
EnterNameDialogState
contains a
show
variable, which defines if the dialog is shown or hidden. So if I want to reinitialize the remembered
currentText
every time the dialog is shown or hidden, I just couple the
remember
invocation to
uiState.show
(instead of
uiState.name
as before). Note that inside of
mutableStateOf()
we still have
uiState.name
, because that's what we want to reinitialize
currentText
with:
Copy code
val currentText = remember(uiState.show) { mutableStateOf(uiState.name) }
What it does is "whenever
uiState.show
is changed, reinitialize
currentText
with a
MutableState
of
uiState.name
, otherwise provide cached value".