I’m trying new `AnimatedContent` in a dev branch a...
# compose
v
I’m trying new
AnimatedContent
in a dev branch and I have a question: what if I can’t map
targetState
to actual content statically? Seems like I need to store map of states to content to be able to map any
targetState
to some content at any time, but that’s not always possible. Let’s say I have a screen backstack (a list, basically). Then my
targetState
would be the last index in that list. And let’s say I’m popping the front screen from backstack. Then the index to that screen is no longer valid and I can’t display exit animation for it. How to approach this?
d
Consider using the screen object the
targetState
, so that it can be mapped to the screen content even after it's popped off the backstack. Using index as the
targetState
is going to be problematic - If you push another screen onto the backstack before the previously popped off screen finishes animating out, you'd briefly have 3 active screens, two of which would have the same index.
v
I tried that and it doesn't work for the case when I just want to update front screen without transition - because the value and the key are same entity, every object change is considered as transition
d
Out of curiosity, why does the screen object change when the value and key stay the same? Without seeing the code I would suggest either using the key as the target state or override
equals
for the screen object to do more meaningful comparison. I could give you more specific suggestion if you don't mind sharing a snippet. 🙂
v
Copy code
sealed class Screen : Parcelable {

    abstract fun reduce(action: Action): Screen

    @Parcelize
    data class ClickTrackList(val state: ClickTrackListScreenState) : Screen() {
        ...
    }

    @Parcelize
    data class PlayClickTrack(val state: PlayClickTrackScreenState) : Screen() {
        ...
    }

    @Parcelize
    data class EditClickTrack(val state: EditClickTrackScreenState) : Screen() {
        ...
    }

    @Parcelize
    data class Metronome(val state: MetronomeScreenState?) : Screen() {
        ...
    }

    @Parcelize
    data class Settings(val state: SettingsScreenState?) : Screen() {
        ...
    }

    @Parcelize
    data class SoundLibrary(val state: SoundLibraryState?) : Screen() {
        ...
    }
}

@Composable
fun ContentView(
    screen: Screen,
    position: Int,
    dispatch: Dispatch,
) {
    val modifier = Modifier.fillMaxSize()

    val previousPosition = remember { mutableStateOf(position) }
    val isPush = remember { mutableStateOf(true) }

    if (position != previousPosition.value) {
        isPush.value = position > previousPosition.value
        previousPosition.value = position
    }

    AnimatedContent(
        targetState = position,
        transitionSpec = {
            if (isPush.value) {
                slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Left) with
                        slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Left)
            } else {
                slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Right) with
                        slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Right)
            }
        }
    ) { targetPosition ->
        val targetScreen by remember { screen }

        when (@Suppress("NAME_SHADOWING") val screen = targetScreen) {
            is Screen.ClickTrackList -> ClickTrackListScreenView(screen.state, modifier, dispatch)
            is Screen.PlayClickTrack -> PlayClickTrackScreenView(screen.state, modifier, dispatch)
            is Screen.EditClickTrack -> EditClickTrackScreenView(screen.state, modifier, dispatch)
            is Screen.Metronome -> MetronomeScreenView(screen.state, modifier, dispatch)
            is Screen.Settings -> SettingsScreenView(screen.state, modifier, dispatch)
            is Screen.SoundLibrary -> SoundLibraryScreenView(screen.state, modifier, dispatch)
        }
    }
}
A bit pseudocody for compactness, sorry. Overriding
equals
is interesting idea… 🤔 feels a bit hacky though
From the code above you can see that my key is position in backstack, and my value is
screen
, so that change in position results in animation but change in value without changing position should not trigger animation (because it’s just updating content of front screen).
The problem in the snippet above is that I should remember which position is which screen. In the snippet I
remember
screen inside passing lambda but this blocks any further updates of this screen without changing position. I can’t just pass
screen
as
targetState
because it will trigger animation on every little `screen`’s state change
d
I agree with you that
screen
as it is designed isn't suitable as
targetState
. But how do you intend to handle the following sequence of events when using position as the key: backstack [B, A] backstack [B] // A popped off backstack [B, C] // C was pushed onto the stack while A is animating out. A and C would have the same
position
at the end of that example, should A disappear immediately, or continue animating out? Using position alone as the key would prevent you from achieving the latter.
If you don't care if A disappears immediately in the case of conflicting
position
s, you could simply create a
MutableMap
that keeps the up to date mapping between position and state.
Copy code
val map = remember { mutableMapOf<Int, Screen>() }
map[position] = screen
AnimatedContent(...) {targetState ->
   map[targetState]?.let { // return appropriate composable based on the screen object } ?: {}
}
Didn't test this snippet. Though this is the main idea. If you'd like to also clean up the map, you could use the Transition-based AnimatedContent API, and reset the map if
transition.targetState == transition.initialState
v
Good point about conflicting positions. I'll consider redesign this to be more robust. And good tip about cleanup in this case, I missed that API. Thank you very much Doris for all help ❤️
👍 1