Expanding Dialog in Jetpack Compose

Using AnimatedVisibility and embracing reusability

Katie Barnett
Bilue Product Design & Technology Blog
5 min readSep 11, 2022

--

Expansion!

When using apps we see dialogs for all sorts of reasons, we see them appear and disappear usually when an error has occurred or something important needs to be brought to the attention of the user. Not much thought is given to the elegance of appearing and disappearing boxes. In my effort to become better at animation and learn more Jetpack Compose I decided to make a reusable fun animation for displaying a dialog.

Starting with a pretty standard dialog with just a button, we can see the normal appear & disappear action:

Pretty standard dialog that just appears and disappears

This is not very interesting! Maybe it would work for a simple error that doesn’t occur very often but if this is something the user will see often we might want to make it a little smoother. How about a nice expand and disappear animation?

So how do we go about this? First up, I wanted to use the standard Jetpack Compose Dialog component. I know that this animation could be achieved in other ways without the dialog component but I wanted to make use of the shaded modal background and other standard capabilities like blocking user gestures beneath it.

Let’s get reusable

So we can make this component reusable for multiple dialogs, we can create a composable that takes in the content we want to show.

Defining the animation

We also need to define the animation. For this animation I am using the AnimatedVisibility API with a tween animation spec to give a nice smooth result, this can be easily customised to fit the user experience desired. This can be created as a composable that we can also reuse for other components.

Note — as of writing scaleIn and scaleOut are experimental animation APIs so the implementation may change in the future.

Trigger the scale in animation via a coroutine

Now we can to make use of this animation in our dialog component, using remember to store a trigger for the animation and a coroutine run inside a LaunchedEffect block to start the animation.

If you try this with a complex dialog layout you may see some initial jankiness as the dialog is built or the dialog may only appear after the animation has started. This is because it take time to inflate the dialog, especially if it has complex content (for my very simple example this is short, but if your dialog has a lot more to it you will want to experiment with different values). We only want to run this animation after the dialog is ready so we can add a short delay of say 300ms (defined above as DIALOG_BUILD_TIME) to allow the layout time prior to setting the animation trigger to true.

Now we have a nice entry animation:

A nice hello!

This is great for most cases, especially if on dismiss of the dialog the user will transition to a new screen. But if they are not transitioning then the disappearing could be a bit abrupt. So, the next stage is to use the exit animation on the dismiss of the dialog.

Scale out on dismiss

What we need to do here is create a function which will set our trigger state to false, allow the the animation to run (reusing the animation time constant) and then dismiss the dialog (even though when the animation is complete the user can’t see the dialog, it is technically still open and our nice modal background is still visible. This function will be run in a coroutine so it needs to be a suspend function.

This we can then call in the dismiss request callback launching our coroutine to start the animation, passing in our trigger state object and the dialog dismiss function argument.

This works fine if we are only dismissing the dialog via the back button where the onDismissRequest callback gets called, but what about from a button on the dialog?

Dismissing via a button

The dialog content itself does not know of the coroutine or how to trigger it from a button and we want to keep this logic out of the content (for better abstraction and reusability). So that the dialog knows where to do the dismiss (the coroutine scope) we get the dialog content to extend a helper class so that the dialog content can invoke the dismiss method without needing the logic.

This helper class needs to take in the coroutine scope from the dialog so that it respects the lifecycle and it also needs a trigger mechanism, this we can do via a SharedFlow which we collect in the coroutine. When a value is emitted to the SharedFlow it will be collected by the dialog coroutine immediately and will start the animation, passing in our trigger state object and the dialog dismiss function argument.

In this diagram you can see that in order to pass the dismiss logic to the coroutine we need to emit a value to a shared flow in a helper method

Here is the helper function AnimatedTransitionDialogHelper and how it fits into our reusable dialog

The dismiss trigger method, triggerAnimiatedDismiss, can now be invoked by the dialog when we want to close the dialog:

And here is the final result!

A nice hello AND goodbye!

These animations can be fun to play around with and you can easily swap them out or reuse them by just altering the parent composable (AnimatedTransitionDialog).

I took a lot of inspiration from Yanneck Reiß’s post where there is a great example of a iOS bottom sheet style dialog.

Full code for the examples described here can be found on my github here.

For more Jetpack Compose animation fun, take a look at my card flipping animation post.

If you would like to see the above in talk format, check out my talk at GDG Melbourne.

--

--