Leave a rating/review
A key part of programming in Jetpack Compose is state. Rather than start with the computer science definition of state, let’s go with something that might be a little more familiar: the dashboard of a car.
The most noticeable parts of a car dashboard are probably its gauges and odometers. They show the car’s current speed, fuel level, distance traveled, and so on, each of which is some kind of numeric quantity.
Dashboards also have warning lights, such as the low oil warning light, or the “it’s time to take the car to the shop for some maintenance” light.
Each of these lights is either on, indicating that there’s a problem that needs the driver’s attention, or off. This “on/off”, “true/false” information can be described as a boolean value.
So the information on a car’s dashboard — such as speed, fuel level, whether or not the car needs maintenance — taken all together, is a visualization of the car’s state.
Keep in mind that the dashboard isn’t the car’s actual state - it’s just a visualization of it.
To see what I mean, think about what happens when the driver changes the car’s state.
For example, the driver presses the accelerator, and the car starts moving faster. The dashboard then updates to show the new speed. So the car’s state is how fast the car is actually moving - and the dashboard is just helping the driver visualize that fact.
Internal circumstances can also change the car’s state. For example, as you drive, the car burns gas. The car’s state is how much gas is in the gas tank, and the dashboard hopefully updates the fuel indicator.
But what happens if the car’s state and dashboard aren’t in sync? For example, what if your dashboard breaks, and doesn’t accurately show your car’s speed? Well that could be a big problem - you might get a ticket - or worse!
It turns out that this type of mistake is quite common while developing an app. That is, your user interface might not accurately represent the internal state of your app. You may have sometimes come across an app with a bug like this - for example, an app says you have 5 new messages, but when you check, you actually have a different amount.
One of the nice things about Jetpack Compose, is that you’re forced to develop your apps in such a way that your user interfaces and your state are always consistent, which prevents these frustrating type of bugs.
At this point the app state we’re trying to add to Bullseye is very simple: either the popup alert is visible, or it’s not.
This is a boolean value, which means it’s either true, or false.
And the reason why the UI doesnt update to show the alert text is simply because alertIsVisible is not a state variable.
State variables are special variables that tells Jetpack Compose when to Recompose the UI. This makes sense because our app can have many variables that changes, maybe based on some computation. We dont want our app to always recompose whenever a variable changes. This would be wasting system resources…plus not every variable change might require a UI element to change.
In our case, alertIsVisible is a perfect scenario to use a state variable or more correctly a state object.
To do this, Composables uses the remember API which stores an object in memory.
This value is available during initial composition and also returned during recomposition.
You can declare a state object using any of the following three(3) ways. These declarations are the same behind the scene but just gives us the choice of choosing the syntax that works best for us. We’ll be using the second one which uses a language feature in Kotlin called “Delegated Properties” and this makes us write lesser code.
Let’s get back to Android Studio where I’ll break this down and you’ll see this in action.
Update the alertIsVisible variable to hold a state object like so:
var alertIsVisible by remember { mutableStateOf(false) }
And remember to add the imports when Android Studio prompts you to do so.
Like I mentioned earlier, composable functions uses the remember API to store the object we want to track in memory.
So alertIsVisible is “provided by” the remember API.
And what does it store? Well, it stores a mutable state which has a default boolean value of false.
It is storing a mutable state because its value can be changed.
Alright, run your app once again. And tap the hit me button.
Cool!!! Everything works fine and you can see the alert text is displayed below. This is possible because the state change initiated by the button tap informed Jetpack Compose to Recompose our UI with the updated state. In this case, a state that makes the alert visible.
Now we have our logic working, its time to swap the alert text with an actual AlertDialog.
You’ll be creating the alert dialog in a new composable file. This approach is common because this dialog can be reused in some other part of our app. In other words, you’ll be creating a reusable dialog widget.
Alright, lets create a new Kotlin file.
Open up the project panel.
Right click on the reverse domain name of your project.
Go to “New” then select Kotlin Class/File.
Select “File” from the popup menu.
Enter ResultDialog as the name.
Then hit the Return key.
For this composable, I’ll paste in the base code as we’ve covered most of the concepts:
@Composable
fun ResultDialog(
modifier: Modifier = Modifier
) {
AlertDialog(
onDismissRequest = {},
confirmButton = {
TextButton(
onClick = {}
) {
Text(stringResource(id = R.string.result_dialog_button_text))
}
},
title = { Text(stringResource(id = R.string.result_dialog_title)) },
text = { Text(stringResource(id = R.string.result_dialog_message)) }
)
}
And make sure you you add all the imports by just hitting Option + Return wherever Android Studio prompts you to.
Firstly, we created a composable named ResultDialog and this is evident because of the @Composable annotation.
Next, we define a modifier parameter and set a default Modifier.
And this is done to prevent an error just in case we dont pass in a modifier argument when calling the Composable function.
Note: It is a recommended practice to always have in a modifier parameter when creating custom composables. This makes more room for customization when using it as you’ll see soon. We might not use it for this composable but I’ll just leave it there.
Inside this composable, we return an AlertDialog.
We pass in four(4) of its arguments: onDismissRequest, confirmButton, title and text.
-
onDismissRequest: is the lambda or anonymous function that’ll be executed whenever theAlertDialogis dismissed. -
confirmButton: is the composable that’ll be used to trigger a done or completed action. In this case, we’ll use it to close the result dialog and set any other action to show we’re done with the dialog. And this button is displayed at the bottom of theAlertDialog. We pass in a aTextButtonwhich is just a button without a background color. It has an emptyonClicklistener and it returns aTextstring resource value. Do note that I’ve added the string resources for the result dialog in thestrings.xmlfile of the starter project. And next is the -
title: which is the title or header of the alert dialog. This is also a text with a string resource value. And finally… -
text: which is the text that is displayed in the body of the alert dialog.
I know that was a lot. Make sure you type in the code and continue with me.
Now its time to use this composable.
Head back to the GameScreen.kt file.
Scroll down to where the if condition is used to display the alert text.
Then replace the alert text with a call to the ResultDialog composable function.
As you type it in, you’ll see Android Studio’s intellisense kick in to help you with the function you’re typing.
Select the ResultDialog from the context menu by double clicking on it or pressing the Return key when it is selected.
This helps you automatically import the function to your file.
ResultDialog()
Run your app. Then click on the hit me button.
And your app is recomposed with the updated state which displays the dialog with a title, message and a button.
So, when a Jetpack Compose app is created, you have the initial state of the app. This is the initial composition of the app UI.
The UI can only be updated whenever there is a state change.
And when the state changes, Jetpack Compose redraws the UI elements that may have changed with the updated states. This process is known as “Recomposition.”
So the flow goes like this: Initial Compostion -> State Change -> Recomposition. With this approach, the state should be the single source of truth for your app so that you can manage its data effectively.
But when you try to dismiss the dialog by touching the screen outside the dialog area, nothing happens. Let’s fix that now.
If you recall, the alertIsVisible state object holds the Boolean value for showing the dialog.
We need a way to affect the state from inside the ResultDialog composable.
Now if you think about this, you might be tempted to pass the original state object as argument of the ResultDialog composable
and update it directly in there.
You’ll learn about why this is not a good approach in the next episode, but for now, just know that goes against having a state as a single source of truth. Its just like you’re creating a new instance of the state.
Alright, we need to update the function definition for the ResultDialog composable.
Head over to the file and add in a new parameter like so:
@Composable
fun ResultDialog(
hideDialog: () -> Unit, // New Code
modifier: Modifier = Modifier
)
//...
The hideDialog parameter is going to be a lambda which is just an anonymous function.
This lambda will not accept any argument as the parenthesis is empty.
Also, it wont return any value as the return value is Unit which is the same thing as void if you’re coming from Java.
So wherever you’re calling the ResultDialog composable function, you’ll pass in a lambda that’ll trigger the state to hide the dialog.
Next, we need to call this parameter in our code.
This will be inside the onDismissRequest of the AlertDialog and the onClick lambda of the TextButton like so:
onDismissRequest = {
hideDialog() // New Code
},
//...
TextButton(
onClick = {
hideDialog() // New Code
}
)
//...
Our code has some odd spacing. To fix this, you’ll use Android Studio’s reformat feature. Head over to the “Code” item from the menu bar above, then select “Reformat Code.”
Finally, let’s pass the hideDialog lambda in the GameScreen composable.
Head back to the GameScreen file.
And right away you can see we have an error in the ResultDialog call and if you hover over it, it says that it expects a value for hideDialog.
Go ahead and add in the following code:
ResultDialog(
hideDialog = { alertIsVisible = false }
)
With this, the lambda block is triggered whenever the hideDialog function is called inside the ResultDialog composable.
And this lambda simply sets the alertIsVisible state object to false.
This time around, you wont be running the code in the emulator. You’ll use a cool feature of live preview which is “Interactive Mode.” Go ahead and Build and Refresh the design view by click on the refresh icon at the top of the design view.
If your preview appears to be bigger or smaller than the preview window, click the “Zoom to Fit Screen” button at the bottom right corner of the design view.
Then click the “Start Interactive Mode” at the top of the Preview. Click on the hit me button.
Then touch the screen outside the dialog area. And the dialog is dismissed.
Let’t try out the dismiss button.
Click on the hit me button once again.
Then tap the button inside the dialog.
This also dismisses the dialog and all these are possible because Jetpack recomposes the UI whenever the alertIsVisible state object changes.
When done, you can hit the stop interactive mode button.