Is there any mechanism like `remember` but instead...
# compose
c
Is there any mechanism like
remember
but instead of recalculating if dependencies change, it recalculates if a given predicate is true? My use case is I have some UI content within an
AnimatedVisibility
block, where
visible = uiState is InitialUiState
. (My UI state is a sealed class with subtypes we can call
InitialUiState
and
SecondaryUiState
.) Some of the UI content I'm animating depends on a property contained within
InitialUiState
, but not
SecondaryUiState
. So when it's animating out of visibility, my
uiState
no longer contains the data needed in order to render the UI. I can wrap the content in
if (uiState is InitialUiState)
, but then the content just disappears instead of animating out. My thought is I need to memoize the most recent
InitialUiState
value, using something like
val initialUiState = rememberUnless(uiState is InitialUiState) { uiState as? InitialUiState }
, so that we can access the data contained within the last
InitialUiState
, even when the current value of
uiState
is a
SecondaryUiState
. In this example it would recalculate on every composition where
uiState is InitialUiState
, but "remember" and return the previous calculation otherwise.
c
DerivedStateOf?
c
@Colton Idle Never used
derivedStateOf
before, but after reading the docs I'm not sure how it would apply to my situation. It seems like it will always use the most recent value of the state it depends on, so when the
uiState
is a
SecondaryUiState
, how would I access the most recent
InitialUiState
?
For now I'm using the following custom helper, but I'm not sure if this is the best approach for my use case:
Copy code
@Composable
inline fun <T> rememberAndRecalculateIf(predicate: Boolean, calculation: @DisallowComposableCalls () -> T): T {
    var recalculationKey by remember { mutableStateOf(false) }
    if (predicate) {
        // changing the key means we should recalculate, and we only change it if the predicate is true
        recalculationKey = !recalculationKey
    }
    return remember(recalculationKey, calculation)
}
This definitely seems like a shortfall of
AnimatedVisibility
, to not capture and make available within its scope the last Snapshot state before
visible
became false...
c
Hm. Def over my head. Sorry!
d
It seems like
AnimatedContent
would work better for your use case, where you specify what the UI looks like for
initialUIState
and
{}
(i.e. empty) for
SecondaryUiState
. Then when the targetState changes from initialUIState to SecondaryUiState,
AnimatedContent
will keep a reference to the previous state until the previous content has been animated out.
👍 1
c
Thanks @Doris Liu, I was not familiar with
AnimatedContent
! Following the flowchart in the documentation led me to
AnimatedVisibility
, because I wouldn't say the answer to "Animating appearance/disappearance" is "No" for my use case 😅 So
AnimatedContent
works except for one major issue -- since my
targetState
is the current
uiState
(as it contains the data I need to keep a reference to while animating out), it is now running the transition each time there are any changes to the data contained within
InitialUiState
! Ideally I want to only run the transition each time
uiState is InitialUiState
changes, but if I set that to my
targetState
then I only have access to that boolean within the
content
block, whereas what I really need is a reference to the previous
InitialUiState
, so that I can animate that data out once the actual
uiState
is
SecondaryUiState
. So, is there any way to run the transition every time a particular state changes, but then each time that state changes, also hold onto additional state to make available within the
content
block? If not, I don't see any way to achieve my (pretty normal) use case with the current animation APIs.
d
We need to make
AnimatedContent
perceive all
InitialUiState
instances as the same state. One thing you could try is overriding
equals
in
InitialUiState
. Seems like AnimatedContent should have customizable equality support.
Good point on the flowchart, we'll work out a better decision tree there. The challenge is to make it concise. 🙂
👍 1
c
When I override
equals
in
InitialUiState
to return true if the other value is also an
InitialUiState
, then whenever I modify a property of
InitialUiState
, the UI does not update to reflect the change, I assume because the Compose runtime thinks the
State
has not changed since it sees that it is "equal" to the previous
State
.
d
This will probably work for you, without overriding
equals
:
Copy code
val targetState = key(state is InitialUiState) {
        rememberUpdatedState(state)
    }
    AnimatedContent(targetState) { target ->
        if (target.value is InitialUiState) {
             // Content for initial ui state
        } else {
            // Content for secondary ui state
        }
    }
c
Adding customizable equality support to
AnimatedContent
seems like it would get the job done. Although I wonder if this would be flexible enough for all use cases -- I could see the
content
block potentially needing to reference multiple state objects from when the old content started animating out. Almost seems like when the
targetState
changes, instead of calling the
content
block with the previous
targetState
to animate the outgoing content, we should enter into a
Snapshot
taken just before
targetState
changed, and then call the
content
block inside of that
Snapshot
to animate the outgoing content. Wouldn't that automatically take care of making all state objects from the old content still available while it's animating out?
In the mean time I will try what you just proposed
d
Almost seems like when the 
targetState
 changes, instead of calling the 
content
 block with the previous 
targetState
 to animate the outgoing content, we should enter into a 
Snapshot
 taken just before 
targetState
 changed, and then call the 
content
 block inside of that 
Snapshot
 to animate the outgoing content.
We are working on such a concept in the low level of Compose. It would be very useful as a default behavior for removal animation, once it's done.
❤️ 1
c
Oh nice! Looking forward to it
Your suggestion of using
key
and
rememberUpdatedState
seems to work, but I can't figure out why. What exactly is
key
doing here? I can't tell from the docs, and the examples I've seen all have to do with identifying composables within a for loop. At first I assumed it would only run the block to update the state if the value of
state is InitialUiState
changed, but I tested and this doesn't seem to be the case. So if the
targetState
is changing even when it's changing from one
InitialUiState
to another
InitialUiState
, why does no transition occur between the two?
d
key
specifies the identity of containing composable. In that sense, it's the same as specifying different keys for different content in a loop. What it does here is to create a new
rememberUpdatedState
composable as the identity changes. Therefore you get a new
State<UiState>
instance when changing from InitialUiState to SecondaryUiState. When changing from one InitialUIState to another, the key stays the same (i.e. true), only the
State<UiState>#value
gets updated.
c
Ah, I see now, thank you!
👍 1