androidandroid-jetpack-compose

Support configuration change with MutableTransitionState


I am using MutableTransitionState to support animation for a Popup in my Compose app. The problem is, the Popup disappears after a configuration change such as screen rotation because I am using expandedState = remember { MutableTransitionState(false) }.

I would like the Popup to survive a configuration change to improve user experience. In order to achieve this, I've written the following code:

data class Holder(var expandedState: MutableTransitionState<Boolean>)

val expandedSaver = Saver<Holder, MutableTransitionState<Boolean>>(
    save = { it.expandedState},
    restore = { Holder(it) }
)

@Composable
public fun SomeComposable() {
    val holder = rememberSaveable(stateSaver = expandedSaver) {
         mutableStateOf(Holder(MutableTransitionState(false)))
    }
    ...
}

However, when I run the code, I get the usual error complaining that I should consider implementing a custom Saver for this class despite already using a custom saver!


Solution

  • Using rememberSaveable on a MutableTransitionState won't work.

    You use the rememberSaveable overload with the stateSaver parameter. That implies that the value is a MutableState which is unnecessary. You probably meant to use the overload using the saver parameter.

    But that won't work either. You would have to construct a MutableTransitionState off the single expanded property of your Holder, but a MutableTransitionState has both a currentState and a targetState property. You would need to save both if you want to restore the MutableTransitionState to what it was before. Furthermore, there is a third property isRunning which is internal and cannot be accessed by your code, so you will never be able to restore it.

    Bottom line: You cannot fully save and restore a MutableTransitionState. After a configuration change at least some parts of the state are irrevocably lost.


    If you are only interested in saving the targetState then the second approach you had in your original question is the way to go. You just need to update the MutableTransitionState's targetState when expanded changes1:

    var expanded by rememberSaveable { mutableStateOf(false) }
    val expandedState = remember { MutableTransitionState(expanded) }
        .apply { targetState = expanded }
    

    The rest of your code shouldn't touch expandedState directly anymore, so it would be best to never save the MutableTransitionState in a variable in the first place. That would prevent accidently doing something like expandedState.targetState = !expandedState.targetState. When you only need it for an AnimatedVisibility you could rewrite it like this:

    var expanded by rememberSaveable { mutableStateOf(false) }
    
    AnimatedVisibility(
        visibleState = remember { MutableTransitionState(expanded) }
            .apply { targetState = expanded },
        // ...
    )
    

    Now only the expanded variable is accessible by the rest of your code (and it survives configuration changes), the MutableTransitionState is hidden but will always be updated accordingly when expanded changes.


    1 expanded is used twice here. The first usage is needed when the MutableTransitionState is constructed to set the initial value. It is behind a remember, so it is only executed once (until a configuration change takes place, then it is executed again and a new MutableTransitionState is created).
    The second usage is needed to actually start a transition. Whenever the MutableState expanded changes, a recomposition occurs. The remember lambda is skipped, but the apply is executed again, setting targetState to the new value.