Jetpack Compose with Copilot: Create a button to increment a counter
Video: Jetpack Compose with Copilot: Create a button to increment a counter by Taught by Celeste AI - AI Coding Coach
Jetpack Compose: Counter Button
var count by remember { mutableStateOf(0) }.Button(onClick = { count++ }) { Text("Count: $count") }. The "Hello, World!" of Compose state management.
A small puzzle that combines episode 16 (state) and episode 14 (counts) — a button that increments a counter and displays the current value.
The Copilot prompt
// A counter button that increments a number on each click
@Composable
fun CounterButton() {
Copilot generates:
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun CounterButton() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Count: $count")
}
}
Walkthrough
1. State. var count by remember { mutableStateOf(0) } — reactive integer, starts at 0.
2. Increment in onClick. count++ increments the state. Compose detects the change and recomposes anything reading count.
3. Display in the label. "Count: $count" interpolates the current value. Each click → recompose → new label.
That's the entire pattern. Less than 10 lines of code, and it's a complete reactive counter.
mutableIntStateOf for primitives
For Int specifically, Kotlin Compose offers mutableIntStateOf:
import androidx.compose.runtime.mutableIntStateOf
var count by remember { mutableIntStateOf(0) }
mutableIntStateOf avoids boxing the int into a Java Integer — a tiny performance gain. Same applies for mutableFloatStateOf, mutableLongStateOf, etc.
For most code, mutableStateOf<Int>(0) is fine. The specialised versions matter only in hot paths.
Adding a reset button
@Composable
fun CounterApp() {
var count by remember { mutableStateOf(0) }
Column {
Text("Count: $count", style = MaterialTheme.typography.headlineLarge)
Row {
Button(onClick = { count++ }) { Text("+1") }
Button(onClick = { count-- }) { Text("-1") }
Button(onClick = { count = 0 }) { Text("Reset") }
}
}
}
Three buttons sharing one state. Increment, decrement, reset. The state is owned by CounterApp; the buttons are dumb renderers.
Hoisting
For a reusable counter widget, hoist state up:
@Composable
fun CounterButton(count: Int, onIncrement: () -> Unit) {
Button(onClick = onIncrement) {
Text("Count: $count")
}
}
@Composable
fun CounterApp() {
var count by remember { mutableStateOf(0) }
CounterButton(count = count, onIncrement = { count++ })
}
CounterButton is now stateless — testable, previewable, reusable. CounterApp owns the state.
With a ViewModel
For a counter that survives rotation:
class CounterViewModel : ViewModel() {
var count by mutableStateOf(0)
private set
fun increment() { count++ }
fun reset() { count = 0 }
}
@Composable
fun CounterScreen(vm: CounterViewModel = viewModel()) {
CounterButton(count = vm.count, onIncrement = vm::increment)
}
var count by mutableStateOf(0) directly on the ViewModel — Compose can read it just like local state. The private set ensures only the ViewModel methods can mutate.
Animation on change
import androidx.compose.animation.core.animateFloatAsState
val scale by animateFloatAsState(
targetValue = if (count % 5 == 0) 1.5f else 1f,
animationSpec = tween(300),
label = "scale",
)
Text(
"Count: $count",
modifier = Modifier.graphicsLayer(scaleX = scale, scaleY = scale),
)
Every fifth click, the count text pulses larger. Trivial polish, big perceptual payoff.
For more elaborate animations, AnimatedContent does smooth transitions when the value changes:
import androidx.compose.animation.AnimatedContent
AnimatedContent(targetState = count, label = "count") { c ->
Text("Count: $c", style = MaterialTheme.typography.headlineLarge)
}
Slides or fades between numbers as count changes.
Saving across rotation
import androidx.compose.runtime.saveable.rememberSaveable
var count by rememberSaveable { mutableStateOf(0) }
rememberSaveable persists in the saved-instance-state bundle. Survives configuration changes (rotation, theme switch, language change).
For state that should survive process death too, use a ViewModel + DataStore.
Common mistakes
Mutating in the body. count++ outside onClick triggers an infinite recomposition loop. Always mutate in callbacks.
Forgetting by imports. getValue and setValue need to be imported for var x by ... to work.
Sharing state via top-level var. Defeats remember and breaks across screens.
Counting events vs counting state. A button that fires an event "clicked" three times can produce inconsistent counts if the click handler is async. For race-free counting, use atomic operations or hoist to a ViewModel.
Forgetting private set on a ViewModel. Without it, anyone can write to the count from outside, breaking encapsulation.
What's next
Episode 24: Filter a list to numbers divisible by 3. A filter puzzle that's similar to the divisible-by-7 count, but with the values returned.
Recap
var count by remember { mutableStateOf(0) }. Increment in onClick. Display via string interpolation. Hoist state up for reuse. For Int specifically, mutableIntStateOf avoids boxing. Survive rotation with rememberSaveable. For complex state, use a ViewModel with private set. For animated value changes, AnimatedContent and animateFloatAsState.
Next episode: filter divisible by 3.