Techbase Academy
Android · Lesson 17

Buttons and Click Behaviour: The Dice Roller App

10 phasesTwo-Dice Lucky SevenAndroid Studio
Phase 1 of 10
Lesson Introduction

So far your apps have only displayed things. A birthday card and a business card are lovely, but they never react when you touch them. Real apps respond: you tap a button and something changes. In this lesson you will build your first interactive app, a dice roller, and learn the single most important idea in Jetpack Compose: state.

This lesson matches three items from Unit 2 of Google's official Android Basics with Compose course: the Intro to Dice Roller app video, the Create an interactive Dice Roller app codelab, and the Practice: Click behavior exercise. We take the same journey with extra explanation and our own code.

By the end of this lesson you will be able to:

  • Add a Button to a screen and run code when it is tapped.
  • Explain why a normal Kotlin variable does not update the screen.
  • Use remember and mutableIntStateOf so Compose redraws automatically.
  • Use the by delegate with the getValue and setValue imports.
  • Pick a random number with (1..6).random().
  • Choose what to show with a when expression.
  • Show an image from res/drawable with painterResource, or fall back to text if you have no images yet.
  • Notice and fix the "my state vanished when I rotated the phone" bug with rememberSaveable.

A promise: if you only remember one thing from this lesson, remember this sentence. In Compose, the screen is a function of state. Change the state and Compose redraws the screen for you.

✏️ Your Task
Practise what you just learned about Lesson Introduction. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 2 of 10
Prerequisite Concepts

Make sure these ideas feel familiar. If one does not, a quick revisit will pay off.

  • Composable functions (lesson 15). A function marked @Composable describes a piece of UI. You call composables inside other composables, like Text inside Column.
  • Modifiers and layouts (lesson 16). Column stacks children vertically, Modifier.fillMaxSize() fills the available space, and Arrangement and Alignment position the children.
  • when expressions (lesson 9). A when chooses between branches and can return a value.
  • Ranges (lesson 9). 1..6 is every whole number from 1 up to and including 6.
  • Lambdas (lesson 12). A button's behaviour is a lambda: a small block of code you hand to the button, saying "run this when tapped".

If you are unsure about lambdas, remember this picture: a lambda is a sticky note you give to a button. The note says what to do. The button keeps it and reads it every time it is tapped.

✏️ Your Task
Practise what you just learned about Prerequisite Concepts. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 3 of 10
Conceptual Understanding: Events, State and Recomposition

Events

When a user touches the screen, Android creates an event. A tap on a button is a click event. In Compose you do not write "listener objects". You pass a lambda to the composable:

kotlin
Button(onClick = { /* code that runs on every tap */ }) {
 Text("Roll")
}

The parameter is called onClick, and its value is the code to run.

Why a plain variable is not enough

Imagine you try the obvious thing:

kotlin
@Composable
fun BrokenCounter() {
 var count = 0
 Column {
 Text("Count: $count")
 Button(onClick = { count++ }) { Text("Add") }
 }
}

You tap the button. The variable count really does go up, but the number on screen never changes. There are two separate problems:

  1. Compose does not know that count changed, so it has no reason to redraw.
  2. Even if it did redraw, the function would run again from the top, and var count = 0 would reset the variable to zero.

State fixes both problems

State is a value that Compose watches. When it changes, Compose re-runs (the official word is recomposes) the composables that read it, so the screen updates. To survive the re-run without resetting, the value must be remembered.

Ingredient What it does Analogy
mutableIntStateOf(1) Creates a watched whole-number value starting at 1. A scoreboard that tells the referee every time it changes.
remember { ... } Keeps the value alive across recompositions instead of recreating it. Writing the score on a whiteboard instead of in your head.
by delegate Lets you read and write the state like an ordinary variable. A cover that hides the scoreboard machinery.

Putting them together:

kotlin
var result by remember { mutableIntStateOf(1) }

Read it like a sentence: "result is a remembered, watched integer that starts at 1."

The cycle

text
   user taps button
          |
          v
   onClick lambda runs  ---> changes state (result = 4)
          ^                          |
          |                          v
   screen shows new UI  <---  Compose recomposes

Every interactive Compose screen follows this loop: event, state change, recomposition, new UI. You never say "set the text to 4". You only change the state, and the UI follows.

✏️ Your Task
Practise what you just learned about Conceptual Understanding: Events, State and Recomposition. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 4 of 10
Step-by-Step Build 1: The Static Screen

Create a new project exactly as in lesson 03: File > New > New Project > Empty Activity. Name it Dice Roller, keep the package com.example.diceroller, choose a minimum SDK your emulator supports, and finish. Wait for Gradle to sync.

Open MainActivity.kt. The template gives you a MainActivity class that calls setContent. Replace the file contents with the following. Your theme function is named after your app, so it will be DiceRollerTheme if you used the name above.

MainActivity.kt
package com.example.diceroller
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.diceroller.ui.theme.DiceRollerTheme
class MainActivity : ComponentActivity() {
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 enableEdgeToEdge()
 setContent {
 DiceRollerTheme {
 Surface(modifier = Modifier.fillMaxSize()) {
 DiceRollerApp()
 }
 }
 }
 }
}
@Composable
fun DiceRollerApp(modifier: Modifier = Modifier) {
 Column(
 modifier = modifier.fillMaxSize(),
 horizontalAlignment = Alignment.CenterHorizontally,
 verticalArrangement = Arrangement.Center
 ) {
 Text(text = "⚀", fontSize = 120.sp)
 Spacer(modifier = Modifier.height(24.dp))
 Button(onClick = { }) {
 Text(text = "Roll")
 }
 }
}
@Preview(showBackground = true)
@Composable
fun DiceRollerAppPreview() {
 DiceRollerTheme {
 DiceRollerApp()
 }
}

What each part does

  • Surface gives the app a background colour from the Material theme.
  • Column stacks the die and the button, centred both ways using horizontalAlignment and verticalArrangement.
  • The die is the Unicode character ⚀ (die face one), shown at 120.sp. It lets you finish the whole lesson without any image files. We replace it with real pictures later.
  • Spacer adds an empty 24 dp gap between the die and the button.
  • Button(onClick = { }) has an empty lambda for now, so tapping does nothing.

Run the app (lesson 04) or open the Preview pane. You should see a big die face above a Roll button.

Tip: sp (scale-independent pixels) is for text, so it respects the user's font-size setting. dp (density-independent pixels) is for everything else. You met both in lesson 15.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 1: The Static Screen. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 5 of 10
Step-by-Step Build 2: Make It Respond

Step 1: Add state

Inside DiceRollerApp, before the Column, create the state. Add these imports at the top of the file:

kotlin
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

The two imports getValue and setValue are what make the by keyword work. If you forget them, Android Studio underlines by in red with a message about a missing getValue operator. Press Alt + Enter and choose Import.

Step 2: Roll a random number

Kotlin makes randomness easy:

kotlin
val diceRoll = (1..6).random()

1..6 is a range, and .random() picks one number from it. Both ends are included, so you can get 1 or 6.

Step 3: Connect the button to the state

Here is the complete composable. Only DiceRollerApp changes, so the rest of the file stays as before.

kotlin
@Composable
fun DiceRollerApp(modifier: Modifier = Modifier) {
 var result by remember { mutableIntStateOf(1) }
 val faces = listOf("⚀", "⚁", "⚂", "⚃", "⚄", "⚅")
 Column(
 modifier = modifier.fillMaxSize(),
 horizontalAlignment = Alignment.CenterHorizontally,
 verticalArrangement = Arrangement.Center
 ) {
 Text(text = faces[result - 1], fontSize = 120.sp)
 Text(text = "You rolled a $result", fontSize = 20.sp)
 Spacer(modifier = Modifier.height(24.dp))
 Button(onClick = { result = (1..6).random() }) {
 Text(text = "Roll")
 }
 }
}

Walk through it slowly:

  1. var result by remember { mutableIntStateOf(1) } creates the remembered, watched number, starting at 1.
  2. faces is a list of six characters. Lists start counting at zero, so the face for a roll of n is faces[n - 1]. A roll of 1 uses index 0.
  3. The two Text composables read result. Because they read it, Compose knows they depend on it.
  4. The button's lambda writes result = (1..6).random(). That write is the state change.
  5. Compose notices, recomposes DiceRollerApp, the Texts show the new face and sentence.

Run it and tap Roll several times. The die changes and the sentence updates. You just built a reactive UI.

Expected behaviour: Starting screen shows ⚀ and "You rolled a 1". After a tap you might see ⚄ and "You rolled a 5". Results are random, so yours will differ.

Splitting into smaller composables

Big composables get hard to read. A common pattern is to separate the layout from the dice. Here the state still lives in one place, and a helper composable does the drawing:

kotlin
@Composable
fun DiceRollerApp(modifier: Modifier = Modifier) {
 DiceWithButtonAndFace(
 modifier = modifier
 .fillMaxSize()
 .wrapContentSize(Alignment.Center)
 )
}
@Composable
fun DiceWithButtonAndFace(modifier: Modifier = Modifier) {
 var result by remember { mutableIntStateOf(1) }
 val faces = listOf("⚀", "⚁", "⚂", "⚃", "⚄", "⚅")
 Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
 Text(text = faces[result - 1], fontSize = 120.sp)
 Spacer(modifier = Modifier.height(16.dp))
 Button(onClick = { result = (1..6).random() }) {
 Text(text = "Roll")
 }
 }
}

You need import androidx.compose.foundation.layout.wrapContentSize. The modifier wrapContentSize(Alignment.Center) centres the content inside the full-screen box.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 2: Make It Respond. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 6 of 10
Step-by-Step Build 3: Real Dice Images with painterResource

Unicode faces are great for learning, but a shipped app usually shows pictures. Here is how to swap in image resources. The official course provides dice image files, and you may also draw your own vector assets or export six images from any design tool.

  1. In Android Studio switch the Project tool window to the Android view.
  2. Copy six images named dice_1.png to dice_6.png (file names must be lowercase letters, digits and underscores).
  3. Paste them into app/src/main/res/drawable. The resource names are the file names without the extension.

Now choose the image with a when expression, which returns the value, so it can be assigned directly:

kotlin
val imageResource = when (result) {
 1 -> R.drawable.dice_1
 2 -> R.drawable.dice_2
 3 -> R.drawable.dice_3
 4 -> R.drawable.dice_4
 5 -> R.drawable.dice_5
 else -> R.drawable.dice_6
}
Image(
 painter = painterResource(imageResource),
 contentDescription = result.toString()
)

Add the imports androidx.compose.foundation.Image, androidx.compose.ui.res.painterResource and make sure R resolves (it is generated from your package, so no import is needed inside com.example.diceroller).

Two details matter:

  • when as an expression must cover every case, so the final else is required. Kotlin cannot know that result will only ever be 1 to 6.
  • contentDescription is read aloud by screen readers such as TalkBack. result.toString() announces the number. Lesson 47 explains accessibility in depth, but form the habit now.

Think about it: Why use when here instead of six if statements? Because when reads like a table, returns a value, and the compiler checks that the cases are complete.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 3: Real Dice Images with painterResource. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 7 of 10
Rotation, Process Death and rememberSaveable

Run the app, roll until you get a 6, then rotate the emulator (use the rotate buttons beside the emulator, or press Ctrl + Left on Windows or Linux). The die resets to 1!

Why? Rotating the phone is a configuration change. Android destroys the Activity and creates a new one, and everything stored with plain remember is lost. Lesson 23 explains the lifecycle in detail. The quick fix is rememberSaveable:

kotlin
import androidx.compose.runtime.saveable.rememberSaveable
var result by rememberSaveable { mutableIntStateOf(1) }

rememberSaveable stores the value in a bundle that Android keeps across configuration changes and even across process death. For simple types like Int, String, and Boolean nothing else is needed. Use rememberSaveable for state that the user would be annoyed to lose, and plain remember for temporary visual state.

Function Survives recomposition Survives rotation Survives process death
plain variable No No No
remember Yes No No
rememberSaveable Yes Yes Yes
✏️ Your Task
Practise what you just learned about Rotation, Process Death and rememberSaveable. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 8 of 10
Guided Practice Exercises
🎯 Your Challenge

Exercise 1: The Click Counter

Objective: Practise remember, mutableIntStateOf and Button with a different shape of state.

Steps:

  1. Create a composable ClickCounter that shows Taps: 0.
  2. Add a Tap button that adds 1 each time.
  3. Add a Reset button that sets the counter back to 0.
  4. Make the text red once the count reaches 10 (hint: color = Color.Red and an if expression).
  5. Keep the count across rotation.
kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun ClickCounter() {
 var taps by rememberSaveable { mutableIntStateOf(0) }
 Column(horizontalAlignment = Alignment.CenterHorizontally) {
 Text(
 text = "Taps: $taps",
 fontSize = 32.sp,
 color = if (taps >= 10) Color.Red else MaterialTheme.colorScheme.onBackground
 )
 Spacer(modifier = Modifier.height(16.dp))
 Row {
 Button(onClick = { taps++ }) { Text("Tap") }
 Spacer(modifier = Modifier.width(12.dp))
 Button(onClick = { taps = 0 }) { Text("Reset") }
 }
 }
}

You also need import androidx.compose.ui.Modifier. taps++ works on a delegated property because it expands to a read followed by a write. The colour uses an if expression, so the whole thing fits inside the parameter. Using MaterialTheme.colorScheme.onBackground keeps the text readable in both light and dark theme.

Self-check: What happens to the count on rotation if you replace rememberSaveable with remember? (It resets to zero.)

🎯 Your Challenge

Exercise 2: The Lemonade Tapper (the Practice: Click behavior exercise)

Objective: Use a step state and a when inside onClick to build a four-stage tapping game.

Rules:

  1. Step 1: a lemon tree. Tapping it picks a lemon and chooses a random squeeze count between 2 and 4.
  2. Step 2: a lemon. Each tap is one squeeze. After the required squeezes, move on.
  3. Step 3: a glass of lemonade. Tapping it drinks it.
  4. Step 4: an empty glass. Tapping it starts over at step 1.
  5. Show an instruction sentence under the picture that changes with the step.
kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun LemonadeApp(modifier: Modifier = Modifier) {
 var step by rememberSaveable { mutableIntStateOf(1) }
 var squeezesNeeded by rememberSaveable { mutableIntStateOf(0) }
 var squeezesDone by rememberSaveable { mutableIntStateOf(0) }
 val (emoji, instruction) = when (step) {
 1 -> "🌳" to "Tap the lemon tree to pick a lemon"
 2 -> "🍋" to "Keep tapping the lemon to squeeze it"
 3 -> "🥤" to "Tap the lemonade to drink it"
 else -> "🥛" to "Tap the empty glass to start again"
 }
 Column(
 modifier = modifier.fillMaxSize(),
 horizontalAlignment = Alignment.CenterHorizontally,
 verticalArrangement = Arrangement.Center
 ) {
 Button(
 onClick = {
 when (step) {
 1 -> {
 squeezesNeeded = (2..4).random()
 squeezesDone = 0
 step = 2
 }
 2 -> {
 squeezesDone++
 if (squeezesDone >= squeezesNeeded) step = 3
 }
 3 -> step = 4
 else -> step = 1
 }
 }
 ) {
 Text(text = emoji, fontSize = 96.sp)
 }
 Spacer(modifier = Modifier.height(24.dp))
 Text(text = instruction, fontSize = 18.sp)
 }
}

How it works: step decides what the screen shows and what a tap means. The when that picks the emoji returns a pair that is destructured into two values. The second when runs inside the click lambda and changes state. Because squeeze counts are random, two playthroughs differ, which is the point of the exercise.

✏️ Your Task

Change the dice app so the Roll button is disabled for one second after each tap (hint: look at the enabled parameter of Button, and learn rememberCoroutineScope in lesson 28 to add a delay). If you are not ready for coroutines, simply disable the button once the player rolls a 6.

Phase 9 of 10
Common Beginner Mistakes (and How to Fix Them)

Mistake #1: A plain var instead of state. The wrong code: var result = 1. What happens: the variable changes but the screen never does. The fix: var result by remember { mutableIntStateOf(1) }.

Mistake #2: remember without the state wrapper. The wrong code: var result by remember { 1 }. What happens: the code does not compile, because by needs a state object. The fix: remember { mutableIntStateOf(1) }.

Mistake #3: Forgetting the getValue and setValue imports. Symptom: a red underline on by. The fix: import androidx.compose.runtime.getValue and androidx.compose.runtime.setValue, or press Alt + Enter.

Mistake #4: Off-by-one in a list. The wrong code: faces[result]. What happens: a roll of 6 crashes with IndexOutOfBoundsException, because the list has indexes 0 to 5. The fix: faces[result - 1]. Reading the red stack trace in Logcat (lesson 18) points straight to the line.

Mistake #5: Creating state outside a composable. The wrong code: putting remember in a top-level property or in a normal function. The fix: remember may only be called from inside a composable.

Mistake #6: Doing heavy work in a composable body. The wrong code: calling (1..6).random() directly in the body of DiceRollerApp so a new number is chosen on every recomposition. What happens: the die changes by itself. The fix: generate the random number only inside onClick.

Mistake #7: Forgetting else in a when expression. What happens: a compile error saying the when must be exhaustive. The fix: add an else branch.

Mistake #8: Losing state on rotation. The fix: use rememberSaveable for anything the user expects to survive.

✏️ Your Task
Practise what you just learned about Common Beginner Mistakes (and How to Fix Them). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 10 of 10
Knowledge Check

Answer each question before revealing the answer.

Question 1. What is state in Compose?

State is a value that Compose watches. When it changes, Compose recomposes the composables that read it so the screen shows the new value.

Question 2. Why does var count = 0 inside a composable not make a counter work?

Compose is not told when a plain variable changes, so it does not redraw. And when the composable re-runs, the line var count = 0 runs again and resets it.

Question 3. What do remember and mutableIntStateOf each contribute?

mutableIntStateOf creates a state object Compose can observe. remember keeps that same object alive across recompositions instead of creating a new one each time.

Question 4. Which function would you use so a value survives rotating the phone?

rememberSaveable. Plain remember is lost when the Activity is recreated on a configuration change.

Question 5. What does (1..6).random() return, and can it return 6?

A random integer from 1 to 6. Ranges built with .. include both ends, so 6 is possible.

Go deeper: official resources

✏️ Your Task
Practise what you just learned about Knowledge Check. Open Android Studio, type the examples by hand, change a value, and observe what happens.
🏗️ Build It — Mini Project
Two-Dice Lucky Seven

Project goal: Turn the dice roller into a tiny game with two dice. Rolling a total of seven wins, rolling a double earns a bonus message, and any other result invites another try.

Requirements:

  1. Show two dice faces side by side in a Row.
  2. A Roll both button rolls both dice at once.
  3. Show the total under the dice.
  4. Show a message chosen with a when that has no argument (conditions in each branch): Tap Roll to begin before the first roll, Lucky seven! You win! when the total is 7, Double N! Roll again when both dice match, and Total N. Try again otherwise.
  5. Count how many rolls the player has made and show it.
  6. Keep every piece of state across rotation.

Stretch goal: Add a Best streak counter that remembers the longest run of non-winning rolls before a win.

MainActivity.kt
package com.example.diceroller
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.diceroller.ui.theme.DiceRollerTheme
private val faces = listOf("⚀", "⚁", "⚂", "⚃", "⚄", "⚅")
class MainActivity : ComponentActivity() {
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 enableEdgeToEdge()
 setContent {
 DiceRollerTheme {
 Surface(modifier = Modifier.fillMaxSize()) {
 LuckySevenApp()
 }
 }
 }
 }
}
@Composable
fun LuckySevenApp(modifier: Modifier = Modifier) {
 var first by rememberSaveable { mutableIntStateOf(1) }
 var second by rememberSaveable { mutableIntStateOf(1) }
 var rolls by rememberSaveable { mutableIntStateOf(0) }
 val total = first + second
 val message = when {
 rolls == 0 -> "Tap Roll to begin"
 total == 7 -> "Lucky seven! You win!"
 first == second -> "Double ${first}s! Roll again"
 else -> "Total $total. Try again"
 }
 Column(
 modifier = modifier.fillMaxSize(),
 horizontalAlignment = Alignment.CenterHorizontally,
 verticalArrangement = Arrangement.Center
 ) {
 Row {
 Text(text = faces[first - 1], fontSize = 96.sp)
 Spacer(modifier = Modifier.width(16.dp))
 Text(text = faces[second - 1], fontSize = 96.sp)
 }
 Spacer(modifier = Modifier.height(12.dp))
 Text(text = message, fontSize = 22.sp)
 Text(text = "Rolls so far: $rolls", fontSize = 16.sp)
 Spacer(modifier = Modifier.height(24.dp))
 Button(onClick = {
 first = (1..6).random()
 second = (1..6).random()
 rolls++
 }) {
 Text(text = "Roll both")
 }
 }
}
@Preview(showBackground = true)
@Composable
fun LuckySevenPreview() {
 DiceRollerTheme {
 LuckySevenApp()
 }
}

Expected screen after the first tap (example):

⚃ ⚂
Lucky seven! You win!
Rolls so far: 1

Why it works: three separate state values change inside one onClick, and Compose recomposes once with all of them. The when without an argument tests conditions from top to bottom, so the order matters: a roll of double three is not seven, but a roll of 3 and 4 is, and the seven check comes first so a win is never reported as a double.

1

Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.

2

Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 17 project”.

3

On github.com click New repository, name it android-lesson-17-buttons-and-click-behaviour-the-dice-rolle, set it to Public, leave Add a README unticked, and click Create repository.

4

Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.

5

Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉

Lesson 17 complete! 🎉

You covered:

  • Lesson Introduction
  • Prerequisite Concepts
  • Conceptual Understanding: Events, State and Recomposition
  • Step-by-Step Build 1: The Static Screen
  • Step-by-Step Build 2: Make It Respond
  • Step-by-Step Build 3: Real Dice Images with painterResource
  • Rotation, Process Death and rememberSaveable
  • Guided Practice Exercises
  • Common Beginner Mistakes (and How to Fix Them)
  • Knowledge Check
All 72 lessons in Google Android Studio ·Techbase Academy ·© 2025–2026 Techbase Consultant Services, Ibadan
Esc
↑ ↓ to moveEnter to openEsc to close