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.
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)
}
} 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