Jetpack Compose, the much-awaited UI toolkit from Google, is finally production-ready. With the power and flexibility of the declarative UI paradigm and access to low-level animation APIs, it is about to take the Android developer experience to a whole new level.
If you’re new to Jetpack Compose and declarative UI, you might want to check out the introductory post I wrote last year.
In this post, we’ll explore Jetpack Compose by building our own UI component—a 5×7 LED Matrix Display, the idea for which struck me while listlessly watching the LED display on an elevator ride. Though old, LED matrix displays are very much in vogue—take scoreboard displays, stock tickers, countdown timers, for instance.
The LED matrix display that we are going to create will:
- Support any numeral (and only numerals)
- Have numbers scrolling up and down
- Have configurable shape and color
1. Getting Started
In Android Studio, create a new project by selecting the “Empty Compose Activity” template. You need the latest version of Android Studio (Arctic Fox) to work on Compose projects.
The project initializes with a simple
Greeting composable and a
Surface composable. To keep things simple, we won’t touch any of the theming/styles of the application; we will just remove the
Surface and place all our code there. Also, we will show everything on the same screen.
2. Building a Grid
I’m going to use 35
Box composables arranged in a grid of 5 columns and 7 rows (Illustration 1):
A “composable” is a function that will emit a UI. This is equivalent to
Viewin traditional XML-based system. Jetpack Compose comes with a predefined collection of composables like
Textetc. For example, a
Rowcomposable is equivalent to the
LinearLayoutwith horizontal orientation.
Note: There’s a composable named
LazyVerticalGrid to build grids like this, but that API is not stable yet. So I’m sticking with the
Column composables for now.
Here’s how it looks:
Now let’s extract this matrix UI into a separate
@Composable function to make it more manageable.
And let’s use the new composable inside the
3. Adding a Character: Number “0”
Now we need to show numbers using the grid of boxes that we just created.
We can use a 5×7 matrix to represent each character internally. I’m going to use a matrix of integers, with 0 for “off” and 1 for “on” states. A matrix of booleans is a possible option, but I’m going ahead with an integer matrix because I can use other numbers to represent more LED colors, brightness level, etc., later.
Here’s an integer matrix that represents the character “slashed zero”.
In Kotlin, there are several ways to represent this data. I’m going to use a list of lists for this now.
4. Display the Number “0”
Now let’s update our composable function to accept a parameter
number and traverse through the above matrix and update the color of each
Box. In our case, we currently have only one matrix,
number00, but we will add more later.
Build and run the code. Your application will look like this.
Perfect! It’s the time to add all numbers from 0 to 9.
5. Adding Remaining Numbers
Once all the 10 matrices are added, we will update our composable to choose the matrix from the given parameter
when statement looks polluted. Don’t worry, we can improve this code later.
6. Switching Numbers in the Display
Now that we have all the numbers in a matrix, we can proceed to create a UI to select and show a number. Let’s add one button each for all ten numeric digits. Upon clicking a button, we can show the corresponding number on the LED display.
Enter the “state”!
A “state” is a value that can change over time. The Compose runtime can magically react to state changes and update the UI without our intervention.
Let’s introduce a state
number with initial value 0 and update the value when each button is clicked.
In this code, a mutable state
number is added with the
remember function. Let me explain what happens here:
var numberis the variable that holds the state.
byis just a Kotlin keyword to delegate instance variables (it has nothing to do with Compose).
rememberis a Kotlin compiler plugin API that tells the Compose runtime to cache the given value across recompositions. When
Columnrecomposes the next time,
rememberwill return whatever value was cached before.
mutableStateOf(0)creates a mutable state variable, initializes it with 0, and returns it. The Compose runtime is intelligent enough to observe the changes to this variable and schedules a recomposition of every composable scope that reads this variable.
Let’s see the code in action.
7. Merging Matrices into a Single Matrix
To further clean up the code, we can merge the 10 different matrices into a single 5×70 matrix. I like to call it a “ribbon”.
Now let’s update the
LedMatrixDisplay composable to use the new matrix instead of 10 different matrices. Given a value for the
number, find the first row representing the LED state for that number in the new matrix and display 7 rows from that position, including the starting row.
For example, if the
number is 9, then calculate the starting row of the number, which results in 63. Now use the rows from 63 to 69 to update the boxes. Nothing major here!
Running this code shows the same output because nothing has changed at the UI-level.
8. Adding Animation
It’s time to bring animation effects to our spanking new LED display. We want a scrolling effect for changing numbers and for that, we’ll animate the value of
For example, for number=0, the
characterRow is 0, and for number = 1, the
characterRow is 7. To make the LEDs “slide” the number from 0 to 1, we only need to update the value of
characterRow from 0 to 7 in small steps.
Jetpack Compose comes with a collection of APIs to easily implement animations. The
animate*AsState() function animates a single value as a state. We can provide the target value, and the state will animate to the new value. Each state update will trigger recompositions of composables that read this state.
In our case, we want to animate the index of a matrix, so we will use the
animateIntAsState() function. Let’s add it.
We have specified a
tween animation for 500 milliseconds with
LinearEasing as the easing effect.
LedMatrixDisplay recomposes with a new value, the
characterRow is updated to the new value. In the next line, the state
characterRowAnimated receives this new value as the
targetValue and starts animating towards the new
targetValue. Since we mentioned 500 milliseconds as the duration, the value change will finish in 500 milliseconds. Since the Boxes read the value of
characterRowAnimated, the Boxes recompose every time the
characterRowAnimated is updated.
Here’s an illustration of this. The long vertical gray bar represents a recomposition.
Here’s how it looks in action.
The numbers stick together, so let’s add an empty line between the numbers.
Also, update the logic to access the character row inside
LedMatrixDisplay to consider the new empty line. Again, no rocket science here.
Now the composable is ready to take any number and animate it when updated. Now we can do some code cleanup, file organization, and move the new
LedMatrixDisplay composable to a new file with an updated color (amber—as seen on elevator displays).
That’s all! Now you have a composable that can display the given number and scroll up or down to the new number.
Example 1: Three-Digit Counter
We will add three LED Matrix displays and two buttons—one for starting/stopping the counter and another for randomizing the number.
First, we define two states:
number to represent the number to be displayed on the counter and
started to represent the present status of the counter.
Then we add three
LedMatrixDisplay composables in a
Row. Also, let’s add two buttons: one to update the value of
started to true or false and another to generate a random value between 0 and 999 and update the
number with it.
LedMatrixDisplay composables will recompose whenever the state that it depends on changes. In our case, whenever the value of
number changes, the three composables will recompose with the new value.
Next, we need to start counting when the value of
true. We can use a
while loop with a delay for this, but again we will have to manage the threads. Luckily, Compose embraces coroutines at its core, so we can make use of coroutines and use a “side effect” to launch the coroutine.
Side Effect APIs
Composable functions should always be free of side effects, that is, they should not alter the state of an application. Since recompositions are unpredictable, modifying states within a composable function causes unexpected behavior. To update states in a predictable manner, Compose provides a set of Effect APIs. These are actually composable functions, but they don’t emit any UI.
In our example, we can use a side effect named
LaunchedEffect to run a coroutine.
LaunchedEffect is a special composable that accepts a lambda but doesn’t emit a UI. It executes the lambda in a coroutine whenever it enters the composition and cancels the coroutine when it exits the composition. This code doesn’t restart and continues execution across recompositions. If any of the arguments change, the
LaunchedEffect will cancel the currently running coroutine and restart it. In our case, we use
LaunchedEffect(started) to ensure that the side effect is executed every time the value of
started is updated (from true to false and vice versa).
LaunchedEffect(started), we update the value of
number at regular intervals. If the value reaches 999, we reset it to 0. Every time
number is updated with a new value, the three
LedMatrixDisplay composables get recomposed with the new value, which eventually animates the display to the new value.
Styling the LED display
Now let’s make our shiny new
LedMatrixDisplay configurable by supplying different styles. We can create a set of new classes to hold the styling information.
Update the composable to accept the new style parameter. If no style is specified, a default style is used.
Apply the style when iterating through the boxes.
Let’s move the 3-digit counter to a separate composable that works independently and accepts a style parameter.
Adding a Theme Picker
Now let’s build a “theme picker” UI to play around with different styles.
The “theme picker” UI should show a default list of colors for the “on” and “off” states, and the shape of the LED to choose from. Select the color or shape that you want to apply. The new “theme picker” composable should accept three callbacks, one each for “on” color, “off” color, and LED shape.
Now let’s add this new “theme picker” composable to the screen.
Here comes the “state” again!
We define a state
ledStyle to keep the latest style value and initialize it with the default style. When the
ThemePicker composable calls the lambdas, the value of
ledStyle gets updated and the
LedCounterDisplay gets recomposed with the new value of
Example 2: Digital Clock
Let me show you how our new component can be used to build a digital clock.
First, we will add a data class to store the time data. We will use this as a “state” in the next step.
Now let’s build a composable to represent the clock display and add the functionality to update the time every second.
I have used a state to represent time and update it every second in a
LaunchedEffect. I hardcoded
true as the parameter to start the lambda and run it forever. Inside the while loop, the
time = Time.getLatest() gets called every second and all the
LedMatrixDisplay composables get recomposed with the latest value of
time. Also, I have gone for a custom style: black for “on” state, round shape for LEDs, and 5×5 dp dimensions.
Now let’s use the composable.
ClockDisplay composable is self-contained and will update the time on its own.
Let’s see it in action.
Here’s the screen with everything together in one place.
You can browse the latest source code of this tutorial on GitHub.
Try This Out!
Here are a few enhancements you can try out to improve your understanding of Jetpack Compose.
- Make the animation configurable.
- Add different character sets with different shapes for the numbers. Add a dependency like this:
- Add more characters like “%” and build a composable like
PercentageMatrixDisplay(percentage: Int)that will show the percentage value using the dot matrix display.
- Right now, it takes 500 milliseconds to animate from one number to another number. Update it to 500ms for each number, that is, switching from 1 to 9 should take 4500ms. Hint: Use a
- Try everything on a single
Canvasinstead of adding 5×7= 35 different
Boxes to the screen.