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

Garret Yoder

03/20/2024, 2:15 PM
Here's an abstract problem, and maybe this is just my fault and I put myself in a corner with design decisions I made in code, but I have an object that is a state and holds some data - along with a composable method that takes its data and puts out a composable. This state switches fairly frequently, in addition to becoming null to signal don't display. Before I animated it, it was as simple as
object?.theComposeMethod()
and it'd only display if it was not null, and would obviously display whatever the current object is. Now that I animated it, I didn't even think about the fact that
AnimatedVisibility(object!=null) {
object?.theComposeMethod()
}
will work only for the enter animation, not the exit because the value will immediately go null and remove from composition. There's two things I want to accomplish - animate a fade when the value changes, and get it to animate when it's removed. I really want to avoid rewriting a lot upstream of that just for a silly animation, I feel like the answer is no because once it's null its out of composition - but does anyone have a good suggestion?
v

vide

03/20/2024, 2:18 PM
You want
AnimatedContent
Your state is then retained by the container for the duration of the animation and passes it to the content lambda
g

Garret Yoder

03/20/2024, 2:22 PM
Huh I didn't know it kept it's own copy of the state. That was it, thanks!
v

vide

03/20/2024, 2:27 PM
It's not maybe worded very clearly in the docs iirc
g

Garret Yoder

03/20/2024, 2:28 PM
Yeah it's not always super clear when a scope in compose provides you with a local state, or if it's just a direct view to whatevers above it
I guess that would make sense that it has a local state since it'd have to render both composables from the same original value as it animates
Although you need a little mental gymnastics to view disappearance as multiple composables with different content 😅. This usecase is somewhat overlooked there too
g

Garret Yoder

03/20/2024, 2:30 PM
Yeah I think that just clicked in my head that it has to render both sides of the animation until it's over
Although the weirdest thing I don't think I've seen AnimatedContent do before is I set the transition with
transitionSpec = { fadeIn() togetherWith fadeOut() }
but it seems like it still does a scale animation when the value actually switches between null/not null. The value just changing between objects does do a fade.
Crossfade the same way worked fine, not sure why AnimatedContent didn't want to give up it's default scale animation
v

vide

03/20/2024, 2:42 PM
You need to disable the scale animation if you don't want it
Copy code
public abstract infix fun ContentTransform.using(
    sizeTransform: SizeTransform?
): ContentTransform
so for example:
Copy code
fadeIn() togetherWith fadeOut() using null
g

Garret Yoder

03/20/2024, 2:44 PM
Ahh okay, you have to specify the using, omitting doesn't imply nothing, got it
Guess I need to read the animation docs again haha
That's maybe another place the doc can be updated I guess, based on the doc I'd assume that the default is nothing, and if you wanted to add something you'd specify using. Especially since when you look at the default value of
transitionSpec
it's manually specifying that scale animation. Tacking on an animation by default that wasn't specified is also a bit counterintuitive.
m

Mofe Ejegi

03/20/2024, 4:44 PM
I also came across this issue in the past. My suggestion may be superfluous right now, but if you want to keep your
AnimatedVisibility
composable setup as it is, you can try out this simple modification:
Copy code
@Composable
fun <T : Any> NullableAnimatedVisibility(
    nullableArgument: T?,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandIn(),
    exit: ExitTransition = shrinkOut() + fadeOut(),
    content: @Composable AnimatedVisibilityScope.(T) -> Unit,
) {
    var nonNullableArgument by remember {
        mutableStateOf(nullableArgument)
    }

    LaunchedEffect(nullableArgument) {
        nullableArgument?.let { nonNullableArgument = it }
    }

    AnimatedVisibility(
        visible = nullableArgument != null,
        modifier = modifier,
        enter = enter,
        exit = exit,
    ) {
        nonNullableArgument?.let { content(it) }
    }
}
It remembers the object's state so it doesn't disappear immediately it's set to
null
. There's also a way to forget the arg after the content is finally animated away, but this should be sufficient for what you're asking.