With Jetpack Compose for Put on OS, you may construct lovely consumer interfaces for watches. It has tons of elements to select from. On this tutorial, you’ll find out about all the important elements — comparable to Inputs, Dialogs, Progress Indicators and Web page Indicators. You’ll additionally study when to make use of a Vignette and a TimeText.
Getting Began
Obtain the starter challenge by clicking the Obtain Supplies button on the high or backside of the tutorial. Unzip it and import into Android Studio. Construct and run.
The OneBreath app is a set of breath-holding instances. It additionally has a stopwatch to trace new data and save them within the assortment.
Mess around with the app to get a sense of what you’ll construct on this tutorial.
Take a look at ApneaRecordLocalSource.kt and ApneaRecordRepository.kt – these lessons mock an area information supply. It would assist to check the app, however it gained’t maintain your information between app launches.
Look additionally at StopWatchViewModel.kt. That is the view mannequin for the long run stopwatch display. It would deal with counting time.
You don’t have to alter something in these three lessons. Simply give attention to the UI.
Utilizing Appropriate Dependencies
Swap to the starter challenge. Go to the app-level construct.gradle and add the next dependencies:
implementation "androidx.put on.compose:compose-material:$wear_compose_version"
implementation "androidx.put on.compose:compose-navigation:$wear_compose_version"
implementation "androidx.put on.compose:compose-foundation:$wear_compose_version"
Why do you want these? In a Put on OS app, it is best to use the Put on OS variations for compose-material
and compose-navigation
as a result of they’re completely different from their common siblings. As for the compose-foundation
library, it builds upon its common model so you could have each dependencies.
Now that you’ve got all essential dependencies, construct and run. You’ll see the next display:
Time to dive in!
Watching over the Navigation
To start, you’ll add Compose navigation so you may navigate between screens.
Navigating Compose for Put on OS
Navigation in Compose for Put on OS is rather a lot just like the common Compose navigation.
Open MainActivity.kt and declare a NavHostController
above apneaRecordLocalSource
:
personal lateinit var navController: NavHostController
In setContent()
above OneBreathTheme()
, initialize a swipeDismissableNavController
:
navController = rememberSwipeDismissableNavController()
The distinction between Put on OS and an everyday app is in how the consumer navigates again. Since watches don’t have again buttons, navigation again occurs when customers swipe to dismiss. That’s why you’ll use a SwipeDissmissableNavHost()
right here.
Contained in the OneBreathTheme()
, substitute the non permanent Field()
composable with the entry level to the app:
OneBreathApp(
swipeDismissableNavController = navController,
apneaRecordRepository = apneaRecordRepository
)
Right here, you cross the just lately created navController
and the repository to OneBreathApp()
, the place you’ll arrange the app navigation.
Go to OneBreathApp.kt. As you may see, it makes use of Scaffold()
. However in contrast to the common Compose Scaffold()
, it has new attributes like timeText
and vignette
. You’ll get again to those later. For now, give attention to SwipeDismissableNavHost()
, the place you cross navController
and startDestination
as parameters.
Take a look at the Vacation spot.kt file within the ui/navigation folder:
sealed class Vacation spot(
val route: String
) {
object Data : Vacation spot("data")
object DayTrainingDetails : Vacation spot("dayTrainingDetails")
object StopWatch : Vacation spot("stopWatch")
}
This sealed class describes all of the attainable routes within the app. Now you may arrange navigation for these routes. In OneBreathApp.kt, substitute SwipeDismissableNavHost
‘s empty physique with the related routes:
composable(route = Vacation spot.StopWatch.route) {
}
composable(route = Vacation spot.TrainingDayDetails.route) {
}
composable(route = Vacation spot.Data.route) {
}
Add the next inside the primary route composable:
val stopWatchViewModel = StopWatchViewModel(apneaRecordRepository)
StopWatchScreen(stopWatchViewModel)
Right here, you create a StopWatchViewModel
and cross it to the StopWatchScreen()
.
The subsequent route is Vacation spot.TrainingDayDetails
. This can lead you to the TrainingDayDetailsScreen()
, the place you’ll see the stats for all of the breath holds you tried on that day. In a big app, you’d create a particulars display route primarily based on the id
of the merchandise you need to show and use that id
in a related DetailsViewModel
. However this app is quite easy, so you may simply maintain a reference to a particular coaching day within the OneBreathApp()
. Thus, add this line above Scaffold()
:
var selectedDay: TrainingDay? = null
Write this code contained in the composable with Vacation spot.TrainingDayDetails
:
selectedDay?.let { day -> // 1
TrainingDayDetailsScreen(
day.breaths, // 2
onDismissed = { swipeDismissableNavController.navigateUp() } // 3
)
}
Right here’s what’s occurring within the code above:
- Navigate solely after you set the
selectedDay
. - Solely the record of makes an attempt is important to show the small print.
- Not like the earlier route, you set the
onDismissed()
callback explicitly right here since you’re utilizingSwipeToDismissBox()
inTrainingDayDetails()
.
HorizontalViewPager and SwipeToDismissBox Navigation
Earlier than shifting on to the following vacation spot, open TrainingDayDetailsScreen.kt. The explanation why the compose navigation in OneBreathApp.kt is completely different for this display is the SwipeToDismissBox()
composable. The SwipeToDismissBox()
has two states:
if (isBackground) {
Field(modifier = Modifier.fillMaxSize()) // 1
} else {
Field(
modifier = Modifier
.fillMaxSize()
.edgeSwipeToDismiss(state) // 2
) {
HorizontalPager(state = pagerState, depend = maxPages) { web page ->
selectedPage = pagerState.currentPage
DetailsView(makes an attempt[page].utbTime, makes an attempt[page].totalDuration)
}
}
}
-
SwipeToDismissBox()
has a background scrim, which on this case is only a black full-screen field. - In a traditional state, this
Field()
composable holds aHorizontalPager
, which lets you scroll by way of the small print display horizontally, but in addition makes swipe-to-dismiss motion inconceivable. That’s why it’s essential place it inside aSwipeToDismissBox()
and have theedgeSwipeToDismiss()
modifier to navigate again solely when the consumer swipes proper within the small area on the left a part of the display.
Lastly, arrange the final navigation route: Vacation spot.Data
. Again in OneBreathApp.kt in SwipeDismissableNavHost()
, add the next code contained in the related composable:
RecordsListScreen(
apneaRecordRepository.data, // 1
onClickStopWatch = { // 2
swipeDismissableNavController.navigate(
route = Vacation spot.StopWatch.route
)
},
onClickRecordItem = { day -> // 3
selectedDay = day
swipeDismissableNavController.navigate(
route = Vacation spot.TrainingDayDetails.route
)
}
)
Right here’s what’s happening:
- The data record display shows a listing of data from the native supply.
- Whenever you faucet the New Coaching button, it redirects you to the stopwatch display.
- Whenever you select a selected coaching day from the record, it redirects to the coaching day particulars display.
As you may see, for the press occasions, this composable makes use of the 2 routes you’ve simply arrange.
You’re executed with the navigation — good job! However there’s nothing spectacular to see within the app but. So, it’s time to study in regards to the Compose UI elements for Put on OS.
Attending to Know the Elements
Open RecordsListScreen.kt and add the next to RecordsListScreen()
physique:
ScalingLazyColumn { // 1
merchandise {
StopWatchListItemChip(onClickStopWatch) // 2
}
for (merchandise in data) {
merchandise {
RecordListItemChip(merchandise, onClickRecordItem)
}
}
}
Right here’s what this implies:
-
ScalingLazyColumn()
is a Put on OS analog forLazyColumn()
. The distinction is that it adapts to the spherical watch display. Construct and refresh the previews inRecordsListScreen
to get a visible illustration. - Each merchandise within the
ScalingLazyColumn()
is aChip()
. Take a look atStopWatchListItemChip()
andRecordListItemChip()
— they’ve placeholders foronClick
,icon
,label
,secondaryLabel
and different parameters.
Construct and run. You’ll see a set of breath holds:
You’ll be able to both begin a brand new coaching or select a coaching day report from the record after which swipe to dismiss.
Congratulations — you nailed the navigation!
Now, open StopWatchScreen.kt. This display shows the information processed within the StopWatchViewModel
. On high of the StopWatchScreen()
composable, there are two states that affect the recomposition:
val state by stopWatchViewModel.state.collectAsState() // 1
val length by stopWatchViewModel.length.collectAsState() // 2
- This state handles all components of the UI that don’t depend on the present stopwatch time, such because the
StartStopButton()
or the textual content trace on high of it. - The
length
state will set off recomposition of the progress indicator and the time textual content each second.
For now, the StopWatchScreen()
solely counts the time. However as soon as the consumer finishes their breath maintain, the app ought to ask for a sure enter. This can be a good place to make use of a dialog.
Utilizing Dialogs
You should use Put on OS dialogs similar to the common Compose dialogs. Take a look at the dialog()
composable in StopWatchScreen()
:
Dialog(
showDialog = showSaveDialog, // 1
onDismissRequest = { showSaveDialog = false } // 2
) {
SaveResultDialog(
onPositiveClick = { // 3
},
onNegativeClick = {
},
end result = length.toRecordString()
)
}
Right here’s what’s occurring:
- You launched
showSaveDialog
on the high ofStopWatchScreen()
. It controls whether or not this dialog is seen or not. - A easy callback resets
showSaveDialog
tofalse
and hides the dialog. -
SaveResultDialog()
is anAlert()
dialog and requiresonPositiveClick()
andonNegativeClick()
callbacks.
To activate this dialog, within the StartStopButton()
discover the onStop()
callback and add the code under stopWatchViewModel.cease()
:
if (state.utbTime > 0) {
showSaveDialog = true
}
In freediving, the primary vital metric in your breath maintain is the Urge To Breathe (UTB) time. That is the second when the CO2 reaches a sure threshold and your mind indicators your physique to inhale. But it surely doesn’t imply you’ve run out of oxygen but.
Take a look at the cease()
perform in StopWatchViewModel.kt. It controls what occurs when the consumer faucets the cease button. On the primary faucet, it saves the UTB time to an area variable. On the second faucet, time monitoring truly stops. That’s why you set showSaveDialog
to true
solely when utbTime
has already been recorded.
Construct and run. Take a deep breath and begin the stopwatch. When you faucet the button two instances — one for UTB and one for closing time — you’ll see the SaveResultDialog dialog:
Subsequent, you’ll add some interactive elements to this app.
Including Inputs
Go to SaveResultDialog.kt. That is an Alert, which is among the Put on OS dialog sorts. The opposite kind is Affirmation. You’ll be able to study extra in regards to the variations between the 2 sorts within the official documentation.
Take a look at the parameters of Alert()
. It has an optionally available icon
and a title
, which is already created. The physique of this alert dialog makes use of a Textual content()
composable. You solely must set the buttons for the consumer interplay. Set the negativeButton
and positiveButton
parameters to:
negativeButton = {
Button(
onClick = onNegativeClick, // 1
colours = ButtonDefaults.secondaryButtonColors() // 2
) {
Icon(
imageVector = Icons.Crammed.Clear, // 3
contentDescription = "no"
)
}
},
positiveButton = {
Button(
onClick = onPositiveClick,
colours = ButtonDefaults.primaryButtonColors()
) {
Icon(
imageVector = Icons.Crammed.Verify,
contentDescription = "sure"
)
}
}
As you may see, utilizing Button()
in Put on OS is easy:
- An important half is offering the buttons with an
onClick
callback, which you’ll set in a second. - You’ll be able to specify the
colours
for the buttons. - You may also select an
icon
— on this case, it’s a cross for the unfavourable motion and a tick for the constructive motion.
Again within the StopWatchScreen.kt, discover SaveResultDialog()
and alter the onPositiveCallback()
and onNegativeCallback()
to:
onPositiveClick = {
showSaveDialog = false
stopWatchViewModel.save()
},
onNegativeClick = {
showSaveDialog = false
stopWatchViewModel.refresh()
}
In each instances right here, you shut the dialog. If the consumer agrees to save lots of the end result, you name the related technique from StopWatchViewModel
. In any other case, you simply must refresh the values proven within the StopWatchScreen()
.
Construct and run.
You’ll be able to work together with the dialog and save or discard the breath maintain end result. Both means, you navigate again to the StopWatchScreen()
.
Buttons are one strategy to work together with the consumer. There are additionally a number of enter choices in Put on OS. You should use one of many following:
- Slider: To select from a spread of values.
- Stepper: In order for you a vertical model of a slider.
- Toggle chip: To modify between two values.
- Picker: To pick out particular information.
Within the OneBreath app, you’ll take care of a Slider()
.
Open AssessmentDialog.kt. Add the next line above the Alert()
, doing all the required imports:
var worth by bear in mind { mutableStateOf(5f) }
This can maintain the worth of an InlineSlider()
with an preliminary worth of 5. Within the subsequent step, you’ll set the worth vary to 10.
Add the InlineSider()
to the empty physique of Alert()
dialog:
InlineSlider(
worth = worth,
onValueChange = { worth = it },
increaseIcon = { Icon(InlineSliderDefaults.Improve, "happy") },
decreaseIcon = { Icon(InlineSliderDefaults.Lower, "unhappy") },
valueRange = 1f..10f,
steps = 10,
segmented = true
)
As you may see, it has a number of parameters for the worth, the buttons, the worth vary, the variety of steps and whether or not it has segments or not. The worth
of this slider modifications when the consumer faucets the Improve or Lower buttons. Because you need to save this worth together with the breath maintain time outcomes, substitute the empty onClick
parameter in positiveButton
:
onClick = {
onPositiveClick(worth)
}
And now, again in StopWatchScreen.kt
, use the AssessmentDialog()
similar to you probably did with SaveResultDialog()
. First, add a variable under showSaveDialog
:
var showRatingDialog by bear in mind { mutableStateOf(false) }
Then, on the backside of StopWatchScreen()
add a dialog. Use the showRatingDialog
as a deal with to point out or cover the dialog and use AssessmentDialog()
as content material:
Dialog(
showDialog = showRatingDialog,
onDismissRequest = { showRatingDialog = false }
) {
AssessmentDialog(
onPositiveClick = { ranking ->
showRatingDialog = false
stopWatchViewModel.save(ranking) // 1
},
onNegativeClick = {
showRatingDialog = false
stopWatchViewModel.save() // 2
}
)
}
Right here’s what occurs:
- After tapping the constructive button, you save the self-rating within the database together with different values from the
StopWatchViewModel
. - When the consumer doesn’t need to fee himself, you simply save the end result.
Additionally, substitute stopWatchViewModel.save()
in SaveResultDialog()
with showRatingDialog = true
, since you need to present one dialog after one other and save the end result solely after the AssessmentDialog()
.
Construct and run. If you happen to selected to maintain the report within the first dialog, you’ll see the second dialog as properly:
Prepared for some even cooler Put on OS Composables? It’s time to speak about Vignette and TimeText.
Including a Vignette
Open OneBreathApp.kt and take a look at the parameters in Scaffold()
once more.
Set vignette
parameter to:
vignette = {
if (currentBackStackEntry?.vacation spot?.route == Vacation spot.Data.route) {
Vignette(vignettePosition = VignettePosition.TopAndBottom)
}
}
This situation means the vignette will likely be there just for the RecordsListScreen()
. A vignette is a UI characteristic that dims an edge a part of the display. In your case, it’s TopAndBottom
, as laid out in vignettePosition
.
Examine the report record display with and with out the vignette:
With out Vignette | With Vignette |
---|---|
![]() |
![]() |
See the distinction? Within the right-hand model, the perimeters are barely darker.
TimeText
One other important Put on OS UI part is TimeText()
. Nonetheless in OneBreathApp.kt, substitute the empty timeText
parameter in Scaffold()
with:
timeText = {
if (currentBackStackEntry?.vacation spot?.route == Vacation spot.TrainingDayDetails.route) { // 1
TimeText(
startLinearContent = { // 2
Textual content(
textual content = selectedDay?.date.toFormattedString(),
colour = colorPrimary,
type = textStyle
)
},
startCurvedContent = { // 3
curvedText(
textual content = selectedDay?.date.toFormattedString(),
colour = colorPrimary,
type = CurvedTextStyle(textStyle)
)
}
)
} else TimeText() // 4
}
Right here’s a breakdown of this code:
- You solely need to present an extra textual content earlier than the time within the coaching day particulars display. This extra textual content will maintain the date of the report.
-
TimeText()
adapts to spherical and sq. watches. For sq. watches, it makes use ofTimeTextDefaults.timeTextStyle()
. - For spherical watches, use
CurvedTextStyle()
. - All of the screens besides the coaching day particulars display will nonetheless present the present time on high.
Construct and run. You’ll see the present time on high now. Faucet on one of many inexperienced chips. Within the coaching day particulars display, you’ll additionally see the date:
Progress Indicator
Wouldn’t or not it’s good to have one thing like a clock hand for the stopwatch? You are able to do that with a Put on OS CircularProgressIndicator.
Go to StopWatchScreen.kt and add the next to the highest of the Field()
, proper above Column()
:
CircularProgressIndicator(
progress = length.toProgress(),
modifier = Modifier
.fillMaxSize()
.padding(all = 1.dp)
)
This indicator will recompose each second and present the present length of your breath maintain. It’s often advisable to go away a spot for the TimeText()
by including startAngle
and endAngle
parameters, however in OneBreath you’ll sacrifice these to make the indicator resemble a clock hand.
Construct and run the app and begin the stopwatch. You’ll see the clock ticking:
This CircularProgressIndicator()
is determinate, however you too can use its indeterminate model to point out a loading indicator – simply omit the progress parameter. It could seem like this:
Web page Indicator
When you’re nonetheless working the app, go to the report record display and faucet on one of many coaching day objects. Right here, within the particulars display, you may web page by way of all of your breath holds on that day. Could be good to know what web page you’re on, proper? A HorizontalPageIndicator will provide help to with that.
Go to TrainingDayDetailsScreen.kt. In SwipeToDismissBox()
, add this under val pagerState = rememberPagerState()
:
val pageIndicatorState: PageIndicatorState = bear in mind {
object : PageIndicatorState {
override val pageOffset: Float
get() = 0f
override val selectedPage: Int
get() = selectedPage
override val pageCount: Int
get() = maxPages
}
}
Right here, you create a PageIndicatorState
that connects the HorizontalPager()
and HorizontalPageIndicator()
. The selectedPage
is ready whenever you scroll by way of the pager. The pageCount
is the whole variety of makes an attempt on the coaching day. The pageOffset
is 0f
on this case, however you need to use it to animate the indicator.
To make use of it, add HorizontalPageIndicator()
proper under HorizontalPager
:
HorizontalPageIndicator(
pageIndicatorState = pageIndicatorState,
selectedColor = colorAccent
)
Construct and run. Choose a coaching day from the record. You’ll see a paging indicator on the backside:
HorizontalPageIndicator()
is an instance of a horizontal paging indicator. If you happen to want a vertical indicator, you need to use a PositionIndicator()
. Take a look at the official supplies for extra elements.
The place to Go From Right here?
Obtain the finished challenge recordsdata by clicking the Obtain Supplies button on the high or backside of the tutorial.
Congratulations! Now you can monitor your breath-holding data with the app you’ve simply constructed. Now, take a deep breath and set a brand new private report! :]
If you happen to’re taken with studying extra about varied Put on OS Composables, try the official documentation documentation, in addition to the Horologist library for superior date and time pickers. And if you happen to take pleasure in Put on OS growth, don’t miss out on the Creating Tiles for Put on OS video course.
We hope you loved this tutorial. You probably have any questions or feedback, please be a part of the discussion board dialogue under!