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
With Jetpack Compose for Wear OS, you can build beautiful user interfaces for watches. It has tons of components to choose from. In this tutorial, you’ll learn about all of the essential components — such as Inputs, Dialogs, Progress Indicators and Page Indicators. You’ll also learn when to use a Vignette and a TimeText.
Getting Started
Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial. Unzip it and import into Android Studio. Build and run.
The OneBreath app is a collection of breath-holding times. It also has a stopwatch to track new records and save them in the collection.
Play around with the app to get a feeling of what you’ll build in this tutorial.
Check out ApneaRecordLocalSource.kt and ApneaRecordRepository.kt – these classes mock a local data source. It will help to test the app, but it won’t keep your data between app launches.
Look also at StopWatchViewModel.kt. This is the view model for the future stopwatch screen. It will take care of counting time.
You don’t have to change anything in these three classes. Just focus on the UI.
Using Correct Dependencies
Switch to the starter project. Go to the app-level build.gradle and add the following dependencies:
implementation "androidx.wear.compose:compose-material:$wear_compose_version"
implementation "androidx.wear.compose:compose-navigation:$wear_compose_version"
implementation "androidx.wear.compose:compose-foundation:$wear_compose_version"
Why do you need these? In a Wear OS app, you should use the Wear OS versions for compose-material
and compose-navigation
because they are different from their regular siblings. As for the compose-foundation
library, it builds upon its regular version so you have both dependencies.
Now that you have all necessary dependencies, build and run. You’ll see the following screen:
Time to dive in!
Watching over the Navigation
To begin, you’ll add Compose navigation so you can navigate between screens.
Navigating Compose for Wear OS
Navigation in Compose for Wear OS is a lot like the regular Compose navigation.
Open MainActivity.kt and declare a NavHostController
above apneaRecordLocalSource
:
private lateinit var navController: NavHostController
In setContent()
above OneBreathTheme()
, initialize a swipeDismissableNavController
:
navController = rememberSwipeDismissableNavController()
The difference between Wear OS and a regular app is in how the user navigates back. Since watches don’t have back buttons, navigation back happens when users swipe to dismiss. That’s why you’ll use a SwipeDissmissableNavHost()
here.
Inside the OneBreathTheme()
, replace the temporary Box()
composable with the entry point to the app:
OneBreathApp(
swipeDismissableNavController = navController,
apneaRecordRepository = apneaRecordRepository
)
Here, you pass the recently created navController
and the repository to OneBreathApp()
, where you’ll set up the app navigation.
Go to OneBreathApp.kt. As you can see, it uses Scaffold()
. But unlike the regular Compose Scaffold()
, it has new attributes like timeText
and vignette
. You’ll get back to these later. For now, focus on SwipeDismissableNavHost()
, where you pass navController
and startDestination
as parameters.
Check out the Destination.kt file in the ui/navigation folder:
sealed class Destination(
val route: String
) {
object Records : Destination("records")
object DayTrainingDetails : Destination("dayTrainingDetails")
object StopWatch : Destination("stopWatch")
}
This sealed class describes all the possible routes in the app. Now you can set up navigation for those routes. In OneBreathApp.kt, replace SwipeDismissableNavHost
‘s empty body with the relevant routes:
composable(route = Destination.StopWatch.route) {
}
composable(route = Destination.TrainingDayDetails.route) {
}
composable(route = Destination.Records.route) {
}
Add the following inside the first route composable:
val stopWatchViewModel = StopWatchViewModel(apneaRecordRepository)
StopWatchScreen(stopWatchViewModel)
Here, you create a StopWatchViewModel
and pass it to the StopWatchScreen()
.
The next route is Destination.TrainingDayDetails
. This will lead you to the TrainingDayDetailsScreen()
, where you’ll see the stats for all the breath holds you attempted on that day. In a large app, you’d create a details screen route based on the id
of the item you want to display and use that id
in a relevant DetailsViewModel
. But this app is rather simple, so you can just keep a reference to a selected training day in the OneBreathApp()
. Thus, add this line above Scaffold()
:
var selectedDay: TrainingDay? = null
Write this code inside the composable with Destination.TrainingDayDetails
:
selectedDay?.let { day -> // 1
TrainingDayDetailsScreen(
day.breaths, // 2
onDismissed = { swipeDismissableNavController.navigateUp() } // 3
)
}
Here’s what’s happening in the code above:
- Navigate only after you set the
selectedDay
. - Only the list of attempts is necessary to display the details.
- Unlike the previous route, you set the
onDismissed()
callback explicitly here because you’re usingSwipeToDismissBox()
inTrainingDayDetails()
.
HorizontalViewPager and SwipeToDismissBox Navigation
Before moving on to the next destination, open TrainingDayDetailsScreen.kt. The reason why the compose navigation in OneBreathApp.kt is different for this screen is the SwipeToDismissBox()
composable. The SwipeToDismissBox()
has two states:
if (isBackground) {
Box(modifier = Modifier.fillMaxSize()) // 1
} else {
Box(
modifier = Modifier
.fillMaxSize()
.edgeSwipeToDismiss(state) // 2
) {
HorizontalPager(state = pagerState, count = maxPages) { page ->
selectedPage = pagerState.currentPage
DetailsView(attempts[page].utbTime, attempts[page].totalDuration)
}
}
}
-
SwipeToDismissBox()
has a background scrim, which in this case is just a black full-screen box. - In a normal state, this
Box()
composable holds aHorizontalPager
, which allows you to scroll through the details screen horizontally, but also makes swipe-to-dismiss action impossible. That’s why you need to place it within aSwipeToDismissBox()
and have theedgeSwipeToDismiss()
modifier to navigate back only when the user swipes right in the small space on the left part of the screen.
Finally, set up the last navigation route: Destination.Records
. Back in OneBreathApp.kt in SwipeDismissableNavHost()
, add the following code inside the relevant composable:
RecordsListScreen(
apneaRecordRepository.records, // 1
onClickStopWatch = { // 2
swipeDismissableNavController.navigate(
route = Destination.StopWatch.route
)
},
onClickRecordItem = { day -> // 3
selectedDay = day
swipeDismissableNavController.navigate(
route = Destination.TrainingDayDetails.route
)
}
)
Here’s what’s going on:
- The records list screen displays a list of records from the local source.
- When you tap the New Training button, it redirects you to the stopwatch screen.
- When you choose a particular training day from the list, it redirects to the training day details screen.
As you can see, for the click events, this composable uses the two routes you’ve just set up.
You’re done with the navigation — good job! But there’s nothing spectacular to see in the app yet. So, it’s time to learn about the Compose UI components for Wear OS.