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:
For our 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 Dot
s 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 Dot
s 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
remember
ed variable that holds the animation value that we will use to animate the dots. - We use a
LaunchedEffect
to trigger the start of the animation. Because thekey
isUnit
, 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
tweeen
that will linearly interpolate between the 2 values, and because we want the animation to revert and start over again, we specify arepeatMode
ofRepeatMode.Reverse
. - 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
remember
ed 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
animation
we described earlier, we create a list of those, one for each dot we want to render. This is where theindex
that we use to offset each animation comes from. - In our
Row
, we iterate over theanimation
s we just built and for each we add aDot
. - Each of these
Dot
s is offset vertically, by the amount specified by ourremember
ed 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
animating
boolean that tells us if the dots are animating. - Our animated value is
remember
ed on theanimating
value, so that if we toggle theanimating
flag, we reset the dots to their resting position. - The
LaaunchedEffect
is also using theanimating
flag as the key, so that when we toggleanimating
the previousLaunchedEffect
will be cancelled (the animation will stop) and a newLaunchedEffect
will trigger. Note that, if we are not animating, then we do not callanimate
and ouranimatedValue
will be simply0f
all 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
LoadingButton
composable mimicking theButton
from Jetpack Compose. I used some of the attributes ofButton
and left others out, but we coud have copied all the attributes if we may expect to need them. Besides theButton
attributes, 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
Button
from Jetpack Compose as our root element. - Inside our
Button
we have aBox
— as described, this will ensure theButton
is 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
loading
flag as theanimating
toggle. - We add a
graphicsLayer
modifier to animate the alpha of the loading indicator. - We wrap the idle content in a
Box
… - 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:
Additional animations
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 .2f
and 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
enum
to indicate what kind of animation we want. - The
remember
ed animated value is keyd off the animation type, so that if we change it, the value resets back to 0. - Likewise, the
LaunchedEffect
also 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
offset
oralpha
for the dots, using thethen
property ofModifier
.
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!