Home > front end >  Jetpack Compose MutableTransitionState currentState holds wrong value
Jetpack Compose MutableTransitionState currentState holds wrong value

Time:10-17

I use AnimatedVisibility to animate deleting an item from a LazyColumn together with MutableTransitionState to catch the end of animation to delete this item from LazyColumn's list.

I composed a handy extension function for this:

@ExperimentalTransitionApi
fun MutableTransitionState<Boolean>.transitionState(): TransitionState =
         if (this.isIdle && this.currentState)  TransitionState.Visible
    else if (this.isIdle && !this.currentState) TransitionState.Invisible
    else if (!this.isIdle && this.currentState) TransitionState.Disappearing
    else TransitionState.Appearing


enum class TransitionState(private val whatever: Int) {
    Visible(1),
    Appearing(2),
    Invisible(-1),
    Disappearing(-2)
}

It is correct in a sense that it returns correct values (tested), but currentState appears to be false only initially, so I can't catch the only event I'm interested in - Invisible.

Here is my LazyColumn:

val items by viewModel.itemsFlow.collectAsState()

LazyColumn(
    verticalArrangement = Arrangement.spacedBy(10.dp),
    contentPadding = PaddingValues(horizontal = 15.dp, vertical = 15.dp)
) {
    items(items) { item ->
        val animationState = remember {
            MutableTransitionState(false) // The only time currentState is false!
        }.apply { targetState = true }
        AnimatedVisibility(
            visibleState = animationState,
            enter = slideInVertically()   fadeIn(),
            exit = slideOutHorizontally(
                targetOffsetX = { it*2 },
                animationSpec = tween(
                    durationMillis = 700
                )
            )
        ) {
            ItemCard(
                item = item,
                viewModel = viewModel,
                animationState = animationState
            )
        }
    }
}

My ItemCard has a Button that changes animationState.tagetValue to false, and the state is logged inside the card:

Card {
    Log.v("ANIMATION", "View ${item.name} is now ${animationState.transitionState().name}")
    Log.v("ANIMATION", "View ${item.name} has values: isIdle = ${animationState.isIdle}, currentState = ${animationState.currentState}")
        /*...*/
        Button(
            /*...*/
            onClick = {
                animationState.targetState = false
            }
        ) {/*...*/}
 }

My logs, quite unfortunately, are like this:

V/ANIMATION: View name is now Appearing
V/ANIMATION: View name has values: isIdle = false, currentState = false
V/ANIMATION: View name is now Appearing
V/ANIMATION: View name has values: isIdle = false, currentState = false
V/ANIMATION: View name is now Visible
V/ANIMATION: View name has values: isIdle = true, currentState = true
V/ANIMATION: View name is now Visible
V/ANIMATION: View name has values: isIdle = true, currentState = true
// After I click the button:
V/ANIMATION: View name is now Disappearing
V/ANIMATION: View name has values: isIdle = false, currentState = true
V/ANIMATION: View name is now Disappearing
V/ANIMATION: View name has values: isIdle = false, currentState = true
V/ANIMATION: View name is now Disappearing
V/ANIMATION: View name has values: isIdle = false, currentState = true

So where's the invisible state, i.e. false currentState? Have I done something wrong?

CodePudding user response:

Each time you change state, it triggers related views recomposition. And you set animationState to true on each recomposition with .apply { targetState = true }.

Most likely you want to show it animated at the beginning, then you need to use LaunchedEffect: it will be called only once when the view appears.

val animationState = remember {
    MutableTransitionState(false)
}
LaunchedEffect(Unit) {
    animationState.targetState = true
}

Read more about recompositions in Thinking in Compose and about state in Compose in state documentation.

CodePudding user response:

After some testing...

I tested this type of animation on a separate project and figured out that the AnimatedVisibility content gets disposed before the animationState emits its final value, so you can't possibly catch it from inside the view you are animating.

Following @Philip Dukhov advice, my LazyColumns item is now like this:

            val animationState by remember {
                mutableStateOf(MutableTransitionState(false))
            }
            LaunchedEffect(Unit) {
                animationState.targetState = true
            }
            when(animationState.transitionState()) {
                TransitionState.Invisible -> viewModel.deleteInvisibleItems()
                else -> {}
            }
            Log.v("ANIMATION", "View ${item.name} is now ${animationState.transitionState().name}")

This catches emitted values perfectly.

  • Related