Building Wear OS Tiles with Compose

AndroidWear OSComposeTiles
Share on LinkedIn

Tiles occupy the swipe-left zone on Wear OS — a carousel of app cards users check without launching full apps. A transit tile showing the next two departures, a fitness tile with today's stats, a smart home tile with scene toggles. Before Compose for Tiles, you built layouts by constructing LayoutElement protobuf trees by hand. Now you write composable-style Kotlin that reads like the rest of your Wear codebase.

TileProviderService setup

class TransitTileService : TileService() {

    override fun onTileRequest(requestParams: RequestParams): ListenableFuture<Tile> {
        return Futures.immediateFuture(
            tile(requestParams.deviceConfiguration) {
                TransitTileContent(
                    departures = repository.cachedDepartures()
                )
            }
        )
    }

    override fun onResourcesRequest(requestParams: RequestParams): ListenableFuture<Resources> {
        return Futures.immediateFuture(resources { /* icons */ })
    }
}

Manifest registration:

<service
    android:name=".TransitTileService"
    android:exported="true"
    android:label="@string/tile_transit"
    android:permission="com.google.android.wearable.permission.BIND_TILE_PROVIDER">
    <intent-filter>
        <action android:name="androidx.wear.tiles.action.BIND_TILE_PROVIDER" />
    </intent-filter>
    <meta-data
        android:name="androidx.wear.tiles.PREVIEW"
        android:resource="@drawable/tile_transit_preview" />
</service>

Provide a preview drawable — users see it when adding tiles.

Compose for Tiles layout

fun TransitTileContent(departures: List<Departure>) {
    Column(
        modifier = Modifier.fillMaxWidth().padding(8.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text("Next trains", style = TileTypography.titleMedium)
        Spacer(Modifier.height(4.dp))
        departures.take(3).forEach { dep ->
            Row(
                modifier = Modifier.fillMaxWidth().clickable(
                    onClick = Clickable(onClick = launchDeparture(dep.id))
                ),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                Text(dep.line, style = TileTypography.bodyMedium)
                Text("${dep.minutesAway} min", style = TileTypography.bodySmall)
            }
        }
    }
}

Click actions use ActionBuilders to launch activities or fire requests:

private fun launchDeparture(id: String): Action {
    return ActionBuilders.LaunchAction.Builder()
        .setLaunchTarget(
            ActionBuilders.AndroidActivity.Builder()
                .setClassName(DepartureDetailActivity::class.java.name)
                .setPackageName(context.packageName)
                .build()
        )
        .build()
}

Data freshness

Tiles should render from cache synchronously in onTileRequest — no network calls on the binder thread:

override fun onTileRequest(requestParams: RequestParams): ListenableFuture<Tile> {
    val data = cache.getDepartures() // in-memory or DataStore snapshot
    if (data.isStale()) {
        scope.launch { repository.refreshDepartures() }
    }
    return Futures.immediateFuture(tile { TransitTileContent(data.items) })
}

Trigger refresh after background sync:

TileService.getUpdater(context, TransitTileService::class.java)
    .requestUpdate(TransitTileService::class.java)

Tile vs full app UI

Concern Full app (Compose) Tile
Layout depth Unlimited Keep flat, 2-3 levels
Interactions Full gesture set Clicks only
Refresh User-driven System-scheduled
Text length Flexible Truncate aggressively

Design tiles as summaries with a tap-through to detail screens. My rule: if it needs scrolling, it belongs in the app, not a tile.

Testing

Use the Wear OS Tile preview tool in Android Studio (Wear > Tile Preview). Unit test data mapping separately — tile rendering requires the Wear library runtime. Snapshot the proto output if you need regression coverage.

Pair tiles with complications for always-visible single metrics and tiles for richer multi-row summaries.

Tile lifecycle and system constraints

Wear OS manages tile lifecycle aggressively for battery:

Request updates sparingly:

// Good: after meaningful data change
fun onSyncComplete() {
    TileService.getUpdater(context, TransitTileService::class.java)
        .requestUpdate(TransitTileService::class.java)
}

// Bad: every minute via AlarmManager
// System will throttle; wastes battery; may get update requests ignored

Rule of thumb: request tile update when data meaningfully changes (new departure, workout completed), not on a fixed timer.

Multiple tiles per app

Apps can provide multiple tile services — one per logical summary:

<service android:name=".TransitTileService" ... />
<service android:name=".FitnessTileService" ... />

Each needs its own preview drawable and label. Users pick which tiles to add from the tile picker. Don't cram unrelated data into one tile — split by user mental model.

Handling empty and error states

Tiles must render something useful even without data:

fun TransitTileContent(departures: List<Departure>, state: TileDataState) {
    when (state) {
        TileDataState.Loading -> Text("Updating...")
        TileDataState.Empty -> Text("No upcoming departures")
        TileDataState.Error -> Text("Tap to refresh")
        TileDataState.Ready -> DepartureList(departures.take(3))
    }
}

Empty state with a tap action to launch the app is better than a blank tile. Error state should trigger refresh on tap, not just show static text.

Performance on watch hardware

Wear devices have limited CPU and memory:

Profile tile rendering with Android Studio's Wear Tile Preview — check layout timing on representative device configurations (round vs square, small vs large).

Failure modes

Production checklist

Wear OS 5+ adds Material 3 styling for tiles — align typography and color with your app's Material theme for visual consistency across phone and watch surfaces.

Resources

Frequently asked questions

What is the difference between a Tile and a complication?

Complications are small slots rendered inside a watch face by the face designer. Tiles are full swipeable cards in the tile carousel — left of the watch face — with richer layouts and multiple interactive elements. Tiles are better for multi-field summaries; complications for always-visible single metrics.

Can I use Compose to build Wear OS Tiles?

Yes. Compose for Tiles (androidx.wear.tiles: tiles-compose) lets you define tile layouts declaratively with Compose-style builders that compile to Tile proto layouts. This replaces hand-built LayoutElement trees for most use cases.

How do Tiles get refreshed?

Tiles refresh when the user swipes to them, on a system schedule, or when your app calls TileService.getUpdater().requestUpdate(). Avoid requesting updates more often than every few minutes unless data is time-critical — excessive refresh impacts battery.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →