Compose for Wear OS Codelab

1. Introduction

4d28d16f3f514083.png

Compose for Wear OS lets you translate the knowledge you've learned building apps with Jetpack Compose to wearable devices.

With built-in support for Material You, Compose for Wear OS simplifies and accelerates UI development and helps you create beautiful apps with less code.

For this codelab, we expect you to have some knowledge of Compose, but you certainly don't need to be an expert.

You will create several Wear specific composables (both simple and complex), and, by the end, you can start writing your own apps for Wear OS. Let's get started!

What you will learn

  • Similarities/differences between your previous experience with Compose
  • Simple composables and how they work on Wear OS
  • Wear OS specific composables
  • Wear OS's LazyColumn (ScalingLazyColumn)
  • Wear OS's version of the Scaffold

What you will build

You'll build a simple app that displays a scrollable list of composables optimized for Wear OS.

Because you will be using Scaffold, you'll also get a curved text time at the top, a vignette, and finally a scrolling indicator tied to the side of the device.

Here's what it will look like when you are finished with the code lab:

fde77fdcf7ed7390.gif

Prerequisites

2. Getting Set Up

In this step, you will set up your environment and download the starter project.

What you will need

  • Latest stable version of Android Studio
  • Wear OS device or emulator (New to this? Here's how to set it up.)

Download code

If you have git installed, you can simply run the command below to clone the code from this repo. To check whether git is installed, type git --version in the terminal or command line and verify that it executes correctly.

git clone https://github.com/googlecodelabs/compose-for-wear-os.git
cd compose-for-wear-os

If you do not have git, you can click the following button to download all the code for this codelab:

Download ZIP

At any time you can run either module in Android Studio by changing the run configuration in the toolbar.

8a2e49d6d6d2609d.png

Open project in Android Studio

  1. On the Welcome to Android Studio window select 1f5145c42df4129a.png Open an Existing Project
  2. Select the folder [Download Location]
  3. When Android Studio has imported the project, test that you can run the start and finished modules on a Wear OS emulator or physical device.
  4. The start module should look like the screenshot below. It's where you will be doing all your work.

7668d2915856b849.png

Explore the start code

  • build.gradle contains a basic app configuration. It includes the dependencies necessary to create a Composable Wear OS App. We'll discuss what's similar and different between Jetpack Compose and the Wear OS version.
  • main > AndroidManifest.xml includes the elements necessary to create a Wear OS application. This is the same as a non-Compose app and similar to a mobile app, so we won't review this.
  • main > theme/ folder contains the Color, Type, and Theme files used by Compose for the theme.
  • main > MainActivity.kt contains boilerplate for creating an app with Compose. It also contains the top-level composables (like the Scaffold and ScalingLazyList) for our app.
  • main > ReusableComponents.kt contains functions for most of the Wear specific composables we'll create. We will do a lot of our work in this file.

3. Review the dependencies

Most of the Wear related dependency changes you make will be at the top architectural layers (highlighted in red below).

d92519e0b932f964.png

That means many of the dependencies you already use with Jetpack Compose don't change when targeting Wear OS. For example, the UI, Runtime, Compiler, and Animation dependencies will remain the same.

However, you will need to use the proper Wear OS Material, Foundation, and Navigation libraries which are different from the libraries you have used before.

Below is a comparison to help clarify the differences:

Wear OS Dependency(androidx.wear.*)

Comparison

Standard Dependency(androidx.*)

androidx.wear.compose:compose-material

instead of

androidx.compose.material:material

androidx.wear.compose:compose-navigation

instead of

androidx.navigation:navigation-compose

androidx.wear.compose:compose-foundation

in addition to

androidx.compose.foundation:foundation

1. Developers can continue to use other material related libraries like material ripple and material icons extended with the Wear Compose Material library.

Open the build.gradle, search for "TODO: Review Dependencies" in your start module. (This step is just to review the dependencies, you will not be adding any code.)

start/build.gradle:

// TODO: Review Dependencies
// General Compose dependencies
implementation "androidx.activity:activity-compose:$activity_compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation "androidx.compose.foundation:foundation:$compose_version"
implementation "androidx.compose.material:material-icons-extended:$compose_version"

// Compose for Wear OS Dependencies
implementation "androidx.wear.compose:compose-material:$wear_compose_version"

// Foundation is additive, so you can use the standard version in your Wear OS app.
implementation "androidx.wear.compose:compose-foundation:$wear_compose_version"

You should recognize many of the general Compose dependencies, so we won't cover those.

Let's move to the Wear OS dependencies.

Just as outlined earlier, only the Wear OS specific version of material (androidx.wear.compose:compose-material) is included. That is, you will not see or include androidx.compose.material:material in your project.

It's important to call out that you can use other material libraries with Wear Material. We actually do that in this codelab by including androidx.compose.material:material-icons-extended.

Finally, we include the Wear foundation library for Compose (androidx.wear.compose:compose-foundation) . This is additive, so you can use it with the standard foundation you've used before. In fact, you probably already recognized we included it in the general compose dependencies!

Ok, now that we understand the dependencies, let's have a look at the main app.

4. Review MainActivity

We will do all our work in the

start

module, so make sure every file you open is in there.

Let's start by opening MainActivity in the start module.

This is a pretty simple class that extends ComponentActivity and uses setContent { WearApp() } to create the UI.

From your previous knowledge of Compose, this should look familiar to you. We are just setting up the UI.

Scroll down to the WearApp() composable function. Before we talk about the code itself, you should see a bunch of TODOs scattered throughout the code. These each represent steps in this codelab. You can ignore them for now.

It should look something like this:

Code in fun WearApp():

WearAppTheme {
    // TODO: Swap to ScalingLazyListState
    val listState = rememberLazyListState()

    /* *************************** Part 4: Wear OS Scaffold *************************** */
    // TODO (Start): Create a Scaffold (Wear Version)


        // Modifiers used by our Wear composables.
        val contentModifier = Modifier.fillMaxWidth().padding(bottom = 8.dp)
        val iconModifier = Modifier.size(24.dp).wrapContentSize(align = Alignment.Center)

        /* *************************** Part 3: ScalingLazyColumn *************************** */
        // TODO: Create a ScalingLazyColumn (Wear's version of LazyColumn)
        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            contentPadding = PaddingValues(
                top = 32.dp,
                start = 8.dp,
                end = 8.dp,
                bottom = 32.dp
            ),
            verticalArrangement = Arrangement.Center,
            state = listState
        ) {

            // TODO: Remove item; for beginning only.
            item { StartOnlyTextComposables() }


            /* ******************* Part 1: Simple composables ******************* */
            item { ButtonExample(contentModifier, iconModifier) }
            item { TextExample(contentModifier) }
            item { CardExample(contentModifier, iconModifier) }

            /* ********************* Part 2: Wear unique composables ********************* */
            item { ChipExample(contentModifier, iconModifier) }
            item { ToggleChipExample(contentModifier) }
        }

    // TODO (End): Create a Scaffold (Wear Version)

}

We start by setting the theme, WearAppTheme { }. This is exactly the same way you've written it before, that is, you set a MaterialTheme with colors, typography, and shapes.

However, in the case of Wear OS, we generally recommend using the default Material Wear shapes which are optimized for round and non-round devices, so if you dive into our theme/Theme.kt, you can see we don't override shapes.

If you wish, you can open the theme/Theme.kt to explore it further, but, again, it's the same as before.

Next, we create some Modifiers for the Wear composables we're going to build out, so we don't need to specify them every time. It's mostly centering the content and adding some padding.

We then create a LazyColumn which is used to produce a vertically scrolling list for a bunch of items (just like you did before).

Code:

item { StartOnlyTextComposables() }

/* ******************* Part 1: Simple composables ******************* */
item { ButtonExample(contentModifier, iconModifier) }
item { TextExample(contentModifier) }
item { CardExample(contentModifier, iconModifier) }

/* ********************* Part 2: Wear unique composables ********************* */
item { ChipExample(contentModifier, iconModifier) }
item { ToggleChipExample(contentModifier) }

For the items themselves, only StartOnlyTextComposables() produces any UI. (We will populate the rest throughout the code lab.)

These functions are actually in the ReusableComponents.kt file, which we will visit in the next section.

Let's get started with Compose for Wear OS!

5. Add Simple Composables

We'll start with three composables (Button, Text, and Card) that you are probably already familiar with.

First, we are going to remove the hello world composable.

Search for "TODO: Remove item" and erase both the comment and the line below it:

Step 1

// TODO: Remove item; for beginning only.
item { StartOnlyTextComposables() }

Next, let's add our first composable.

Create a Button composable

Open ReusableComponents.kt in the start module and search for "TODO: Create a Button Composable" and replace the current composable method with this code.

Step 2

// TODO: Create a Button Composable (with a Row to center)
@Composable
fun ButtonExample(
    modifier: Modifier = Modifier,
    iconModifier: Modifier = Modifier
) {
    Row(
        modifier = modifier,
        horizontalArrangement = Arrangement.Center
    ) {
        // Button
        Button(
            modifier = Modifier.size(ButtonDefaults.LargeButtonSize),
            onClick = { /* ... */ },
        ) {
            Icon(
                imageVector = Icons.Rounded.Phone,
                contentDescription = "triggers phone action",
                modifier = iconModifier
            )
        }
    }
}

The ButtonExample() composable function (where this code exists) will now generate a centered button.

Let's walk through the code.

The Row is only used here to center the Button composable on the round screen. You can see we are doing that by applying the modifier we created in MainActivity and passing it into this function. Later, when we scroll on a circular screen, we want to make sure our content isn't cut off (which is why it's centered).

Next, we create the Button itself. The code is the same as you would use for a Button before, but, in our case, we use the ButtonDefault.LargeButtonSize. These are preset sizes optimized for Wear OS devices, so make sure you use them!

After that, we set the click event to an empty lamba. In our case, these composables are just for a demo, so we won't need that. However, in a real app, we'd communicate with a, for example, ViewModel to perform business logic.

Then we set an Icon inside our button. This code is the same as you have seen for an Icon before. We are also getting our icon from the androidx.compose.material:material-icons-extended library.

Finally, we set the modifier we set earlier for Icons.

If you run the app, you should get something like this:

f9fce435c935d610.png

This is code you have probably already written before (which is great). The difference is now you get a button optimized for Wear OS.

Pretty straightforward, let's look at another one.

Create a Text composable

In ReusableComponents.kt, search for "TODO: Create a Text Composable" and replace the current composable method with this code.

Step 3

// TODO: Create a Text Composable
@Composable
fun TextExample(modifier: Modifier = Modifier) {
    Text(
        modifier = modifier,
        textAlign = TextAlign.Center,
        color = MaterialTheme.colors.primary,
        text = stringResource(R.string.device_shape)
    )
}

We create the Text composable, set its modifier, align the text, set a color, and finally set the text itself from a String resource.

Text composables should look very familiar to Compose developers and the code is actually identical to the code you've used before.

Let's see what it looks like:

52a4e318d12d7ba5.png

The TextExample() composable function (where we placed our code) now produces a Text composable in our main material color.

The string is pulled from our res/values/strings.xml file. Actually, if you look in the res/values folder, you should see two strings.xml resource files.

Wear OS provides string resources for round and non-round devices, so if we run this on a square emulator, the string will change:

27e50afef57b7717.png

So far, so good. Let's look at our last similar composable, Card.

Create a Card composable

In ReusableComponents.kt, search for "TODO: Create a Card" and replace the current composable method with this code.

Step 4

// TODO: Create a Card (specifically, an AppCard) Composable
@Composable
fun CardExample(
    modifier: Modifier = Modifier,
    iconModifier: Modifier = Modifier
) {
    AppCard(
        modifier = modifier,
        appImage = {
            Icon(
                imageVector = Icons.Rounded.Message,
                contentDescription = "triggers open message action",
                modifier = iconModifier
            )
        },
        appName = { Text("Messages") },
        time = { Text("12m") },
        title = { Text("Kim Green") },
        onClick = { /* ... */ }
    ) {
        Column(modifier = Modifier.fillMaxWidth()) {
            Text("On my way!")
        }
    }
}

Wear is a little different in that we have two major cards, AppCard and TitleCard.

In our case, we want an Icon in our card, so we are going to use AppCard. (TitleCard is focused on text only.)

We create the AppCard composable, set its modifier, add an Icon, add several Text composable parameters (each for a different space on the card), and finally set the main content text at the end.

Let's see what it looks like:

dcb5e5bebf9f19d9.png

At this point, you probably recognize that for these composables the Compose code is virtually the same as you've used before which is great, right? You get to reuse all that knowledge you've already gained!

Ok, let's look at some new composables.

6. Add Wear Unique Composables

For this section, we will explore the Chip and ToggleChip composables.

Create a Chip composable

Chips are actually specified in the material guidelines, but there isn't an actual composable function in the standard material library.

They are meant to be a quick, one tap action, which makes especially good sense for a Wear device with limited screen real estate.

Here's a couple variations of the Chip composable function to give you an idea of what you can create:

Let's write some code.

In ReusableComponents.kt, search for "TODO: Create a Chip" and replace the current composable method with this code.

Step 5

// TODO: Create a Chip Composable
@Composable
fun ChipExample(
    modifier: Modifier = Modifier,
    iconModifier: Modifier = Modifier
) {
    Chip(
        modifier = modifier,
        onClick = { /* ... */ },
        label = {
            Text(
                text = "5 minute Meditation",
                maxLines = 1,
                overflow = TextOverflow.Ellipsis
            )
        },
        icon = {
            Icon(
                imageVector = Icons.Rounded.SelfImprovement,
                contentDescription = "triggers meditation action",
                modifier = iconModifier
            )
        },
    )
}

The Chip composable uses many of the same parameters as you are used to with other composables (modifier and onClick), so we don't need to review those.

It also takes a label (which we create a Text composable for) and an icon.

The Icon code should look exactly the same as the code you saw in other composables, but for this one, we are pulling the Self Improvement icon from the androidx.compose.material:material-icons-extended library.

Let's see what it looks like (remember to scroll down):

77bde81f8f290f87.png

Ok, let's look at a variation on Toggle, the ToggleChip composable.

Create a ToggleChip composable

ToggleChip is just like a Chip but allows the user to interact with a radio button, toggle, or checkbox.

In ReusableComponents.kt, search for "TODO: Create a ToggleChip" and replace the current composable method with this code.

Step 6

// TODO: Create a ToggleChip Composable
@Composable
fun ToggleChipExample(modifier: Modifier = Modifier) {
    var checked by remember { mutableStateOf(true) }
    ToggleChip(
        modifier = modifier,
        checked = checked,
        toggleIcon = {
            ToggleChipDefaults.SwitchIcon(checked = checked)
        },
        onCheckedChange = {
            checked = it
        },
        label = {
            Text(
                text = "Sound",
                maxLines = 1,
                overflow = TextOverflow.Ellipsis
            )
        }
    )
}

Now the ToggleChipExample() composable function (where this code exists) generates a ToggleChip using a toggle (instead of a checkbox or radio button).

First, we create a MutableState. We haven't been doing this in the other functions, because we are mainly doing UI demos so you can see what Wear offers.

In a normal app, you would probably want to pass in the checked state and the lambda for handling the tap, so the composable can be stateless ( more info here).

In our case, we are just keeping it simple to show off what the ToggleChip looks like in action with a working toggle (even though we don't do anything with the state).

Next, we set the modifier, the checked state, and the toggle icon to give us the switch we want. (We use the defaults in this case.)

We then create a lambda for changing the state and finally set the label with a Text composable (and some basic parameters).

Let's see what it looks like:

b76b501e91a64969.png

Ok, now you've seen a lot of Wear OS specific composables and, as stated before, most of the code is almost the same as what you've written before.

Let's look at something a little more advanced.

7. Migrate to ScalingLazyColumn

You probably have used LazyColumn in your mobile apps to produce a vertically scrolling list.

Because a round device is smaller at the top and bottom, there is less space to show items. Therefore, Wear OS has its own version of LazyColumn to better support those round devices.

ScalingLazyColumn extends LazyColumn to support both scaling and transparency at the top and bottom of the screen to make the content more readable to the user.

Here's a demo:

28460354eaf16eea.gif

Notice how as the item gets near the center it scales up to its full size and then as it moves away it scales back down (along with getting more transparent).

Here is a more concrete example from an app:

307c969dccf58e49.gif

We've found this really helps with readability.

Now that you've seen ScalingLazyListState in action, let's get started converting our LazyColumn.

Convert to a ScalingLazyListState

In MainActivity.kt, search for "TODO: Swap to ScalingLazyListState" and replace that comment and line below with this code.

Step 7

// TODO: Swap to ScalingLazyListState
val listState = rememberScalingLazyListState()

The names are almost identical minus the "Scaling" portion. Just like LazyListState handles state for a LazyColumn, ScalingLazyListState handles it for a ScalingLazyColumn.

Convert to a ScalingLazyColumn

Next we swap in ScalingLazyColumn.

In MainActivity.kt, search for "TODO: Swap a ScalingLazyColumn" and replace that line and the line below with this code.

Step 8

        // TODO: Swap a ScalingLazyColumn (Wear's version of LazyColumn)
        ScalingLazyColumn(

That's it! Let's see what it looks like:

264ab059418a6526.png

You can see the content is scaled and the transparency is adjusted at the top and bottom of the screen as you scroll with very little work to migrate!

You can really notice it with the meditation composables as you move it up and down.

Now onto the last topic, Wear OS's Scaffold.

8. Add a Scaffold

Scaffold provides a layout structure to help you arrange screens in common patterns, just like mobile, but instead of an App Bar, FAB, Drawer, or other mobile specific elements, it supports three Wear specific layouts with top-level components: time, vignette, and the scroll/position indicator.

It also handles both round and non-round devices.

Here's what they look like:

TimeText

Vignette

PositionIndicator

We'll look at each component in detail, but, first, let's put the scaffold in place.

Add a Scaffold

Let's add the boilerplate for the Scaffold now.

Find "TODO (Start): Create a Scaffold (Wear Version)" and add the code below it.

Step 9

// TODO (Start): Create a Scaffold (Wear Version)
Scaffold(
    timeText = { },
    vignette = { },
    positionIndicator = { }
) {

We will explain each of these parameters in sequence in later steps. Right now, we aren't producing any UI.

Next, make sure you add the closing bracket to the right location. Find "TODO (End): Create a Scaffold (Wear Version)" and add the bracket there:

Step 10

// TODO (End): Create a Scaffold (Wear Version)
}

Let's run it first. You should see something like this:

2ddc667e07d8f04c.png

Notice, it doesn't really behave any differently than before since we added the Scaffold, but that will change once we start implementing our components.

Let's start with the first of the three parameters, TimeText.

TimeText

TimeText uses curved text under the hood and gives developers an easy way to show the time without placing the composable or having to do any work with time related classes.

The Material Guidelines require that you display the time at the top of any screen within any overlay (or app) experience. Here's an example of what it should look like:

29671ae0a7c3225b.png

Adding TimeText is actually quite simple.

Find "timeText = { }," and replace it with the code below:

Step 11

timeText = {
    if (!listState.isScrollInProgress) {
        TimeText()
    }
},

We are adding a little extra logic to hide the time if the user starts scrolling.

After that, we just create a TimeText composable. We could add extra parameters to add text before and/or after the time, but we want to keep it simple.

Try running it. You should now notice the time, but if you scroll, it disappears.

896aaebb9a89bbe8.png

Next we will cover the Vignette.

Add Vignette

A Vignette blurs the top and bottom edges of the wearable screen when a screen capable of scrolling is displayed.

Developers can specify to blur the top, bottom, or both depending on the use case.

Here's an example:

689ac8becaac6824.png

We only have one screen (which is a scrollable screen), so we want to show the Vignette to help readability. Let's do that now.

Find "vignette = { }," and replace it with the code below:

Step 12

vignette = {
    // Only show a Vignette for scrollable screens. This code lab only has one screen,
    // which is scrollable, so we show it all the time.
    Vignette(vignettePosition = VignettePosition.TopAndBottom)
},

Read the comment for more details on when you should and should not show a vignette. In our case, we want it shown all the time and we want to blur both the top and bottom of the screen.

9a91313e5d59f49e.png

If you look at the top or bottom (especially on the purple composables), you should see the effect.

Let's finish off our final parameter of the Scaffold, PositionIndicator.

Add PositionIndicator

The PositionIndicator (also known as the Scrolling Indicator) is an indicator on the right side of the screen to show the current indicator location based on the type of state object you pass in. In our case, that will be the ScalingLazyListState.

Here's an example:

181b19b55ce37251.png

You might wonder why the position indicator needs to be up at the Scaffold level and not the ScalingLazyColumn level.

Well, due to the curvature of the screen, the position indicator needs to be centered on the watch (Scaffold), not just centered on the viewport (ScalingLazyColumn). Otherwise, it could be cut off.

For example, in the app below, we can assume that the "Playlist" composable isn't part of the scrollable area. The position indicator is centered on the ScalingLazyColumn but it doesn't take up the whole screen. Therefore, you can see that most of it is cut off.

fcc542c5269c8547.png

However, if we center the position indicator instead on the entire viewable surface (which is what the Scaffold gives you), you can now see it clearly.

c9f96a90a3e811fb.png

That does mean the PositionIndicator requires the ScalingLazyListState (telling it the position in the scrolling list) to be above the Scaffold.

If you are especially observant, you might have noticed we hoisted the ScalingLazyListState above the Scaffold already to hide or display the time, so, in our case, the work is already done.

Now that we've covered why the position indicator should be in the scaffold, let's add it into our app.

Find "positionIndicator = { }" and replace it with the code below:

Step 12

positionIndicator = {
    PositionIndicator(
        scalingLazyListState = listState
    )
}

This is pretty straightforward, the PositionIndicator requires the scrolling state to properly render itself, and now it can!

It also has a nice feature where it hides itself when the user isn't scrolling.

We're using the ScalingLazyListState but PositionIndicator takes many other scrolling options, e.g., ScrollState, LazyListState, and even can handle the rotating side button or

the rotating bezel. To see all the options, check out the KDocs for each version.

Ok, let's see what this looks like now:

8c278b78ab008cbb.png

Try scrolling it up and down. You should only see the scrolling indicator show up when you are scrolling.

Nice job, you have finished a UI demo of most of the Wear OS composables!

9. Congratulations

Congratulations! You learned the basics of using Compose on Wear OS!

Now you can reapply all your Compose knowledge to making beautiful Wear OS apps!

What's next?

Check out the other Wear OS codelabs:

Further reading

Feedback

The Developer Preview is your opportunity to influence the APIs, so please share your feedback here or join the #compose-wear channel on the Jetbrains' Slack and let us know there!

Happy coding!