Part 1: Let’s start with the domain and the first visual component of our Wordle app
By now, you have probably heard of Wordle, an app that gained popularity in late 2021 and continues to attract thousands of users to this day.
In order to unravel how it works and learn about Jetpack Compose, the new Android library for creating user interfaces, we are going to create a replica of this well-known app.
We are going to start the design of our domain with the most basic part of it; we are going to model how we want to represent the letters. Since the initial state of our game will be a 6×5 board (and this board will be empty initially and filled little by little), we can represent these cells as a sealed class such as:
sealed class WordleLetter(open val letter: String) {
object EmptyWordleLetter : WordleLetter("")
data class FilledWordleLetter(override val letter: String) : WordleLetter(letter)
We can also add a validation to the FilledWordleLetter entity since, for convenience, we are representing the letter attribute as a String. We are going to look for it to have one and only one letter, for which we can add this check in the constructor and throw an exception in case it is not fulfilled.
if (letter.count() != 1) {
throw IllegalArgumentException("A WordleLetter can have one letter at most")
}
In addition, we also need to represent the state of each letter on our board. For this, we will use an enum class such as:
enum class LetterStatus {
EMPTY,
NOT_CHECKED,
NOT_INCLUDED,
INCLUDED,
MATCH
}
Later we will also add the colors in which we will paint each cell, corresponding to each of its possible states.
Now we have a basic representation of our letters and their possible states, we can start building the different entities that will represent each component of our board, starting once again with the letters.
For this, we can create an entity that represents a letter together with its state, such as:
data class BoardLetter(
val letter: WordleLetter,
val state: LetterStatus
)
Each one of the rows of the board will be formed by a List we can call BoardRow, and the complete board will be formed by a List. We will build these entities later, but for now, it is enough for us to know that this will be their representation. If we pay attention to this implementation we can see that actually, the board is an array of List>, but since we need to add functionality to each component of this array, I have preferred to divide it into concrete classes to make the implementation easier and clearer.
But let’s not get too far ahead of ourselves yet, for now, we had the representation of a letter with its state on the board, so let’s start adding functionality to this class.
The first thing we want to be able to do with our BoardLetter is to be able to write a letter, but how can we do it if all the members of our entity are immutable? Easy! For it, we have used a data class that provides us with the method .copy through which instead of mutating our entity, we will be creating a new instance of the same one but with the modifications that we have specified. In addition, just as we want to add letters, we will want to remove them, and we will do exactly the same as with the creation, using the .copy method that allows us to maintain the immutability of our entity.
fun setLetter(aLetter: String) =
copy(
letter = WordleLetter.FilledWordleLetter(aLetter),
state = LetterStatus.NOT_CHECKED)
fun deleteLetter() =
copy(
letter = WordleLetter.EmptyWordleLetter,
state = LetterStatus.EMPTY)
Finally, we will also add a convenience method to be able to create empty letters from which to start working. We will create this method inside a companion object to be able to invoke it without the need of having an instance of the class
fun empty() = BoardLetter(WordleLetter.EmptyWordleLetter, LetterStatus.EMPTY)
Great! We already have our entity that represents a letter in our game, as well as a first approximation of the functionality we will need throughout our development.
We cannot forget to write the tests for this class. I will not go into detail since they are trivial for this implementation, but they can be consulted here.
Now that we have the implementation of our domain ready, we can create the Jetpack Compose representation of it. For it, we are going to create a Composable called LetterBox which will receive as a parameter the letter that we want to paint.
@Composable
fun LetterBox(
letter: WordleLetter,
state: LetterStatus
)
We want this component to show the letter in question that the user has written, and we also want its background to be painted in a different color depending on the state of the letter. The simplest way to replicate this behavior would be adding directly the background to a Composable Text. However, to make it look a little more elegant we will use a Card such that our component will look like this:
@Composable
fun LetterBox(
letter: WordleLetter,
state: LetterStatus
){
Card(
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = calculateState(state)),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
modifier = Modifier.aspectRatio(1f)
) {
Text(
modifier = Modifier
.fillMaxSize()
.wrapContentHeight(),
text = letter.letter,
textAlign = TextAlign.Center
)
}
}
private fun mapToBackgroundColor(state: LetterStatus) =
when (state) {
EMPTY, NOT_CHECKED -> Color.White
NOT_INCLUDED -> Color.LightGray
INCLUDED -> Color.Yellow
MATCH -> Color.Green
}
We will take advantage of this component to map the different states of each box to a different color, following the rules of the game.
Once we have created this component we can visualize it thanks to the @Preview of Compose.
@Preview
@Composable
fun Preview() {
LetterBox(
letter = WordleLetter.FilledWordleLetter("A"),
state = INCLUDED
)
}
So much for this first installment on creating something similar to the Wordle app with Jetpack Compose. In the following articles in the Apiumhub blog, we will create each of the rows of our board that will be composed of the components we created here, and we will finally create the complete game board, along with a dictionary to load the words we will use and all the logic related to the game.
The complete code for the entire application can be found at this link.
Until next time!