Techbase Academy
Android · Lesson 59

Wear OS and App Widgets with Compose and Glance

13 phasesClass Companion: Watch and WidgetAndroid Studio
Phase 1 of 13
Lesson Introduction

Compose is not only for phone screens. The same ideas (composable functions, state, modifiers) also build interfaces for the wrist with Compose for Wear OS, and for the home screen with Jetpack Glance, which lets you write app widgets in a Compose-like style. This lesson covers the final items of Pathway 5 of the Jetpack Compose for Android developers programme (https://developer.android.com/courses/pathways/jetpack-compose-for-android-developers-5): Compose for Wear OS introduction, Wear OS with Compose and Compose and Glance for remote surfaces. The related codelab is Compose for Wear OS (https://developer.android.com/codelabs/compose-for-wear-os).

By the end you will be able to:

  • Explain how Wear OS design differs from phone design: tiny round screens, glanceable tasks, short sessions, battery.
  • Set up a Wear OS module and an emulator.
  • Build screens with androidx.wear.compose: ScalingLazyColumn (or the newer list components), Chip/Button, Text, TimeText, Scaffold.
  • Handle navigation with SwipeDismissableNavHost.
  • Understand Tiles and complications as Wear's "remote surfaces".
  • Build a phone app widget with Glance: GlanceAppWidget, receiver, provideGlance, GlanceTheme, actions, updates and widget metadata.
  • Explain why Glance is not ordinary Compose.

Big idea: every surface has its own rules. A watch face and a widget cannot show a full app, so each shows the one thing the user needs right now.

Honesty note: Wear OS and Glance libraries change quickly (for example, new list components and Material 3 for Wear). Version numbers below are placeholders; check the official documentation for the current APIs before you ship.

✏️ Your Task
Practise what you just learned about Lesson Introduction. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 2 of 13
Prerequisite Concepts
  • Composables, state and modifiers (lessons 15 to 19).
  • Lazy lists (lesson 21) and Navigation Compose (lessons 26 and 55).
  • ViewModel and Flows (lessons 25 and 33).
  • Manifest, services and broadcast receivers (lessons 3 and 42).
  • Emulators (lesson 4).
✏️ Your Task
Practise what you just learned about Prerequisite Concepts. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 3 of 13
Conceptual Understanding: Designing for Small Surfaces

The wrist

Constraint Consequence
Screen about 1.2 to 1.5 inches, often round Content near the edge is clipped; centre things
Glance time of seconds One primary action per screen
Battery Avoid constant work; use efficient background APIs
Input Touch, rotating crown or bezel, voice; no keyboard
Connectivity Often paired to a phone; may be standalone with its own network

Wear OS building blocks

Piece Purpose
App Activities with Compose for Wear OS
Tile A swipe-away card with at-a-glance info and quick actions
Complication A small data slot on a watch face (steps, next event)
Ongoing activity Shows a running workout or timer on the watch face

App widgets

An app widget is a small view on the phone's home screen (clock, weather, "next lesson"). It is displayed by the launcher, not by your app process, using RemoteViews, a limited serialisable description of views. This is why widgets cannot use arbitrary Compose.

Jetpack Glance gives you a Compose-style API (GlanceAppWidget, Column, Row, Text, Button, LazyColumn) that is translated to RemoteViews. You write familiar code, but the composables come from androidx.glance, not androidx.compose. Mixing them does not compile.

✏️ Your Task
Practise what you just learned about Conceptual Understanding: Designing for Small Surfaces. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 4 of 13
Step-by-Step Build 1: A Wear OS Project
  1. File → New → New Project → Wear OS → Empty Wear App (template name may vary).
  2. Create a Wear OS emulator (small round) from Device Manager. The wizard downloads a Wear OS system image.
  3. Look at the manifest: it declares uses-feature android:name="android.hardware.type.watch", and often a com.google.android.wearable.standalone meta-data flag for apps that work without a phone.

Dependencies (placeholders for versions):

libs.versions.toml
[versions]
wearCompose = "<latest stable>"
wearTiles = "<latest stable>"
[libraries]
androidx-wear-compose-foundation = { group = "androidx.wear.compose", name = "compose-foundation", version.ref = "wearCompose" }
androidx-wear-compose-material = { group = "androidx.wear.compose", name = "compose-material", version.ref = "wearCompose" }
androidx-wear-compose-navigation = { group = "androidx.wear.compose", name = "compose-navigation", version.ref = "wearCompose" }
androidx-wear-tiles = { group = "androidx.wear.tiles", name = "tiles", version.ref = "wearTiles" }

(Wear has its own Material component set: compose-material for Material 2 for Wear and a newer compose-material3 with Material 3 Expressive components. Newer templates use the latest; follow whichever your template generates, and check which is current.)

✏️ Your Task
Practise what you just learned about Step-by-Step Build 1: A Wear OS Project. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 5 of 13
Step-by-Step Build 2: Your First Wear Screen

A "Next Lesson" watch app for a school: show the next class and a button to mark attendance.

kotlin
package com.example.registerwear
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
import androidx.wear.compose.material.Chip
import androidx.wear.compose.material.ChipDefaults
import androidx.wear.compose.material.MaterialTheme
import androidx.wear.compose.material.PositionIndicator
import androidx.wear.compose.material.Scaffold
import androidx.wear.compose.material.Text
import androidx.wear.compose.material.TimeText
import androidx.wear.compose.material.Vignette
import androidx.wear.compose.material.VignettePosition
class MainActivity : ComponentActivity() {
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContent { WearApp() }
 }
}
@Composable
fun WearApp() {
 MaterialTheme {
 val listState = rememberScalingLazyListState()
 Scaffold(
 timeText = { TimeText() },
 vignette = { Vignette(vignettePosition = VignettePosition.TopAndBottom) },
 positionIndicator = { PositionIndicator(scalingLazyListState = listState) }
 ) {
 ScalingLazyColumn(
 modifier = Modifier.fillMaxSize(),
 state = listState,
 verticalArrangement = Arrangement.spacedBy(6.dp)
 ) {
 item { Text("Next class", textAlign = TextAlign.Center, style = MaterialTheme.typography.title3) }
 item { Text("Mathematics\n10:30 SS2 B", textAlign = TextAlign.Center) }
 item {
 Chip(
 onClick = { /* mark attendance */ },
 label = { Text("Mark attendance") },
 colors = ChipDefaults.primaryChipColors()
 )
 }
 }
 }
 }
}

(Add import androidx.compose.ui.unit.dp.) Things that differ from phone Compose:

  • The Scaffold is Wear's: it places the time text at the top, a vignette to blend edges, and a position indicator for scrolling.
  • ScalingLazyColumn is a list whose items scale and fade towards the curved edges, matching round screens. Newer Wear releases introduce TransformingLazyColumn with richer transformations; check which one the current guidance recommends for new code.
  • Chip and Button are Wear-sized, with large touch targets and high contrast. Do not use the phone's Material components on Wear unless the documentation says they are supported.
  • Text is short. If a label does not fit in about two lines, shorten it.

Rotary input

The crown or rotating bezel scrolls lists. ScalingLazyColumn supports it when it can take focus; in many versions you add Modifier.rotaryScrollable(...) or use the default behaviour. See the "rotary input" page for your version.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 2: Your First Wear Screen. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 6 of 13
Step-by-Step Build 3: Navigation on the Watch

The watch back gesture is a swipe from the left edge, so Wear provides SwipeDismissableNavHost.

kotlin
import androidx.wear.compose.navigation.SwipeDismissableNavHost
import androidx.wear.compose.navigation.composable
import androidx.wear.compose.navigation.rememberSwipeDismissableNavController
@Composable
fun WearNav() {
 val navController = rememberSwipeDismissableNavController()
 SwipeDismissableNavHost(navController = navController, startDestination = "home") {
 composable("home") {
 HomeScreen(onOpenClass = { id -> navController.navigate("class/$id") })
 }
 composable("class/{id}") { entry ->
 val id = entry.arguments?.getString("id")
 ClassScreen(id = id)
 }
 }
}

It uses the same NavController ideas as lesson 26 with a swipe-to-dismiss transition. (The string-route API shown here is the one in the Wear navigation library; check whether a type-safe version has been added.)

State and data

Use the same architecture as the phone: a ViewModel, a StateFlow, and collectAsStateWithLifecycle() (lesson 33). Keep the UI very simple: lists of three to five items, large touch targets, and a single clear action.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 3: Navigation on the Watch. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 7 of 13
Step-by-Step Build 4: Tiles and Complications (Overview)

A Tile is a card the user swipes to from the watch face. It is defined by a TileService and described with ProtoLayout elements (a layout format that the system renders), not with Compose UI directly:

kotlin
class NextClassTileService : TileService() {
 override fun onTileRequest(requestParams: RequestBuilders.TileRequest): ListenableFuture<TileBuilders.Tile> =
 Futures.immediateFuture(
 TileBuilders.Tile.Builder()
 .setResourcesVersion("1")
 .setTileTimeline(
 TimelineBuilders.Timeline.fromLayoutElement(
 LayoutElementBuilders.Text.Builder()
 .setText("Maths 10:30")
 .build()
 )
 )
 .build()
 )
 override fun onTileResourcesRequest(
 requestParams: RequestBuilders.ResourcesRequest
 ): ListenableFuture<ResourceBuilders.Resources> =
 Futures.immediateFuture(
 ResourceBuilders.Resources.Builder().setVersion("1").build()
 )
}

The exact builder APIs differ by Tiles library version (newer versions provide Material components and a kotlin-friendly layout DSL). Register the service in the manifest with the androidx.wear.tiles.action.BIND_TILE_PROVIDER intent filter and the BIND_TILE_PROVIDER permission. Key rules:

  • Tiles are rendered by the system, so you describe them; you cannot run arbitrary UI.
  • Keep them cheap: they refresh on request, not every second.
  • Use a freshness interval or push updates when data changes.

Complications are provided by a ComplicationDataSourceService that returns ComplicationData (short text, ranged value, icon) for the slot the watch face asks about.

Treat tiles and complications as the next topic to study after this lesson. The programme's Pathway 5 introduces them; the codelabs on developer.android.com go through each step.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 4: Tiles and Complications (Overview). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 8 of 13
Step-by-Step Build 5: Your First App Widget with Glance

Now move to the phone. A "Next class" widget on the home screen.

Dependencies

libs.versions.toml
[versions]
glance = "<latest stable>"
[libraries]
androidx-glance-appwidget = { group = "androidx.glance", name = "glance-appwidget", version.ref = "glance" }
androidx-glance-material3 = { group = "androidx.glance", name = "glance-material3", version.ref = "glance" }

The widget

kotlin
package com.example.registerwidget
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.action.actionStartActivity
import androidx.glance.action.clickable
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.layout.Alignment
import androidx.glance.layout.Column
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.padding
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
class NextClassWidget : GlanceAppWidget() {
 override suspend fun provideGlance(context: Context, id: GlanceId) {
 // load data here (suspending work is allowed)
 val next = "Mathematics, 10:30"
 provideContent {
 GlanceTheme {
 NextClassContent(next)
 }
 }
 }
}
@Composable
private fun NextClassContent(next: String) {
 Column(
 modifier = GlanceModifier
 .fillMaxSize()
 .background(GlanceTheme.colors.widgetBackground)
 .padding(12.dp)
 .clickable(actionStartActivity<MainActivity>()),
 verticalAlignment = Alignment.CenterVertically,
 horizontalAlignment = Alignment.CenterHorizontally
 ) {
 Text(
 "Next class",
 style = TextStyle(color = GlanceTheme.colors.onSurface, fontSize = 12.sp)
 )
 Text(
 next,
 style = TextStyle(
 color = GlanceTheme.colors.onSurface,
 fontSize = 18.sp,
 fontWeight = FontWeight.Bold
 )
 )
 }
}

Spot the differences from ordinary Compose:

  • GlanceModifier, not Modifier.
  • androidx.glance.layout.Column, Text, Button, not the Compose ones.
  • provideContent { } renders and keeps the widget updated while it is alive; provideGlance may do suspending work first.
  • Click handling uses actions (actionStartActivity, actionRunCallback), not lambdas that run in your app process.

The receiver

kotlin
import androidx.glance.appwidget.GlanceAppWidgetReceiver
class NextClassWidgetReceiver : GlanceAppWidgetReceiver() {
 override val glanceAppWidget: GlanceAppWidget = NextClassWidget()
}

The widget metadata

res/xml/next_class_widget_info.xml:

xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
 android:minWidth="180dp"
 android:minHeight="110dp"
 android:targetCellWidth="3"
 android:targetCellHeight="2"
 android:updatePeriodMillis="0"
 android:resizeMode="horizontal|vertical"
 android:widgetCategory="home_screen"
 android:description="@string/widget_description"
 android:initialLayout="@layout/glance_default_loading_layout" />

(glance_default_loading_layout comes with the Glance library. updatePeriodMillis="0" means "do not update on a timer"; periodic updates under 30 minutes are not honoured by the system anyway, so update from your own code or WorkManager.)

The manifest

xml
<receiver
 android:name=".NextClassWidgetReceiver"
 android:exported="true">
 <intent-filter>
 <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
 </intent-filter>
 <meta-data
 android:name="android.appwidget.provider"
 android:resource="@xml/next_class_widget_info" />
</receiver>

Install the app, long-press the home screen, choose Widgets, and drag Next class onto the screen.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 5: Your First App Widget with Glance. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 9 of 13
Step-by-Step Build 6: Interactive and Updating Widgets

Actions that run code

Widgets cannot call your composable lambdas. Use an ActionCallback:

kotlin
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.state.updateAppWidgetState
import androidx.glance.action.ActionParameters
import androidx.glance.action.actionParametersOf
import androidx.glance.action.ActionParameters.Key
import androidx.glance.appwidget.action.actionRunCallback
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.intPreferencesKey
private val CountKey = intPreferencesKey("present_count")
class MarkPresentCallback : ActionCallback {
 override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
 updateAppWidgetState(context, glanceId) { prefs: Preferences.Builder ->
 // Preferences.Builder variant may be MutablePreferences depending on the version
 }
 NextClassWidget().update(context, glanceId)
 }
}

The exact updateAppWidgetState lambda type depends on the Glance version (MutablePreferences in most). The pattern is stable: store small state in the widget's Preferences DataStore, then call update to re-run provideGlance.

Reading widget state in the content

kotlin
import androidx.glance.currentState
@Composable
private fun CounterContent() {
 val prefs = currentState<Preferences>()
 val count = prefs[CountKey] ?: 0
 Column {
 Text("Present: $count")
 Button(text = "Mark present", onClick = actionRunCallback<MarkPresentCallback>())
 }
}

Updating from elsewhere

kotlin
// From a ViewModel, Worker or repository after data changes:
NextClassWidget().updateAll(context)

For periodic refresh, schedule a WorkManager PeriodicWorkRequest (minimum 15 minutes) that fetches new data, stores it, and calls updateAll.

Responsive sizing

A widget can be resized. With SizeMode.Responsive, Glance gives you different layouts per size:

kotlin
private val Small = DpSize(120.dp, 90.dp)
private val Large = DpSize(250.dp, 150.dp)
class NextClassWidget : GlanceAppWidget() {
 override val sizeMode = SizeMode.Responsive(setOf(Small, Large))
 override suspend fun provideGlance(context: Context, id: GlanceId) {
 provideContent {
 val size = LocalSize.current
 if (size.width >= Large.width) WideLayout() else CompactLayout()
 }
 }
}

This is the adaptive thinking of lesson 58 applied to widgets.

Limits to remember

  • Glance has a subset of Compose: no arbitrary Canvas or custom layouts, limited modifiers and a few component types.
  • Widgets run in the launcher process as RemoteViews, so keep content light.
  • Images must be small: large bitmaps can exceed the RemoteViews memory limit and the widget fails to load.
  • Use GlanceTheme with Material 3 colours for dynamic colour on supported devices.
✏️ Your Task
Practise what you just learned about Step-by-Step Build 6: Interactive and Updating Widgets. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 10 of 13
Step-by-Step Build 7: Practicalities for Both Surfaces

Standalone or companion?

A Wear app can be standalone, working without a phone and using its own network access, or a companion that depends on a phone app for data. Standalone is friendlier because many watches are used without the phone nearby, but it means you must handle connectivity, storage and sign-in on a tiny screen. Many apps choose a hybrid: the watch caches data and syncs through the phone when available. Whichever you choose, declare it honestly in the manifest and design the first-run experience so that a user is never stuck on an empty screen.

Battery and background work

A watch battery is small. Avoid wake locks, polling loops and frequent network calls. Prefer WorkManager for deferrable work, Health Services for sensor and exercise data (instead of reading sensors yourself), and ongoing activities for long-running tasks the user should see. Test with the Battery Historian or the Profiler, and measure before you assume.

Testing on small screens

Test on both a round and a square emulator, with large font and bold text settings turned on in the watch settings. Text that fits at the default size often overflows at the largest size, and clipped text on a round face loses meaning. Use @WearPreviewDevices and @WearPreviewFontScales (from the Wear tooling library, where available) to preview several shapes and font scales at once.

Glance versus ordinary Compose

Aspect Compose UI Glance
Runs in Your app process The launcher, through RemoteViews
Modifier type Modifier GlanceModifier
Click handling Lambdas Actions and callbacks
Animation and custom drawing Full Not supported
Lists LazyColumn Glance LazyColumn (limited)
Update model Recomposition on state change Explicit update or updateAll calls

Remember that a widget is a promise of freshness: if it shows yesterday's timetable, the user stops trusting it. Decide up front who updates it (your app, a Worker, or a push message) and test that path by changing the data while the widget is on the home screen.

✏️ Your Task
Practise what you just learned about Step-by-Step Build 7: Practicalities for Both Surfaces. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 11 of 13
Guided Practice Exercises
🎯 Your Challenge

Exercise 1: Wear versus phone

List four changes you would make when adapting a phone "Class list" screen for a round watch.

(1) Use ScalingLazyColumn (or the current Wear list) instead of LazyColumn. (2) Use Wear Chip/Button components with large touch targets. (3) Shorten labels and show at most the next few items. (4) Add TimeText, a position indicator and use SwipeDismissableNavHost for back. Optional: support the crown and a Tile for the next class.

🎯 Your Challenge

Exercise 2: Spot the bug

Why does this widget code not compile?

kotlin
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
override suspend fun provideGlance(context: Context, id: GlanceId) {
 provideContent { Column { Text("Hi") } }
}

It imports ordinary Compose Column and Text. Glance needs its own: androidx.glance.layout.Column and androidx.glance.text.Text, with GlanceModifier for modifiers.

🎯 Your Challenge

Exercise 3: Update strategy

The widget should show fresh timetable data. Why not set updatePeriodMillis="60000", and what should you do instead?

The system does not honour periods shorter than about 30 minutes, and frequent updates waste battery. Instead update when data changes with updateAll(context), and use a WorkManager periodic job (15 minute minimum) for scheduled refreshes.

✏️ Your Task
Practise what you just learned about Guided Practice Exercises. Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 12 of 13
Common Beginner Mistakes (and How to Fix Them)

Mistake #1: Using phone Compose components on Wear. Fix: use the Wear component set.

Mistake #2: Putting content at the screen edge of a round display. Fix: use Scaffold, ScalingLazyColumn and padding.

Mistake #3: Importing Compose instead of Glance. Fix: check imports begin with androidx.glance.

Mistake #4: Doing network work in provideContent. Fix: load in provideGlance before provideContent, or in a Worker.

Mistake #5: Expecting lambdas to run in your app. Fix: use ActionCallbacks and actions.

Mistake #6: Large bitmaps in widgets. Fix: downscale or avoid.

Mistake #7: Frequent timed updates. Fix: event-driven updates and WorkManager.

Mistake #8: Forgetting to declare the receiver and metadata. Fix: manifest entry plus appwidget-provider XML.

Mistake #9: Testing only on a phone emulator. Fix: use a Wear emulator and several widget sizes.

Mistake #10: Treating alpha APIs as stable. Fix: read the release notes before upgrading.

✏️ Your Task
Practise what you just learned about Common Beginner Mistakes (and How to Fix Them). Open Android Studio, type the examples by hand, change a value, and observe what happens.
Phase 13 of 13
Knowledge Check

Question 1. Why does Wear use SwipeDismissableNavHost?

Back on a watch is a swipe from the left edge, and the host provides that swipe-to-dismiss behaviour.

Question 2. What is ScalingLazyColumn for?

A scrolling list that scales and fades items towards the curved edges of a round screen.

Question 3. What does Glance translate to?

RemoteViews, which the launcher renders.

Question 4. How do you run code when a widget button is tapped?

Use actionRunCallback<YourCallback>() with an ActionCallback.

Question 5. Name a safe way to refresh widget data.

A WorkManager job or event-driven code that calls updateAll(context).

Go deeper: official resources

✏️ Your Task
Practise what you just learned about Knowledge Check. Open Android Studio, type the examples by hand, change a value, and observe what happens.
🏗️ Build It — Mini Project
Class Companion: Watch and Widget

Project goal: Show the next class on a watch and on the home screen.

Requirements:

  1. A shared data source (a simple Kotlin object with a timetable list is fine).
  2. A Wear OS app with a ScalingLazyColumn timetable, a Mark attendance chip, and a detail screen using SwipeDismissableNavHost.
  3. A Glance widget that shows the next class, opens the app when tapped, and has a Mark present button using an ActionCallback.
  4. Widget layouts for at least two sizes with SizeMode.Responsive.
  5. A manifest receiver, metadata XML and a description string.
  6. A WorkManager job (or a simple call after data change) that refreshes the widget.
  7. Test on a Wear emulator and a phone, with the widget at two sizes.

Stretch goal: add a basic Tile that shows the same next-class text.

The shared data and a widget that reads it:

kotlin
data class ClassSlot(val subject: String, val time: String)
object Timetable {
 val slots = listOf(
 ClassSlot("Mathematics", "10:30"),
 ClassSlot("English", "11:30"),
 ClassSlot("Physics", "13:00")
 )
 fun next(): ClassSlot = slots.first()
}
class NextClassWidget : GlanceAppWidget() {
 override val sizeMode = SizeMode.Responsive(setOf(DpSize(120.dp, 90.dp), DpSize(250.dp, 150.dp)))
 override suspend fun provideGlance(context: Context, id: GlanceId) {
 val next = Timetable.next()
 provideContent {
 GlanceTheme {
 val wide = LocalSize.current.width >= 250.dp
 Column(
 GlanceModifier.fillMaxSize()
 .background(GlanceTheme.colors.widgetBackground)
 .padding(12.dp)
 .clickable(actionStartActivity<MainActivity>())
 ) {
 Text(next.subject, style = TextStyle(fontWeight = FontWeight.Bold, fontSize = 18.sp,
 color = GlanceTheme.colors.onSurface))
 Text(next.time, style = TextStyle(color = GlanceTheme.colors.onSurface))
 if (wide) {
 Button("Mark present", actionRunCallback<MarkPresentCallback>())
 }
 }
 }
 }
 }
}

On the watch, the same Timetable.slots feed the ScalingLazyColumn using items(Timetable.slots.size) { index -> Chip(...) }. Because both surfaces read one data source, the next step is moving it into a repository with Room, exactly as in lessons 34 and 62.

1

Open your finished project in Android Studio. Check that the .gitignore file the wizard created is in the project root. It keeps local.properties, build folders and keystores out of Git. Never commit passwords or signing keys.

2

Choose Git > Create Git Repository… (or VCS > Enable Version Control Integration in older versions), select the project folder, then open Git > Commit, tick every file, and write the message “Add Android lesson 59 project”.

3

On github.com click New repository, name it android-lesson-59-wear-os-and-app-widgets-with-compose-and-g, set it to Public, leave Add a README unticked, and click Create repository.

4

Back in Android Studio choose Git > Manage Remotes…, add the repository URL as origin, then choose Git > Push… and push your main branch. Prefer the terminal? Run git remote add origin <your-url> then git push -u origin main.

5

Add a README.md with a screenshot of your app running and a short description. Employers and clients read READMEs first. Your project is now part of your public portfolio. 🎉

Lesson 59 complete! 🎉

You covered:

  • Lesson Introduction
  • Prerequisite Concepts
  • Conceptual Understanding: Designing for Small Surfaces
  • Step-by-Step Build 1: A Wear OS Project
  • Step-by-Step Build 2: Your First Wear Screen
  • Step-by-Step Build 3: Navigation on the Watch
  • Step-by-Step Build 4: Tiles and Complications (Overview)
  • Step-by-Step Build 5: Your First App Widget with Glance
  • Step-by-Step Build 6: Interactive and Updating Widgets
  • Step-by-Step Build 7: Practicalities for Both Surfaces
  • Guided Practice Exercises
  • Common Beginner Mistakes (and How to Fix Them)
  • Knowledge Check
All 72 lessons in Google Android Studio ·Techbase Academy ·© 2025–2026 Techbase Consultant Services, Ibadan
Esc
↑ ↓ to moveEnter to openEsc to close