Jetpack Compose for Wear OS
Learn about Jetpack Compose for Wear OS by building a dedicated app to manage breath-holding times, including a stopwatch to track new records and save them in the collection. In this tutorial, you’ll get to know all the essential components, such as Inputs, Dialogs, Progress Indicators and Page Indicators. You’ll also learn when to use a Vignette and a TimeText. By Lena Stepanova.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Jetpack Compose for Wear OS
20 mins
Getting to Know the Components
Open RecordsListScreen.kt and add the following to RecordsListScreen()
body:
ScalingLazyColumn { // 1
item {
StopWatchListItemChip(onClickStopWatch) // 2
}
for (item in records) {
item {
RecordListItemChip(item, onClickRecordItem)
}
}
}
Here’s what this means:
-
ScalingLazyColumn()
is a Wear OS analog forLazyColumn()
. The difference is that it adapts to the round watch screen. Build and refresh the previews inRecordsListScreen
to get a visual representation. - Every item in the
ScalingLazyColumn()
is aChip()
. Look atStopWatchListItemChip()
andRecordListItemChip()
— they have placeholders foronClick
,icon
,label
,secondaryLabel
and other parameters.
Build and run. You’ll see a collection of breath holds:
You can either start a new training or choose a training day record from the list and then swipe to dismiss.
Congratulations — you nailed the navigation!
Now, open StopWatchScreen.kt. This screen displays the data processed in the StopWatchViewModel
. On top of the StopWatchScreen()
composable, there are two states that influence the recomposition:
val state by stopWatchViewModel.state.collectAsState() // 1
val duration by stopWatchViewModel.duration.collectAsState() // 2
- This state handles all parts of the UI that don’t rely on the current stopwatch time, such as the
StartStopButton()
or the text hint on top of it. - The
duration
state will trigger recomposition of the progress indicator and the time text every second.
For now, the StopWatchScreen()
only counts the time. But once the user finishes their breath hold, the app should ask for a certain input. This is a perfect place to use a dialog.
Using Dialogs
You can use Wear OS dialogs just like the regular Compose dialogs. Look at the dialog()
composable in StopWatchScreen()
:
Dialog(
showDialog = showSaveDialog, // 1
onDismissRequest = { showSaveDialog = false } // 2
) {
SaveResultDialog(
onPositiveClick = { // 3
},
onNegativeClick = {
},
result = duration.toRecordString()
)
}
Here’s what’s happening:
- You introduced
showSaveDialog
at the top ofStopWatchScreen()
. It controls whether this dialog is visible or not. - A simple callback resets
showSaveDialog
tofalse
and hides the dialog. -
SaveResultDialog()
is anAlert()
dialog and requiresonPositiveClick()
andonNegativeClick()
callbacks.
To activate this dialog, in the StartStopButton()
find the onStop()
callback and add the code below stopWatchViewModel.stop()
:
if (state.utbTime > 0) {
showSaveDialog = true
}
In freediving, the first important metric for your breath hold is the Urge To Breathe (UTB) time. This is the moment when the CO2 reaches a certain threshold and your brain signals your body to inhale. But it doesn’t mean you’ve run out of oxygen yet.
Check out the stop()
function in StopWatchViewModel.kt. It controls what happens when the user taps the stop button. On the first tap, it saves the UTB time to a local variable. On the second tap, time tracking actually stops. That’s why you set showSaveDialog
to true
only when utbTime
has already been recorded.
Build and run. Take a deep breath and start the stopwatch. Once you tap the button two times — one for UTB and one for final time — you’ll see the SaveResultDialog dialog:
Next, you’ll add some interactive components to this app.
Adding Inputs
Go to SaveResultDialog.kt. This is an Alert, which is one of the Wear OS dialog types. The other type is Confirmation. You can learn more about the differences between the two types in the official documentation.
Look at the parameters of Alert()
. It has an optional icon
and a title
, which is already created. The body of this alert dialog uses a Text()
composable. You only need to set the buttons for the user interaction. Set the negativeButton
and positiveButton
parameters to:
negativeButton = {
Button(
onClick = onNegativeClick, // 1
colors = ButtonDefaults.secondaryButtonColors() // 2
) {
Icon(
imageVector = Icons.Filled.Clear, // 3
contentDescription = "no"
)
}
},
positiveButton = {
Button(
onClick = onPositiveClick,
colors = ButtonDefaults.primaryButtonColors()
) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = "yes"
)
}
}
As you can see, using Button()
in Wear OS is simple:
- The most important part is providing the buttons with an
onClick
callback, which you’ll set in a moment. - You can specify the
colors
for the buttons. - You can also choose an
icon
— in this case, it’s a cross for the negative action and a tick for the positive action.
Back in the StopWatchScreen.kt, find SaveResultDialog()
and change the onPositiveCallback()
and onNegativeCallback()
to:
onPositiveClick = {
showSaveDialog = false
stopWatchViewModel.save()
},
onNegativeClick = {
showSaveDialog = false
stopWatchViewModel.refresh()
}
In both cases here, you close the dialog. If the user agrees to save the result, you call the relevant method from StopWatchViewModel
. Otherwise, you just need to refresh the values shown in the StopWatchScreen()
.
Build and run.
You can interact with the dialog and save or discard the breath hold result. Either way, you navigate back to the StopWatchScreen()
.
Buttons are one way to interact with the user. There are also several input options in Wear OS. You can use one of the following:
- Slider: To choose from a range of values.
- Stepper: If you want a vertical version of a slider.
- Toggle chip: To switch between two values.
- Picker: To select specific data.
In the OneBreath app, you’ll deal with a Slider()
.
Open AssessmentDialog.kt. Add the following line above the Alert()
, doing all the necessary imports:
var value by remember { mutableStateOf(5f) }
This will hold the value of an InlineSlider()
with an initial value of 5. In the next step, you’ll set the value range to 10.
Add the InlineSider()
to the empty body of Alert()
dialog:
InlineSlider(
value = value,
onValueChange = { value = it },
increaseIcon = { Icon(InlineSliderDefaults.Increase, "satisfied") },
decreaseIcon = { Icon(InlineSliderDefaults.Decrease, "unsatisfied") },
valueRange = 1f..10f,
steps = 10,
segmented = true
)
As you can see, it has several parameters for the value, the buttons, the value range, the number of steps and whether it has segments or not. The value
of this slider changes when the user taps the Increase or Decrease buttons. Since you want to save this value along with the breath hold time results, replace the empty onClick
parameter in positiveButton
:
onClick = {
onPositiveClick(value)
}
And now, back in StopWatchScreen.kt
, use the AssessmentDialog()
just like you did with SaveResultDialog()
. First, add a variable below showSaveDialog
:
var showRatingDialog by remember { mutableStateOf(false) }
Then, at the bottom of StopWatchScreen()
add a dialog. Use the showRatingDialog
as a handle to show or hide the dialog and use AssessmentDialog()
as content:
Dialog(
showDialog = showRatingDialog,
onDismissRequest = { showRatingDialog = false }
) {
AssessmentDialog(
onPositiveClick = { rating ->
showRatingDialog = false
stopWatchViewModel.save(rating) // 1
},
onNegativeClick = {
showRatingDialog = false
stopWatchViewModel.save() // 2
}
)
}
Here’s what happens:
- After tapping the positive button, you save the self-rating in the database along with other values from the
StopWatchViewModel
. - When the user doesn’t want to rate himself, you just save the result.
Also, replace stopWatchViewModel.save()
in SaveResultDialog()
with showRatingDialog = true
, because you want to show one dialog after another and save the result only after the AssessmentDialog()
.
Build and run. If you chose to keep the record in the first dialog, you’ll see the second dialog as well:
Ready for some even cooler Wear OS Composables? It’s time to talk about Vignette and TimeText.