[MUSIC PLAYING]
ATAUL MUNIM: Hi.
I'm Ataul, developer relations engineer on the Android team.
I work with Wear OS, primarily on tiles and watchfaces.
YURI SCHIMKE: And I'm Yuri, a software engineer
on the Wear developer team.
ATAUL MUNIM: With the recent and upcoming product changes
in Wear OS, you might be feeling like it's a lot of work
to build an entirely new app.
And it would be if you had to do that.
Luckily, in this talk, Yuri and I
will show you how you can leverage
your existing mobile projects to build a Wear app quickly
and efficiently.
Over the next 20 minutes, we'll show you
what we're trying to build, our end goal.
We'll demonstrate how well-modularized projects can
get us halfway there and demonstrate how
we plan to structure our codes.
Finally, we'll share some features
from Horologist library, which can
help us build the app quickly.
We'll use a made-up example, Generic Fitness,
to provide context about the best ways
that you can reuse the concepts, principles,
and code with which you're already familiar.
We won't dive into health and fitness APIs in this session.
For more on that, look out for "Creating Helpful Fitness
Experiences" with [? Brianna ?] and [? Garen. ?]
Our imaginary app is a very simple fitness tracker
with one idea.
It wants you to do 150 minutes of moderate exercise
every week.
It helps you do this by keeping track of your workouts
and reminding you when you're falling behind.
It's got two screens, one showing
a history and past activities and the other one for tracking
and exercise.
We learned from case studies on developer.android.com
that other companies had success increasing
engagement with their product by adding a Wear app.
So that's the reason we're interested in building one
for ourselves.
We want it to work on its own.
Our users shouldn't have to take their phone with them
when they want to go to the gym or for a run
unless they want to.
This means the app should be standalone,
with the watch app syncing activities with our back end
directly.
It doesn't need to talk to the mobile app.
And in fact, the user doesn't even need to have it installed.
We don't really want to start coding this from scratch
if we already have the mobile version.
And the good news is we don't have to.
The core functionality of the Generic Fitness app
should be the same on Wear OS--
track exercises and see your weekly progress.
So we should be able to reuse a lot of code
and focus mainly on the UI.
It is an app for Wear OS, though.
So it should support Wear OS-specific features
like tiles and complications, features
that don't exist in mobile.
A complication is any feature that's
displayed on a watch in addition to the time.
So here on the watch face, we've got four of them.
We've got the date at the top, the time
in New York on the right, the user's step count,
and finally, the weather.
One of the use cases for this Generic Fitness app
is to show the user's progress throughout the week.
And complications work really well for this.
We just have to provide the data.
The watch face will render the UI,
and we don't have to write it ourselves.
Now the user can see how much they've achieved of their goal.
And tapping it will open the app,
where they can see more detail.
Adding a tile will also be a relatively low-effort win.
Tiles present information from one app at a time
and live alongside the watch face, where each swipe will
show a different tile.
For Generic Fitness, we could add
one which lets the user start a workout quickly or see
the weekly progress in more detail at a glance.
For us, it's not a lot of effort to create.
But it's something that users who like our app
will want to see.
And it's definitely something that they'll come to expect.
In the next section, we'll take a look
at how the Generic Fitness mobile app is currently
structured and how this lends itself where--
how this lends itself well to adding a Wear app
that reuses a bunch of codes.
Yuri, tell us about modularization.
YURI SCHIMKE: The guide to Android app modularization
is equally applicable to Wear.
The goal is still the same--
split our project into loosely-coupled, self-contained
modules.
We'll take a look at what this means in practice
and why it makes sense for Wear as well.
But first, let's remind ourselves about our north star.
What do we want to achieve with our modularization strategy?
A well-modularized app has lots of benefits,
including the ones shown here--
scalability, encapsulation, reusability.
The reusability aspect is, however, doubly true
for Wear apps.
If we have a single module for Wear
or even a single module for mobile,
we can't take advantage of the code that's
already been written.
Reusing the code here will lead to a consistent experience
for the user and will reduce the risk of adding new bugs.
In your project today, you'll have a top-level app module.
It contains scaffolding classes that
bind the rest of the code base, such as main activity, app
class, top-level navigation.
This depends on UI modules, which
usually correspond to a screen or a collection
of closely-related screens.
UI modules depend on data modules.
Data modules aren't tied to a particular screen.
Instead, they encapsulate all the data
and business logic for a particular domain or data type.
For example, here, the overview module
depends on both the historical and current exercise modules
because it needs to display both for the overview screen.
One benefit to this kind of structure
is that you can use underlying modules as building blocks
to create entirely new apps.
Here, we added a new module, exercise-demo,
represented by this field box.
Exercise-demo can demo just the exercise screen in isolation.
Everything else already existed.
The result is the ability to build apps
without having to duplicate code unnecessarily and, crucially,
only contain the code that you need.
For Wear, we recommend the exact same approach.
Only add the code that you need.
So here we'll add a new app module for Wear.
We also have to add new UI modules.
Compose for Wear OS includes libraries which
are optimized for the wrist.
So we can't reuse the ones from mobile.
And in any case, the screens are going to be different.
The new UI modules can use the existing data modules
because those are UI-agnostic.
And if we had a domain layer, which
can make it easier for the UI modules to access data,
we could also share that between the mobile and Wear apps.
There's no need to make it device-specific.
What we end up with is a Wear app writing relatively little
code that's specific to Wear.
And now Ataul will look at how we
can layer our app according to modern Android development
guidelines.
ATAUL MUNIM: If you've watched the MAD Skills video
series in architecture, the next few sections
might seem familiar.
And that's good.
It's because the guidance is pretty much exactly the same.
To avoid any confusion, though, I
do want to highlight that there is already an API on Wear
called the wearable data layer.
It's used for direct communication
between the watch and the phone, but we're not
referencing that in this talk.
We want our Wear app to be completely standalone.
So any syncing between devices needs
to happen via the cloud using the internet.
And all of this should happen using the data layer
in your codebase.
So this data layer is the one where
your application data lives.
It's also where you're going to have
the logic that determines how that data is fetched, stored,
and updated.
The entry point to the data layer is the repository.
It's our interface to data sources, each of which
work with a single source of data--
for example, a local Room database or a remote server.
Let's take a look at the Generic Fitness mobile app
and see what the data structure looks like there.
We saw before that the mobile app has two main screens.
This one gives an overview of sessions
that you've already done.
The other tab gives more detail about individual sessions.
The data for the screen is accessible
via the historical exercise repository.
In this vertical slice of the Generic Fitness app,
the repository is the entry point for the data layer.
And it's through this repository that all the data operations
will take place.
It's backed by two data sources--
the database, which is our source of truth, and the remote
API so that we can sync completed sessions.
The other screen in the fitness app
is shown when you're currently tracking an exercise.
It shows the user's heart rate and the elapsed time
for the session.
The data that we track during an exercise
is significantly different from what
we keep for a past exercise.
So this is handled via the current exercise repository.
It's backed by the Jetpack data store
object, which stores metadata and metrics
about the current exercise.
And the heart rate data for the mobile app
comes from a Bluetooth heart rate monitor,
like one of those external chest straps.
We create an Android service that
has access to both the repository and the sensor data.
The service ingests data from the sensor
and can persist it via the repository.
This lets our UI subscribe to your metrics about the exercise
without having to deal with the stream of data
from sensors itself.
And we should be able to use almost all
of these classes for the Wear app.
But what changes?
So instead of an external heart rate monitor,
we can use sensors built into the device via Wear Health
Services, a platform-level library that
can manage health metrics in a power-efficient way.
Otherwise, the architecture is exactly the same,
and the code is reusable.
And our UI, on the watch this time,
will still subscribe to changes from the repository,
as it does in the mobile app.
And it will read from the data store
that's saved on the watch.
For tiles and complications there, we
need a different solution.
Like widgets on mobile, tiles and complications
are periodically refreshed at intervals that you can specify.
We want them to be up to date when
an exercise is being performed.
But we don't want to set the refresh period too short
and wake our app unnecessarily.
There's APIs we can use to request updates
to tiles and complications so we can call these APIs whenever
there's new data.
We made a class called Surface Updater that knows
how to update these surfaces.
Whenever it's told that there's new data,
the Surface Updater can choose whether to request a refresh.
Here's what it looks like.
onExerciseEvent is the public function
that's called whenever there's new data.
This class can decide whether to ignore it
or not-- for example, if one was recently requested.
For complications, we can create and update request
using the app context and the complication class name.
And for tiles, there's a similar API as well.
Other screens in our app are simpler
and don't need anything specific for Wear.
But it could still be worth refactoring
to avoid duplicating code.
In the next section, Yuri will describe
how we can use domain-level classes to combine data
from multiple repositories.
YURI SCHIMKE: The domain layer is
described as an optional layer in the Modern Android
Development Architecture Guide.
It sits between the UI and the data
and is responsible for encapsulating
business logic that's either complex or reused
by multiple ViewModels.
Including a domain layer can help us avoid duplicate logic.
In the Generic Fitness Wear app, we
wanted to display a chart showing the past week's
progress.
We also have a tile that shows the same chart
with the same underlying data and a complication and a tile
that each show the number of minutes
exercise for the current week.
All of these are representing the same data
in different ways.
But we don't want to duplicate the logic fetching
in each of the ViewModels.
So we have two options--
push it down into the data layer, into the repository,
or extract the logic into a new class.
The problem with moving the logic into the repository
is that the data we need comes from two
different repositories.
We want the historical data from the last seven days
and data from the user's current exercise,
because we want to show the most up-to-date information.
So for us, extracting the logic into a class
makes sense, the GetWeeklyProgressUseCase.
Now, each one of the ViewModels for the screens we showed just
becomes easier to understand and test
because they only have to interact with this one class.
The use case itself has a single responsibility,
to collate the weekly progress.
It returns the flow of WeeklyProgressReport,
which contains the information needed for us to render the UI.
It emits a new WeeklyProgressReport
whenever the current activity or historical activity changes.
We first fetch all historical sessions
and combine it with the current session, if there is one.
Now we have a single class that contains the logic
to provide data not just to the four
different screens in the Wear app, but also the mobile app.
Do we need to introduce a use case?
No.
When we just had the mobile app, it
wasn't worth extracting the logic from the ViewModel,
and it worked just fine.
But adding it helped us avoid copying
that code for more times.
So it made sense to refactor at this stage.
Most of the new code you'll write for Wear OS
will likely be UI.
We can share most of the data and domain layers.
But since your user journeys in UI code
will be different depending on form factor,
it's the one thing we really can't avoid.
Since that's the case, let's focus
on making it as easy as possible to develop and test.
Apps for Wear OS can be written using Jetpack Compose.
And just like Compose on mobile, the same best practices apply.
For this screen, we're using a stateful composable
that takes a ViewModel as a parameter
and remembers its state inside the function.
It delegates to a stateless composable, also
called WeeklyProgressScreen, where all the state
is passed in explicitly.
This one doesn't have a reference to the ViewModel,
and it doesn't maintain any state
using remember constructs.
Instead, it uses event handlers and state objects, which
means it can be easily tested.
Having a stateless composable means
we can use it for previews in Android Studio
and screenshot testing, too.
And now Ataul to talk about Horologist.
ATAUL MUNIM: Horologist is a group
of libraries that makes it easier to develop for Wear OS.
It's got a toolkit for media which Yuri and [? Chiara ?]
will present later in "Building Media Apps for Wear OS."
And it's also got support for high quality,
pre-built composables.
There's also several tools that facilitate code reuse
within your Wear apps, tiles, and complications.
In the last section, Yuri touched
on the difference between stateful and stateless
composables.
We prefer composables that don't maintain their own state,
because we can use them for previews and screenshot
testing.
Even the tiles and complications that don't use Compose,
we can still apply the same principle
using helpers from Horologist.
Let's use tiles as an example.
The Jetpack Tiles library provides TileService.
It's the entry point for developing a tile,
and it's what the sys UI will use to request your tile layout
and your tile resources for example images.
SuspendingTileService a coroutines-friendly wrapper
from Horologist that sits on top of TileService.
It's not necessary to build a tile.
But if you prefer coroutines over ListenableFutures,
it's handy.
And our tile, WeeklyProgressTileService,
is the concrete implementation.
Horologist introduces the concept
of a tile layout renderer.
Our concrete tile service delegates to its renderer,
allowing us to move codes out of what's essentially an Android
service to another class that's completely synchronous,
which means it's easy to test.
TileLayoutRenderer isn't intrinsically complicated.
It just has synchronous versions of the TileService functions
with all state passes explicit parameters.
The renderer is typed.
You have to specify the types for both the layout
state and the resource state.
In this case, they're both the same.
We need the WeeklyProgressReport state object
to build and bind the layout.
But we also need it to generate the image resources.
Notice how the renderTile function
takes the state as a parameter and returns a layout.
It doesn't manage its own state.
This means it's easy to test, but we can also
use it to generate previews in Android Studio.
Previewing tiles for Wear OS used to be a bit of a pain.
You can run your title on an emulator or device,
installing the app, adding the tiles to the carousel,
and swiping to it.
The Jetpack Tiles library does include
a tool which you can use to preview your tiles
within the Android view system.
This meant that you could have a debug activity that showed you
a tile without having to manually add it
to the carousel, which was better.
Android Studio stable now has direct surface launching.
So you don't even need that anymore.
Instead of launching a debug activity,
you can launch your tile directly,
and it'll install it to the carousel for you.
With Horologist, though, you can preview your trial
in Android Studio without launching it.
It uses the tool from Jetpack Tiles and the Android view
composable to wrap the tile layout.
So even though tiles aren't built with Compose,
we can still leverage the tooling that's
available to preview them.
And because we have the TileLayoutRenderer abstraction,
we can just pass the state, and it
will render in Android Studio.
The chart, though, is another issue.
How do you draw it?
Neither Jetpack Tiles nor Tiles Material
includes any Canvas drawing APIs.
But the same chart is used in the app where
we use Compose for Wear OS.
So can we reuse code here?
Absolutely, we can.
So the chart itself can be drawn using the--
by creating a DrawScope extension function,
weeklyProgressChart, passing the WeeklyProgressReport as state.
In the app, we can pass that DrawScope extension
to the Canvas composable.
And of course, we can preview it as well.
But what about the tile?
So I said before that tiles don't support Compose.
So how do we get the chart there?
We cheat, of course.
So Horologist includes an API that
takes the drawScope lambda, creates a bitmap, draws to it,
and finally converts that bitmap to an image resource, which
is the format that works with tiles.
So while in our app, we pass the WeeklyProgressChart function
to a Canvas composable.
In our tile renderer, we can use the same function
to create an image resource.
So we covered a lot in the last 20 minutes.
So let's recap.
Starting with modularization, we recommend
treating your Wear and mobile app
modules as hierarchical siblings and share underlying modules.
Then we reiterated that the MAD Architecture Guidance
applies to Wear apps as well.
If you've been following along with modern Android
development, you can use everything
that you've learned so far.
Finally, we introduced some of the tools
available in Horologist that can help
you build using the concepts and principles that we share today.
Today's overview has hopefully reassured you
that building apps for Wear OS is a lot more familiar than it
might have seen from the onset.
If you're going to take away one thing from this session,
let it be building blocks.
Keep that in mind as you think about existing or new projects
and how you can modularize your codebase
to facilitate reuse of code, not just across features,
but across different form factors.
Thank you.
[APPLAUSE]
[MUSIC PLAYING]