Wear OS Compose Watch Face Development: Complete Guide

Wear OS Compose Watch Face Development

Wear OS Compose watch face development has been revolutionized by the Watch Face Format (WFF) and Jetpack Compose for Wear OS. Google’s new declarative approach replaces the legacy Canvas-based watch face API with an XML-based format that is more battery efficient, easier to build, and compatible with the Watch Face Studio visual editor. In 2026, WFF is the required format for new watch face submissions on Google Play.

This guide covers building watch faces from scratch using both the Watch Face Format for static/semi-dynamic faces and the Compose-based watch face service for fully custom rendering. You will learn how to add complications, handle user customization, optimize battery life, and publish to the Play Store.

Understanding Watch Face Formats

Google offers two approaches to watch face development in 2026, each suited for different complexity levels:

  • Watch Face Format (WFF) — XML-based declarative format. Best for most watch faces. Battery efficient because the system renders it without running your app’s code. Supports complications, animations, and user configuration.
  • WatchFaceService (Compose) — Code-based approach using Canvas or Compose. Required for truly custom rendering (particle effects, 3D, real-time data visualization). Uses more battery but offers unlimited creativity.

For 90% of watch faces, WFF is the right choice. Use the code-based approach only when WFF cannot express your design.

Wear OS Compose watch face design
Modern watch face designs built with Watch Face Format and Compose

Building with Watch Face Format

WFF watch faces are defined in XML and packaged as an APK with no Activity or Service. The system’s watch face renderer handles all drawing, which means your watch face consumes zero CPU while the screen is on.

<!-- res/raw/watchface.xml -->
<WatchFace width="450" height="450">

  <!-- Background -->
  <Scene>
    <Group name="background">
      <PartImage
        x="0" y="0" width="450" height="450"
        resourceId="@drawable/bg_dark" />
    </Group>

    <!-- Hour markers -->
    <Group name="hour_markers">
      <PartImage x="213" y="10" width="24" height="40"
        resourceId="@drawable/marker_12" />
      <PartImage x="400" y="213" width="40" height="24"
        resourceId="@drawable/marker_3"
        pivotX="0.5" pivotY="0.5" />
      <!-- ... other markers -->
    </Group>

    <!-- Digital time display -->
    <DigitalClock x="125" y="180" width="200" height="50">
      <TimeText format="HH:mm"
        align="CENTER"
        size="42"
        color="#FFFFFF"
        font="@font/roboto_mono_bold" />
    </DigitalClock>

    <!-- Date display -->
    <DigitalClock x="150" y="240" width="150" height="30">
      <TimeText format="EEE, MMM d"
        align="CENTER"
        size="16"
        color="#88FFFFFF"
        font="@font/roboto_regular" />
    </DigitalClock>

    <!-- Analog hands -->
    <AnalogClock x="225" y="225">
      <HourHand resourceId="@drawable/hand_hour"
        width="16" height="140"
        pivotX="0.5" pivotY="0.85" />
      <MinuteHand resourceId="@drawable/hand_minute"
        width="12" height="180"
        pivotX="0.5" pivotY="0.9" />
      <SecondHand resourceId="@drawable/hand_second"
        width="4" height="190"
        pivotX="0.5" pivotY="0.85"
        color="#FF4444" />
    </AnalogClock>

    <!-- Complication slots -->
    <ComplicationSlot x="125" y="315" width="80" height="80"
      slotId="1"
      supportedTypes="SHORT_TEXT ICON RANGED_VALUE"
      defaultProvider="com.google.android.wearable.provider.battery" />

    <ComplicationSlot x="245" y="315" width="80" height="80"
      slotId="2"
      supportedTypes="SHORT_TEXT ICON"
      defaultProvider="com.google.android.wearable.provider.steps" />
  </Scene>

  <!-- Ambient mode (always-on display) -->
  <AmbientScene>
    <Group name="ambient_bg">
      <PartImage x="0" y="0" width="450" height="450"
        resourceId="@drawable/bg_ambient" />
    </Group>
    <DigitalClock x="125" y="200" width="200" height="50">
      <TimeText format="HH:mm"
        align="CENTER" size="48"
        color="#AAAAAA"
        font="@font/roboto_mono_light" />
    </DigitalClock>
  </AmbientScene>

</WatchFace>

User Customization with Flavors

WFF supports user-configurable options through flavors. Users can change colors, toggle complications, or switch between analog and digital modes directly from the watch face settings.

<!-- Watch face configuration options -->
<UserConfiguration>
  <ColorOption id="accent_color"
    label="Accent Color"
    defaultValue="#4FC3F7">
    <ColorEntry value="#4FC3F7" label="Blue" />
    <ColorEntry value="#81C784" label="Green" />
    <ColorEntry value="#FFB74D" label="Orange" />
    <ColorEntry value="#F06292" label="Pink" />
    <ColorEntry value="#BA68C8" label="Purple" />
  </ColorOption>

  <BooleanOption id="show_seconds"
    label="Show Seconds"
    defaultValue="true" />

  <ListOption id="style"
    label="Watch Style"
    defaultValue="analog">
    <ListEntry value="analog" label="Analog" />
    <ListEntry value="digital" label="Digital" />
    <ListEntry value="hybrid" label="Hybrid" />
  </ListOption>
</UserConfiguration>

Code-Based Watch Face with Compose

For watch faces that need custom rendering — particle effects, real-time data visualization, or complex animations — use the Wear OS Compose watch face service approach with Canvas drawing:

class CustomWatchFaceService : WatchFaceService() {

    override suspend fun createWatchFace(
        surfaceHolder: SurfaceHolder,
        watchState: WatchState,
        complicationSlotsManager: ComplicationSlotsManager,
        currentUserStyleRepository: CurrentUserStyleRepository
    ): WatchFace {

        val renderer = CustomCanvasRenderer(
            surfaceHolder = surfaceHolder,
            watchState = watchState,
            complicationSlotsManager = complicationSlotsManager,
            currentUserStyleRepository = currentUserStyleRepository,
            canvasType = CanvasType.HARDWARE
        )

        return WatchFace(WatchFaceType.ANALOG, renderer)
    }
}

class CustomCanvasRenderer(
    surfaceHolder: SurfaceHolder,
    watchState: WatchState,
    complicationSlotsManager: ComplicationSlotsManager,
    currentUserStyleRepository: CurrentUserStyleRepository,
    canvasType: Int
) : Renderer.CanvasRenderer2(
    surfaceHolder, currentUserStyleRepository, watchState,
    canvasType, 16L, clearWithBackgroundTintBeforeRenderingHighlightLayer = true
) {

    private val hourPaint = Paint().apply {
        color = Color.WHITE
        strokeWidth = 8f
        strokeCap = Paint.Cap.ROUND
        isAntiAlias = true
    }

    private val minutePaint = Paint().apply {
        color = Color.WHITE
        strokeWidth = 5f
        strokeCap = Paint.Cap.ROUND
        isAntiAlias = true
    }

    override suspend fun createSharedAssets(): SharedAssets = object : SharedAssets {
        override fun onDestroy() {}
    }

    override fun render(
        canvas: Canvas,
        bounds: Rect,
        zonedDateTime: ZonedDateTime,
        sharedAssets: SharedAssets
    ) {
        val centerX = bounds.exactCenterX()
        val centerY = bounds.exactCenterY()

        // Draw background
        canvas.drawColor(Color.BLACK)

        // Calculate hand angles
        val hours = zonedDateTime.hour % 12
        val minutes = zonedDateTime.minute
        val seconds = zonedDateTime.second

        val hourAngle = (hours + minutes / 60f) * 30f
        val minuteAngle = (minutes + seconds / 60f) * 6f

        // Draw hour hand
        val hourLength = centerX * 0.5f
        drawHand(canvas, centerX, centerY, hourAngle, hourLength, hourPaint)

        // Draw minute hand
        val minuteLength = centerX * 0.75f
        drawHand(canvas, centerX, centerY, minuteAngle, minuteLength, minutePaint)

        // Draw complications
        for ((_, slot) in complicationSlotsManager.complicationSlots) {
            slot.render(canvas, zonedDateTime, renderParameters)
        }
    }

    private fun drawHand(
        canvas: Canvas, cx: Float, cy: Float,
        angle: Float, length: Float, paint: Paint
    ) {
        val radians = Math.toRadians((angle - 90).toDouble())
        val endX = cx + length * Math.cos(radians).toFloat()
        val endY = cy + length * Math.sin(radians).toFloat()
        canvas.drawLine(cx, cy, endX, endY, paint)
    }

    override fun renderHighlightLayer(
        canvas: Canvas, bounds: Rect,
        zonedDateTime: ZonedDateTime, sharedAssets: SharedAssets
    ) {
        canvas.drawColor(renderParameters.highlightLayer!!.backgroundTint)
    }
}
Smartwatch development and testing
Testing custom watch faces on Wear OS devices and emulators

Battery Optimization

Watch face battery impact is the number one reason for negative Play Store reviews. Follow these rules to minimize power consumption:

// Battery optimization best practices
class BatteryEfficientRenderer : Renderer.CanvasRenderer2(...) {

    override fun render(
        canvas: Canvas, bounds: Rect,
        zonedDateTime: ZonedDateTime, sharedAssets: SharedAssets
    ) {
        // 1. In ambient mode: no second hand, no animations,
        //    minimal colors, update once per minute
        if (renderParameters.drawMode == DrawMode.AMBIENT) {
            renderAmbient(canvas, bounds, zonedDateTime)
            return
        }

        // 2. Use hardware canvas (CanvasType.HARDWARE) not software
        // 3. Avoid allocating objects in render() — pre-create paints
        // 4. Skip second hand animation when wrist is down
        // 5. Cache bitmap draws, redraw only changed portions
        renderInteractive(canvas, bounds, zonedDateTime)
    }

    private fun renderAmbient(
        canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime
    ) {
        // Burn-in protection: shift content by a few pixels
        val burnInOffsetX = (zonedDateTime.minute % 5) - 2
        val burnInOffsetY = (zonedDateTime.minute % 7) - 3

        canvas.translate(burnInOffsetX.toFloat(), burnInOffsetY.toFloat())

        // Use outline-only rendering to minimize lit pixels on OLED
        canvas.drawColor(Color.BLACK)
        // Draw only hours and minutes, no seconds
    }
}

When NOT to Use Wear OS Watch Faces

Building a watch face is not justified if your goal is just to display app-specific data (steps, weather, calendar). Instead, build a complication data provider — it integrates with any existing watch face and takes significantly less development effort. Similarly, if your watch face concept requires constant network requests or sensor polling, it will drain the battery quickly and receive poor user reviews.

The Watch Face Format has limitations — if you need real-time physics simulations, 3D rendering, or camera integration, WFF cannot support these. However, building a code-based watch face for these features means your app uses considerably more battery. Consider whether the creative vision justifies the battery cost for end users.

Key Takeaways

  • Wear OS Compose watch face development offers two paths: WFF (XML, battery efficient) for most faces, and code-based Canvas rendering for custom effects
  • Watch Face Format is required for new Google Play submissions and renders without running your code, maximizing battery life
  • Complications let users add data from other apps to your watch face — always include 2-4 complication slots
  • Ambient mode must use minimal rendering: no second hand, outline fonts, burn-in protection pixel shifting
  • Battery optimization is critical — use hardware canvas, pre-allocate paints, and avoid object allocation in the render loop

Related Reading

External Resources

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top