Checkmate on Compose — Part II Lessons learned from a Jetpack Compose-based chess app A sneak peek at what this two-part article series is about Chesso is a Jetpack Compose-based chess app with animated visualisation layers on top of the board: https://github.com/zsoltk/chesso https://medium.com/media/8023dfe2ff9c87eae415b569c39a4e56/href In this two-part article we explore the principles on which it was built: Part I (previous article): fundamentals […]
Lessons learned from a Jetpack Compose-based chess app
In this two-part article we explore the principles on which it was built:
We left off at the end of the first article with having finished the core elements of the app. Now it’s time to start adding some fancy bits!
One of the visually appealing features of chess apps is when piece moves are animated — but in the beginning, it wasn’t obvious how to make it work. Rather than jumping straight to the end result, let me walk you through my thought process in tackling the challenge.
The main issue is that the piece being moved ends up in different positions in the hierarchy of composables, and
There is no shared element transition available in Jetpack Compose at the time of writing this article.
But then — I thought — maybe I don’t need it after all. Couldn’t I just animate each piece manually? It couldn’t be that complicated, could it?
The good news is that Compose has an offset modifier that could be used to… well, offset the position at which a composable is rendered. Halfway there!
Also on the plus side, Compose does not clip when drawing outside of the “default” bounds (as would be the case with old-school, XML-based views), so there’s no need to worry about that either.
One look at the official docs on animation, and you’ll see that it’s super simple to animate a single value. So I figured I could certainly animate a piece by animating the value passed to the offset modifier — if I only knew where to animate the value to. So how could I calculate that?
At this point, my initial approach on rendering the board was becoming a limitation. (If you recall, I used equal weights to render an 8×8 board filling up the screen space — consequently, I delegated the calculation to Compose and I couldn’t know which position any given square was rendered at.)
So I decided to take the whole thing apart and start from the other end:
But how do we get the measured size at step 1? BoxWithConstraints to the rescue, which exposes the maxWidth and maxHeight properties:
This offset-based approach also made flipping the board extremely easy: we can calculate the offsets relative to either the top left or the bottom right corner based on a boolean value — and that’s it!
Following our method of calculating offsets, we can obtain the offsets both for the previous and the current game states. (Bear in mind that there might not be a previous one in some cases, e.g. at the beginning of the game!)
We can then create the animation. Handling the case when there’s no previous state, we can immediately use the target value as the starting point, and we have a graceful fallback with no animation:
The LaunchedEffect starts the animation to the targetOffset value (and relaunches it every time targetOffset changes).
The animated offset value then gets passed as a modifier to the Piece composable.
A fun consequence of all this is that once we can animate a single piece, we can animate all the pieces between any two arbitrary states of the game:
However, flipping the board also changes the offsets of the pieces, which is then automatically animated — this means that we’ll see all the pieces flying across the board each time, which isn’t quite what I wanted:
To avoid this we can simply add a second LaunchedEffect to run every time the value of isFlipped changes, and immediately snap the animation to its end value:
Up until this point all the Composables I wrote were embedded into each other the usual way.
However, I wanted to introduce some flexibility here so that visualisations (and really, any kind of generic decoration) could be easily added via separate classes.
I came up with a few classes. First, I captured all the information that rendering would need from UI state on individual squares to game state regarding the whole board:
Next, I created interfaces that can emit UI using the above data structures:
For example, the responsibility of a single SquareDecoration can be to paint a square’s background; or add a label to it, or add some highlight (topics we covered in the first article) —but not more than one at a time to keep them nicely separated.
A single BoardDecoration can be to paint squares (decorated, using SquareDecorations), or to add pieces.
One decoration each would surely not be enough — we’re going to need many. So renderer interfaces are introduced, which are basically glorified lists of decorations:
The default implementations are for example:
And finally, the client code — the refactored Board composable, using the above in practice:
Benefits of using this pattern:
Now that we have a fully functional chess app with the rendering layers in place, we can add the visualisations that were the motivation for creating the app in the first place.
All visualisations will be based on a numerical dataset:
Let’s consider this interface:
We can create a new SquareDecoration using these interfaces.
It uses an ActiveDatasetVisualisation CompositionLocal to fetch the active visualisation which the user sets in the dialog:
We can then leverage this API quite easily. Consider these:
A simple visualisation for beginners that shows in which squares the knight is most useful.
This is the code:
And this is how it works:
This one’s based on a post I ran into on Reddit, and shows the percentage of checkmates that occurred on any given square, based on a million actual games from Lichess. Credit goes to /u/atlas_scrubbed!
The implementation follows the same logic as the Knight’s move count, so I will skip the code here.
These depend on the current state of the game and will animate along with moves.
Highlights those pieces which can’t make a move in the current game state.
How does this work?
This one was actually quite easy to implement based on the already implemented game mechanics (explained in the first article)! All we need to do is:
This one is almost identical in implementation to blocked pieces — except now we visualise those pieces which have legal moves instead of the ones that don’t. The more they have, the stronger the green colour we apply.
It’s a great way to demonstrate to beginner players which pieces aren’t really doing much on the board.
A useful visualisation when trying to corner the king. Brighter coloured squares show the king’s immediate moves, paler ones show possible moves from those squares.
Compose comes with an extremely powerful animation system that relieves the developer of an enormous amount of effort.
I’d like to highlight two of its important features in the context of this article:
To demonstrate all this, I slowed down the animations quite a bit:
Here you can see:
I accidentally introduced some unwanted performance hits by forgetting how these factors interact:
This is a problem because it fetches data points for every frame of the animation:
Instead, we need it to calculate only once per game move, and to be recalculated in two cases only:
We can express this easily. Remember comes in really handy with its vararg keys:
There are many different methods in Compose (remember, LaunchedEffect, etc.) that allow passing generic vararg keys to tell the framework when a certain block should be re-run / re-calculated — a brilliant mechanism in all its succinctness.
And by fixing the above, now — of course — there’s a significant improvement in UI performance too.
Thanks for reading these articles! I hope you’ve enjoyed them as much as I enjoyed creating this project. I found it had just the right level of challenge, and I certainly learned a lot in an entertaining way. Compose is definitely a lot of fun, a fun that we all needed in the world of Android — and this is just the beginning.
Found a bug?
Got an idea of how to do something better than I did?
Got a ground-breaking idea for a new visualisation layer?
Come over to https://github.com/zsoltk/chesso and open an issue!