26 August 2025
This post is part of Wear OS Spotlight Week. Today, we're focusing on creating engaging experiences across the various surfaces available on the wrist.
Put your app's unique information directly on a user's watch face by building your own complications. These are the small, glanceable details on a watch face, like step count, date, or weather, that are used to convey additional information, beyond simply telling the time.
Watches such as the recently-launched Pixel Watch 4 feature watch faces with as many as 8 complications. These small, powerful display elements are a great way to provide quick, valuable information and keep users connected to your app.
Let’s look at how you can build your own complication data sources, surfacing useful information to the user directly on their watch face, and helping drive engagement with your app.
In order to help understand complications, let’s first review some of the key architectural aspects of their design:
Complications are great for providing the user with bite-size data during the course of the day. Additionally, complications can provide a great launch point into your full app experience.
Complications Data source types (full list) include SHORT_TEXT and SMALL_IMAGE. Similarly, watch faces declare what types they can render.
For example, if you’re building an app which includes fitness goals, a good choice for a complication data source might be one that provides the GOAL_PROGRESS or RANGED_VALUE data types, to show progress toward that goal.
Conversely, complications are less appropriate for larger amounts of data, such as the contents of a chat message. They’re also not suitable for very frequent updates, such as real-time fitness metrics generated by your app.
Let’s look at creating a complication data source for that fitness goal mentioned above.
First, we create a service that extends SuspendingComplicationDataSourceService:
class MyDataSourceService : SuspendingComplicationDataSourceService() { override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData? { // Handle both GOAL_PROGRESS and RANGED_VALUE return when (request.complicationType) { ComplicationType.GOAL_PROGRESS -> goalProgressComplicationData() ComplicationType.RANGED_VALUE -> rangedValueComplicationData() else -> NoDataComplicationData() } } // Apps should override this so that watch face previews contain // complication data override fun getPreviewData(type: ComplicationType) = createPreviewData() }
To create the actual data to return, we create a ComplicationData object, shown here for GOAL_PROGRESS:
fun goalProgressComplicationData(): ComplicationData { val goalProgressText = PlainComplicationText .Builder("${goalProgressValue.toInt()} km") .build() return GoalProgressComplicationData.Builder( value = goalProgressValue, targetValue = goalTarget, contentDescription = goalProgressText ) // Set some additional optional data .setText(goalProgressText) .setTapAction(tapAction) .setMonochromaticImage(...) .build() }
Note: The GoalProgressComplicationData has numerous optional fields in addition to the mandatory ones. You should try to populate as many of these as you can.
Finally, add the data source to the manifest:
<service android:name=".WorkoutStatusDataSourceService" android:exported="true" android:directBootAware="true" android:label="@string/status_complication_label" android:permission="com.google.android.wearable.permission.BIND_COMPLICATION_PROVIDER"> <intent-filter> <action android:name="android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST" /> </intent-filter> <!-- Supported data types. Note that the preference order of the watch face, not the complication data source, decides which type will be chosen. --> <meta-data android:name="android.support.wearable.complications.SUPPORTED_TYPES" android:value="GOAL_PROGRESS,RANGED_VALUE" /> <meta-data android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS" android:value="300" /> </service>
Note: The use of the directBootAware attribute on the service lets the complication service run before the user has unlocked the device on boot.
Complications support both a push and a pull-style update mechanism. In the example above, UPDATE_PERIOD_SECONDS is set such that the data is refreshed every 5 minutes. Wear OS will check the updated value of the complication data source with that frequency.
This works well for a scenario such as a weather complication, but in other scenarios, it may make more sense for the updates to be driven by the app. To achieve this, you can:
Particularly for health-related complications, we can take advantage of platform data sources, to improve our goal progress complication. We can use these data sources with dynamic expressions to create complication content which is dynamically re-evaluated every second while the watch face is in interactive mode (that is, when it’s not in system ambient / always-on mode).
Let’s update the complication so that instead of just showing the distance, it shows a celebratory message when the target is reached. First we create a dynamic string as follows:
val distanceKm = PlatformHealthSources.dailyDistanceMeters().div(1000f) val formatter = DynamicBuilders.DynamicFloat.FloatFormatter.Builder() .setMaxFractionDigits(2) .setMinFractionDigits(0) .build() val goalProgressText = DynamicBuilders.DynamicString .onCondition(distanceKm.lt(distanceKmTarget)) .use( distanceKm .format(formatter) .concat(DynamicBuilders.DynamicString.constant(" km")) ) .elseUse( DynamicBuilders.DynamicString.constant("Success!") )
Then we include this text, and the dynamic value distanceKm, with the dynamic version of the complication builder.
In this way, the distance is updated every second, with no need for further requests to the data source. This means UPDATE_PERIOD_SECONDS can be set to a large value, saving battery, and the celebratory text is immediately shown the moment they pass their target!
For some data sources, it is useful to let the user configure what data should be shown. In the fitness goal example, consider that the user might have weekly, monthly, and yearly goals.
Adding a configuration activity allows them to select which goal should be shown by the complication. To do this, add the PROVIDER_CONFIG_ACTION metadata to your service definition, and implement an activity with a filter for this intent, for example:
<service android:name=".MyGoalDataSourceService" ...> <!-- ... --> <meta-data android:name="android.support.wearable.complications.PROVIDER_CONFIG_ACTION" android:value="com.myapp.MY_GOAL_CONFIG" /> </service> <activity android:name=".MyGoalConfigurationActivity" ...> <intent-filter> <action android:name="com.myapp.MY_GOAL_CONFIG" /> <category android:name="android.support.wearable.complications.category.PROVIDER_CONFIG" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
In the activity itself, the details of the complication being configured can be extracted from the intent:
// Keys defined on ComplicationDataSourceService // (-1 assigned when the ID or type was not available) val id = intent.getIntExtra(EXTRA_CONFIG_COMPLICATION_ID, -1) val type = intent.getIntExtra(EXTRA_CONFIG_COMPLICATION_TYPE, -1) val source = intent.getStringExtra(EXTRA_CONFIG_DATA_SOURCE_COMPONENT)
To indicate a successful configuration, the activity should set the result when exiting:
setResult(Activity.RESULT_OK) // Or RESULT_CANCELED to cancel configuration finish()
The ID is the same ID passed in ComplicationRequest to the complication data source service. The Activity should write any configuration to a data store, using the ID as a key, and the service can retrieve the appropriate configuration to determine what data to return in response to each onComplicationRequest().
In the example above, UPDATE_PERIOD_SECONDS is set at 5 minutes - this is the smallest value that can be set for the update period. Ideally this value should be set as large as is acceptable for the use case: This reduces requests and improves battery life.
Consider these examples:
This allows you to provide the series of events in advance, with no need for the watch face to request updates. The calendar data source would only need to push updates if a change is made, such as another event being scheduled for the day, offering timeliness and efficiency.
ComplicationDataTimeline requires a defaultComplicationData as well as the list of entries: This is used in the case where none of the timeline entries are valid for the current time. For example, for a calendar it could contain the text “No event” where the user has nothing booked. Where there are overlapping entries, the entry with the shortest interval is chosen.
override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationDataTimeline? { return ComplicationDataTimeline( // The default for when there is no event in the calendar defaultComplicationData = noEventComplicationData, // A list of calendar entries timelineEntries = listOf( TimelineEntry( validity = TimeInterval(event1.start, event1.end), complicationData = event1.complicationData ), TimelineEntry( validity = TimeInterval(event2.start, event2.end), complicationData = event2.complicationData ) ) ) }
For example, to create a countdown to the New Year:
TimeDifferenceComplicationText.Builder( TimeDifferenceStyle.SHORT_SINGLE_UNIT, CountDownTimeReference(newYearInstant) ) .setDisplayAsNow(true) .build()
This can be useful in the case where it is not possible to use a timeline but where data can become stale, allowing you to control the visibility of this data.
It can be very useful to track whether your complication is currently in use on the active watch face or not. This can help with:
This can be a useful signal to provide an educational moment in your phone or Wear OS app, drawing attention to this feature, and sharing potential benefits with the user that they may not be aware of.
To facilitate these use cases, override the appropriate methods in your complication service:
class MyDataSourceService() : SuspendingComplicationDataSourceService() { override fun onComplicationActivated(complicationInstanceId: Int, type: ComplicationType) { super.onComplicationActivated(complicationInstanceId, type) // Keep track of which complication has been enabled, and // start any necessary work such as registering periodic // WorkManager jobs } override fun onComplicationDeactivated(complicationInstanceId: Int) { super.onComplicationDeactivated(complicationInstanceId) // Complication instance has been disabled, so remove all // registered work }
Adding support to your data source for multiple types makes it most useful to the user. In the above example, we implemented both RANGED_VALUE and GOAL_PROGRESS, as both can be used to represent progress-type data.
Similarly, if you were to implement a calendar complication, you could use both SHORT_TEXT and LONG_TEXT to maximize compatibility with the available slots on the watch face.
Complications are a great way to elevate your app experience for users, and to differentiate your app from others.
Check out these resources for more information on creating complication data sources. We look forward to seeing what you can do.
Happy Coding!