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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

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:

  1. ScalingLazyColumn() is a Wear OS analog for LazyColumn(). The difference is that it adapts to the round watch screen. Build and refresh the previews in RecordsListScreen to get a visual representation.
  2. Every item in the ScalingLazyColumn() is a Chip(). Look at StopWatchListItemChip() and RecordListItemChip() — they have placeholders for onClick, icon, label, secondaryLabel and other parameters.

Build and run. You’ll see a collection of breath holds:

records list

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
  1. 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.
  2. 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:

  1. You introduced showSaveDialog at the top of StopWatchScreen(). It controls whether this dialog is visible or not.
  2. A simple callback resets showSaveDialog to false and hides the dialog.
  3. SaveResultDialog() is an Alert() dialog and requires onPositiveClick() and onNegativeClick() 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:

result 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:

  1. The most important part is providing the buttons with an onClick callback, which you’ll set in a moment.
  2. You can specify the colors for the buttons.
  3. 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.

interact with dialog

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:

  1. After tapping the positive button, you save the self-rating in the database along with other values from the StopWatchViewModel.
  2. 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:

slider dialog
Ready for some even cooler Wear OS Composables? It’s time to talk about Vignette and TimeText.