Creating a Loading Button in Jetpack Compose
In this tutorial we will implement a loading button, i.e., a button that replaces its content with a loading indicator to signify to the user that some operation is under way, and when that operation has completed, reverts to its original state. This GIF shows what we want to achieve:
Let’s see how we can achieve this, step by step.
Creating the loading indicator dots
In Jetpack Compose, as the name indicates, elements are composed of smaller units, we build our composables by creating the basic pieces that we later assemble to get the desired effect. For our animated button, the most basic element we need is the animating dot, so let’s start there.
To render a solid circle on Jetpack Compose we can use a
Box and then apply the
clip modifier with a circular shape, we don’t need anything fancier than this, so let’s see how we can render this dot:
LoadingDot we just need a couple of parametes, the color we want to use to fill the dot with, and a modifier that we will use to customize this composable.
Creating the bouncing loading composable
The row of dots
Next we are going to create the composable that will render the 3 dots and animate them. As we want to display these dots in a row, our composable will therefore use a
Row as its root element. We will allow for a bit of customization, so besides the color that the dots already accept, we will also let users of this composable specify the spacing between the dots. We could take this a step further and also allow customizing the dot size, but for this exeercise we’ll limit it to the spacing.
With these requirements, we can display our composable as shown below:
There isn’t much to it, we just have a
Row and we place the
Dots in it, in our case we define
NumIndicators as 3. The only thing worth of note is that we use the
aspectRatio modifier to ensure our
Dots are a circle.
As mentioned, our composable takes a color for the dots, and the spacing between them. As is customary, we also provide a
Modifer so that this composable can be easily customized. Per Jetpack Compose guidelines, the
Modifier is the first optional argument.
Animating the dots
Next we want to animate the dots. As shown in the animation at the beginning of this article, we want to stagger the dots so that they are offset from one another. In Jetpack Compose animations are built on top of Kotlin coroutines, so we can leverage that and use a
delay to start the animation for the 2nd and 3rd dot, so that they are offset relative to the first dot.
To animate each invidual dot we willl use the
animate function from Jetpack Compose. This function takes an initial value, a target value, an optional animation spec to control how the value animates from start to target and a lambda that gets called as the animation value is updated. In our case, we want to animate the dots up and down, and we want the overall translation to be equal to the height of the dots, in other words, we want to move the dots up by half their height, and then down below their rest position by half their height again. If we place the dots centered in their container initially, and we want to start the movement from bottom to top, this means our initial value will be the dot height divided by 2, and the target value will be minus the dot height divided by 2. Maybe seeing the code will make this clearer, so this is how our animation will be defined:
Let’s analyze this:
- As we want our dots to be staggered, we define an animation delay that determines how much delayed a dot animation is relative to the previous dot animation. Because we want the dots to animation in a harmonious way, we ensure that the delays between dots are all the same; we do that by dividing the animation duration by the number of dots.
- We define a
remembered variable that holds the animation value that we will use to animate the dots.
- We use a
LaunchedEffectto trigger the start of the animation. Because the
Unit, this will start when the composable enters the composition and won’t be cancelled until the composable leaves the composition.
- Next we define the animation, with the start value being half the dot height (so that it initially starts at its downmost position), and the target as minus half the dot height, so that the end position is the topmost one. For our animation we use a
tweeenthat will linearly interpolate between the 2 values, and because we want the animation to revert and start over again, we specify a
- We delay each dot by the delay we calculated above, and we multiply it by the dot index (we will see where this comes from in a minute).
- Finally, we specify the animation callback that will simply udpate our
remembered variable with the animation value.
Now that we have the animation defined, we can build on this and render the dots. Our animated dots composable will therefore be:
Let’s have a more detailed look at what we are doing here:
- Using the
animationwe described earlier, we create a list of those, one for each dot we want to render. This is where the
indexthat we use to offset each animation comes from.
- In our
Row, we iterate over the
animations we just built and for each we add a
- Each of these
Dots is offset vertically, by the amount specified by our
remembered animation value.
If we run this animation, we get this result:
That’s starting to look pretty good, let’s move to the next step.
Creating the animating button
So our goal is to have a
Button that displays its standard content when it’s idle, and the loading dots when it’’s busy (from now on I’ll refer to
busy content to the loading dots, and
idle content to the default button content). We don’t know how large the idle content is, and we don’t want our button to change sizes when we toggle between the idle and the busy contents, so we have to find a way to ensure the button is as wide and as tall as the widest and tallest of the 2 contents.
There are a few different ways to achieve this, but because I also want to crossfade the idle and busy contents, the solution I have chosen for this is to render both contents within a
Box, which will ensure it’s large enough to fit both, and then crossfade the alphas between idle and busy content.
The problem with this approach is that our animating dots are always animating, so if we add the dots to a
Box and set their alpha to 0, they won’t be visible, but the animation will still be running and wasting CPU resources for no good reason, so we will need to update our animating dots composable to stop animating when it’s not visible.
Our animating dots run the animations in a
LaunchedEffect, which we have defined as taking a
Unit for key, which means this launches when the composable enters the composition and runs for as long as the composable remains in the composition. To be able to stop the animation we will need to update our
LaunchedEffect, so that we can leverage the
key to stop the animation. The simplest solution is to provide a
boolean to our composable that indicates if we are animating, and then restart the
LaunchedEffect on that
boolean. The code is probably going to be easier to understand than this description, so let’s see how we can update our animating dots composable to offer a start/stop control:
We only need to make 3 small changes to our composable:
- We pass an
animatingboolean that tells us if the dots are animating.
- Our animated value is
remembered on the
animatingvalue, so that if we toggle the
animatingflag, we reset the dots to their resting position.
LaaunchedEffectis also using the
animatingflag as the key, so that when we toggle
LaunchedEffectwill be cancelled (the animation will stop) and a new
LaunchedEffectwill trigger. Note that, if we are not animating, then we do not call
animatedValuewill be simply
0fall the time, so the dots will be at rest.
Now that we have our animating dots optimized to only animate when the
animating flag indicates so, we can work on our Loading Button. As we said earlier, we will use a
Box and render the 2 contents, idle and busy, and crossfade them. We don’t want to use the
Crossfade animation because that won’t keep the size of the largest content, so we will manually crossfade the 2 contents. To animate the alpha of the idle and busy contents we will use
animateFloatAsState. Let’s see how we do that:
- We define our
LoadingButtoncomposable mimicking the
Buttonfrom Jetpack Compose. I used some of the attributes of
Buttonand left others out, but we coud have copied all the attributes if we may expect to need them. Besides the
Buttonattributes, we have the loading flag and the indicator spacing.
- Next we define the 2 alpha animations for the idle and busy contents, they run opposite one another (one runs from 1f to 0f, the other from 0f to 1f).
- We are leveraging the
Buttonfrom Jetpack Compose as our root element.
- Inside our
Buttonwe have a
Box— as described, this will ensure the
Buttonis as wide and tall as needed and doesn’t change size when we toggle between idle and busy content.
- We specify a center alignment for the button content, so that both the idle and busy contents are nicely centered.
- Next we display the loading indicator we built earlier, passing the
loadingflag as the
- We add a
graphicsLayermodifier to animate the alpha of the loading indicator.
- We wrap the idle content in a
- Because we want to control its alpha based on whether we are loading or not.
- Finally, we render the content.
And with this our composable is complete! We can add this to a screen with these few lines of code:
Now that we have our bouncing dots button, we can build on top of it and add other animations. Another one that comes to mind is a fade animation, where the dots fade in and out, also in a staggered way.
To specify which kind of animation we want for our button, we will define an
enum class that we will add as an extra parameter to our
LoadingButton composable, as shown below:
Next we need to pass this to our
LoadingIndicator composable, so that we can choose which animation to run. For the
fade animation we want to change our animation target values to be
1f for the alpha, and we will make our animation a bit longer as well.
One option would be to add these attributes as part of our
enum above, but the
enum is going to be part of our public API, and these attributes are internal to the composable, so we should not be exposing them. Instead, we will define a set of extension properties on the
enum that will provide the start and target animation values, as well as the animation duration and the delay:
Now that we have this, we can update our
LoadingIndicator composable to either bounce or fade the dots:
There are only a few changes we need to do for this:
- We pass our
enumto indicate what kind of animation we want.
remembered animated value is keyd off the animation type, so that if we change it, the value resets back to 0.
- Likewise, the
LaunchedEffectalso keys off the animation type, so that we cancel and restart the animation if the value changes.
- We use the extension properties we just defined to get the animation attributes.
- Finally, based on the type of animation we want, we either use
alphafor the dots, using the
And here’s our final result:
The composable we have defined could be further enhanced with additional animations; the steps to do so are the same as those to add the
fade animation, simply add a new entry to the
enum, update the extension properties and finally animate the new property in
LoadingIndicator. Other improvements we could add is preventing the button from triggering when we are in the busy state; adding these additional animations and enhancing the button behaviour is left as an exercise to the reader.
The full code is available here. If you made it all the way here, hope you found this useful, and I’ll see you on the next one!