Show periodic updates in tiles

Create tiles with content that changes as time passes.

Work with timelines

A timeline consists of one or more TimelineEntry instances, each of which contain a layout that is displayed during a specific time interval. All tiles need a timeline.

Diagram of tile timeline

Single-entry tiles

Often a tile can be described with a single TimelineEntry. The layout is fixed, and only the information inside the layout changes. For example, a tile that shows your fitness progress of the day always shows the same progress layout, though you might adjust that layout to show different values. In these cases, you don't know in advance when the content might change.

See the following example of a tile with a single TimelineEntry:

Kotlin

override fun onTileRequest(
    requestParams: TileRequest
): ListenableFuture<Tile> {
    val tile = Tile.Builder()
        .setResourcesVersion(RESOURCES_VERSION)

        // We add a single timeline entry when our layout is fixed, and
        // we don't know in advance when its contents might change.
        .setTileTimeline(
            Timeline.fromLayoutElement(...)
        ).build()
    return Futures.immediateFuture(tile)
}

Java

@Override
protected ListenableFuture<Tile> onTileRequest(
       @NonNull TileRequest requestParams
) {
   Tile tile = new Tile.Builder()
       .setResourcesVersion(RESOURCES_VERSION)
       
       // We add a single timeline entry when our layout is fixed, and
       // we don't know in advance when its contents might change.
       .setTileTimeline(
            Timeline.fromLayoutElement(...)
       ).build();
   return Futures.immediateFuture(tile);
}

Timebound timeline entries

A TimelineEntry can optionally define a validity period, allowing a tile to change its layout at a known time without requiring the app to push a new tile.

The canonical example is an agenda tile whose timeline contains a list of upcoming events. Each upcoming event contains a validity period to indicate when to show it.

The tiles API allows for overlapping validity periods, where the screen with the shortest period of time left is the one shown. Only one event is displayed at a time.

Developers can provide a default fallback entry. For example, the agenda tile could have a tile with an infinite validity period, which is used if no other timeline entry is valid, as shown in the following code sample:

Kotlin

public override fun onTileRequest(
    requestParams: TileRequest
): ListenableFuture<Tile> {
    val timeline = Timeline.Builder()

    // Add fallback "no meetings" entry
    // Use the version of TimelineEntry that's in androidx.wear.protolayout.
    timeline.addTimelineEntry(TimelineEntry.Builder()
        .setLayout(getNoMeetingsLayout())
        .build()
    )

    // Retrieve a list of scheduled meetings
    val meetings = MeetingsRepo.getMeetings()
    // Add a timeline entry for each meeting
    meetings.forEach { meeting ->
        timeline.addTimelineEntry(TimelineEntry.Builder()
            .setLayout(getMeetingLayout(meeting))
            .setValidity(
                // The tile should disappear when the meeting begins
                // Use the version of TimeInterval that's in
                // androidx.wear.protolayout.
                TimeInterval.Builder()
                    .setEndMillis(meeting.dateTimeMillis).build()
            ).build()
        )
    }

    val tile = Tile.Builder()
        .setResourcesVersion(RESOURCES_VERSION)
        .setTileTimeline(timeline.build())
        .build()
    return Futures.immediateFuture(tile)
}

Java

@Override
protected ListenableFuture<Tile> onTileRequest(
       @NonNull RequestBuilders.TileRequest requestParams
) {
   Timeline.Builder timeline = new Timeline.Builder();
   // Add fallback "no meetings" entry
   // Use the version of TimelineEntry that's in androidx.wear.protolayout.
   timeline.addTimelineEntry(new TimelineEntry.Builder().setLayout(getNoMeetingsLayout()).build());
   // Retrieve a list of scheduled meetings
   List<Meeting> meetings = MeetingsRepo.getMeetings();
   // Add a timeline entry for each meeting
   for(Meeting meeting : meetings) {
        timeline.addTimelineEntry(new TimelineEntry.Builder()
            .setLayout(getMeetingLayout(meeting))
            .setValidity(
                // The tile should disappear when the meeting begins
                // Use the version of TimeInterval that's in
                // androidx.wear.protolayout.
                new TimeInterval.builder()
                    .setEndMillis(meeting.getDateTimeMillis()).build()
            ).build()
        );
    }

    Tile tile = new Tile.Builder()
        .setResourcesVersion(RESOURCES_VERSION)
        .setTileTimeline(timeline.build())
        .build();
    return Futures.immediateFuture(tile);
}

Refresh a tile

Information shown on a tile might expire after some time. For example, a weather tile that shows the same temperature throughout the day isn't accurate.

To deal with expiring data, set a freshness interval at the time of creating a tile, which specifies how long the tile is valid. In the example of the weather tile, you might update its content every hour, as shown in the following code sample:

Kotlin

override fun onTileRequest(requestParams: RequestBuilders.TileRequest) =
    Futures.immediateFuture(Tile.Builder()
        .setResourcesVersion(RESOURCES_VERSION)
        .setFreshnessIntervalMillis(60 * 60 * 1000) // 60 minutes
        .setTileTimeline(Timeline.fromLayoutElement(
            getWeatherLayout())
        ).build()
    )

Java

@Override
protected ListenableFuture<Tile> onTileRequest(
       @NonNull TileRequest requestParams
) {
    return Futures.immediateFuture(new Tile.Builder()
        .setResourcesVersion(RESOURCES_VERSION)
        .setFreshnessIntervalMillis(60 * 60 * 1000) // 60 minutes
        .setTimeline(Timeline.fromLayoutElement(
            getWeatherLayout())
        ).build());
}

When you set a freshness interval, the system calls onTileRequest() shortly after the interval finishes. If you don't set a freshness interval, the system doesn't call onTileRequest().

A tile can also expire because of an external event. For example, a user might remove a meeting from their calendar, and if the tile wasn't refreshed, then the tile would still show that deleted meeting. In this case, request a refresh from any place in your application code, as shown in the following code sample:

Kotlin

fun eventDeletedCallback() {
     TileService.getUpdater(context)
             .requestUpdate(MyTileService::class.java)
}

Java

public void eventDeletedCallback() {
   TileService.getUpdater(context)
           .requestUpdate(MyTileService.class);
}

Choose an update workflow

Use these best practices to determine how to configure your tile updates:

  • If the update is predictable—for example, if it's for the next event in the user's calendar—use a timeline.
  • When you fetch platform data, use data binding so that the system updates the data automatically.
  • If the update can be calculated on-device in a small amount of time—such as updating the position of an image on a sunrise tile—use onTileRequest().

    This is particularly useful when you need to generate all images ahead of time. If you need to generate a new image at a future time, call setFreshnessIntervalMillis().

  • If you're doing more intensive background work repeatedly, such as polling for weather data, use WorkManager, and push updates to your tile.

  • If the update is in response to an external event—such as the lights turning on, receiving an email, or updating a note—send a Firebase Cloud Messaging (FCM) message to make your app active again, then push updates to the tile.

  • If the tile data sync process might be expensive, do the following:

    1. Schedule a data sync.
    2. Start a timer for 1-2 seconds.
    3. If you receive an update from a remote data source before time runs out, show the updated value from the data sync. Otherwise, show a cached local value.