Tiles

Tiles provide easy access to the information and actions users need in order to get things done. With a simple swipe from the watch face, a user can find out the latest forecast or start a timer.

tiles swiping from time to weather to timer.

Users can choose what Tiles they’d like to see. There are Tiles for checking the weather, setting a timer, tracking daily fitness progress, quick-starting a workout, playing a song, scanning upcoming meetings, and sending a message to a favorite contact.

Examples of Tiles next to each other.

The Tiles API allows developers to build custom Tiles that users can include on their watch. Using the Tiles API requires targeting API level 26 or higher.

For more information, read the Tiles blog post.

Best practices

Working with Tiles requires some specific considerations:

  • While the OS handles rendering the UI of the Tile, you provide the layout, information, and resources using the TileService.
  • Tiles are meant for glanceable information that users can read in a matter of seconds. Display only the most important content with clear information hierarchy.
  • To safeguard the user’s battery, avoid elements that require frequent re-rendering.
  • Save highly interactive experiences for your activities. However, you can link into those activities from your Tile.
  • Avoid text like “x minutes ago” or "in x minutes" for past or future events as this requires frequent updates. Instead, display the actual start or end time or use a statement like "in the past."
  • Avoid long-running asynchronous work when providing a Tile’s layout and/or resources. Your Tile code should execute quickly.
  • Consider allowing the user to tap on Tiles to learn more and take action in an overlay, where there is support for rich interactivity and the user can scroll for more information.
  • If you have a large app that supports the user with multiple tasks, consider creating a Tile for each task. For example a fitness app might have a Goals Tile, and a Workout Activity Tile.

Getting started

Setup

To start providing Tiles from your app, include the following dependencies in your app's build.gradle file.

Groovy

dependencies {
    // Use to implement support for wear tiles
    implementation "androidx.wear.tiles:tiles:1.0.0-rc01"

    // Use to preview wear tiles in your own app
    debugImplementation "androidx.wear.tiles:tiles-renderer:1.0.0-rc01"
}

Kotlin

dependencies {
    // Use to implement support for wear tiles
    implementation("androidx.wear.tiles:tiles:1.0.0-rc01")

    // Use to preview wear tiles in your own app
    debugImplementation("androidx.wear.tiles:tiles-renderer:1.0.0-rc01")
}

Create a Tile

To provide a Tile from your application, create a class that extends TileService and implement the methods, as shown in the following code sample:

Kotlin

private val RESOURCES_VERSION = "1"
class MyTileService : TileService() {
    override fun onTileRequest(requestParams: RequestBuilders.TileRequest) =
        Futures.immediateFuture(Tile.Builder()
            .setResourcesVersion(RESOURCES_VERSION)
            .setTimeline(Timeline.Builder().addTimelineEntry(
                TimelineEntry.Builder().setLayout(
                    Layout.Builder().setRoot(
                        Text.Builder().setText("Hello world!").build()
                    ).build()
                ).build()
            ).build()
        ).build())

    override fun onResourcesRequest(requestParams: ResourcesRequest) =
        Futures.immediateFuture(Resources.Builder()
            .setVersion(RESOURCES_VERSION)
            .build()
        )
}

Java

public class MyTileService extends TileService {
    private static final String RESOURCES_VERSION = "1";

    @NonNull
    @Override
    protected ListenableFuture<Tile> onTileRequest(
        @NonNull TileRequest requestParams
    ) {
        return Futures.immediateFuture(new Tile.Builder()
            .setResourcesVersion(RESOURCES_VERSION)
            .setTimeline(new Timeline.Builder()
                .addTimelineEntry(new TimelineEntry.Builder()
                    .setLayout(new Layout.Builder()
                        .setRoot(new Text.Builder()
                            .setText("Hello world!").build()
                        ).build()
                    ).build()
                ).build()
            ).build()
        ).build();
   }

   @NonNull
   @Override
   protected ListenableFuture<Resources> onResourcesRequest(
       @NonNull ResourcesRequest requestParams
   ) {
       return Futures.immediateFuture(new Resources.Builder()
               .setVersion(RESOURCES_VERSION)
               .build()
       );
   }
}

Next, add a service inside the <application> tag of your AndroidManifest.xml.

<service
   android:name=".MyTileService"
   android:label="@string/tile_label"
   android:description="@string/tile_description"
   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_preview" />
</service>

The permission and intent filter register this service as a Tile provider.

The icon, label, and description is shown to the user when they configure Tiles on their phone or watch.

Use the preview meta-data tag to show a preview of the Tile when configuring it on your phone.

Preview the Tile in your app

The wear-tiles-renderer library provides a way to preview Tiles in an activity within your app.

To preview your Tile, create an activity that uses the renderer library to render the Tile. Add this activity in src/debug instead of src/main, as you’ll use this activity only for debugging purposes. See the following code sample for an example:

Kotlin

class MainActivity : ComponentActivity() {
    private lateinit var tileUiClient: TileUiClient

    override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         val rootLayout = findViewById<FrameLayout>(R.id.tile_container)
         tileUiClient = TileUiClient(
                 context = this,
                 component = ComponentName(this, MyTileService::class.java),
                 parentView = rootLayout
         )
         tileUiClient.connect()
    }

    override fun onDestroy() {
         super.onDestroy()
         tileUiClient.close()
    }
}

Java

public class MainActivity extends ComponentActivity {
   private TileUiClient mTileUiClient;

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       FrameLayout rootLayout = findViewById(R.id.tile_container);
       mTileUiClient = new TileUiClient(
           this,
           new ComponentName(this, MyTileService.class),
           rootLayout
       );
       mTileUiClient.connect();
   }

   @Override
   protected void onDestroy() {
       super.onDestroy();
       mTileUiClient.close();
   }
}

Create a layout file at activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   android:id="@+id/tile_container"
   android:layout_width="match_parent"
   android:layout_height="match_parent" />

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)
        .setTimeline(Timeline.Builder()
            // We add a single timeline entry when our layout is fixed, and
            // we don't know in advance when its contents might change.
            .addTimelineEntry(TimelineEntry.Builder()
                // .setLayout(...).build()
            ).build()
        ).build()
    return Futures.immediateFuture(tile)
}

Java

@Override
protected ListenableFuture<Tile> onTileRequest(
       @NonNull TileRequest requestParams
) {
   Tile Tile = new Tile.Builder()
       .setResourcesVersion(RESOURCES_VERSION)
       .setTimeline(
           new Timeline.Builder()
               // We add a single timeline entry when our layout is fixed, and
               // we don't know in advance when its contents might change.
               .addTimelineEntry(new TimelineEntry.Builder()
                   // .setLayout(...).build()
               ).build()
       ).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 future events. Each future 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
    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
                TimeInterval.Builder()
                    .setEndMillis(meeting.dateTimeMillis).build()
            ).build()
        )
    }

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

Java

@Override
protected ListenableFuture<Tile> onTileRequest(
       @NonNull TileRequest requestParams
) {
   Timeline.Builder timeline = new Timeline.Builder();
   // Add fallback "no meetings" entry
   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
                new TimeInterval.builder()
                    .setEndMillis(meeting.getDateTimeMillis()).build()
            ).build()
        );
    }

    Tile Tile = new Tile.Builder()
        .setResourcesVersion(RESOURCES_VERSION)
        .setTimeline(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, we might update its content once an 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
        .setTimeline(Timeline.Builder()
            .addTimelineEntry(TimelineEntry.Builder()
                .setLayout(getWeatherLayout())
                .build()
            ).build()
        ).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(new Timeline.Builder()
            .addTimelineEntry(new TimelineEntry.Builder()
                .setLayout(getWeatherLayout())
                .build()
            ).build()
        ).build());
}

When you set a freshness interval, the system calls onTileRequest() soon 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);
}

Design layouts

The layout of a Tile is written using a builder pattern. A Tile’s layout is built up like a tree that consists of layout containers and basic layout elements. Each layout element has properties, which you can set through various setter methods.

Basic layout elements

The following visual elements are supported:

  • Text: renders a string of text, optionally wrapping.
  • Image: renders an image.
  • Spacer: provides padding between elements or can act as a divider when you set its background color.

Layout containers

The following containers are supported:

  • Row: lays child elements out horizontally, one after another.
  • Column: lays child elements out vertically, one after another.
  • Box: overlays child elements on top of one another.
  • Arc: lays child elements out in a circle.
  • Spannable: applies specific FontStyles to sections of text along with interleaving text and images. For more information, see Spannables.

    Every container can contain one or more children, which themselves can also be containers. For example, a Column can contain multiple Row elements as children, resulting in a grid-like layout.

    As an example, a Tile with a container layout and two child layout elements could look like this:

    Kotlin

    private fun myLayout(): LayoutElement =
        Row.Builder()
            .setWidth(wrap())
            .setHeight(expand())
            .setVerticalAlignment(VALIGN_BOTTOM)
            .addContent(Text.Builder()
                .setText("Hello world")
                .build()
            )
            .addContent(Image.Builder()
                .setResourceId("image_id")
                .setWidth(dp(24f))
                .setHeight(dp(24f))
                .build()
            ).build()
    

    Java

    private LayoutElement myLayout() {
        return new Row.Builder()
            .setWidth(wrap())
            .setHeight(expand())
            .setVerticalAlignment(VALIGN_BOTTOM)
            .addContent(new Text.Builder()
                .setText("Hello world")
                .build()
            )
            .addContent(new Image.Builder()
                .setResourceId("image_id")
                .setWidth(dp(24f))
                .setHeight(dp(24f))
                .build()
            ).build();
    }
    

    Arcs

    The following Arc container children are supported:

    • ArcLine: renders a curved line around the Arc.
    • ArcText: renders curved text in the Arc.
    • ArcAdapter: renders a basic layout element in the arc, drawn at a tangent to the arc.

    Note: While an ArcText draws curved text around the Arc, using a Text in an ArcAdapter draws linear text at a tangent to the arc.

    For more information, see the reference documentation for each of the element types.

    Modifiers

    Every available layout element can optionally have modifiers applied to it. Use these modifiers for the following purposes:

    • Change the visual appearance of the layout. For example, add a background, border, or padding to your layout element.
    • Add metadata about the layout. For example, add a semantics modifier to your layout element for use with screen readers.
    • Add functionality. For example, add a clickable modifier to your layout element to make your Tile interactive. For more information, see Interact with the Tile.

    For example, we can customize the default look and metadata of an Image, as shown in the following code sample:

    Kotlin

    private fun myImage(): LayoutElement =
        Image.Builder()
            .setWidth(dp(24f))
            .setHeight(dp(24f))
            .setResourceId("image_id")
            .setModifiers(Modifiers.Builder()
                .setBackground(Background.Builder().setColor(argb(0xFFFF0000)).build())
                .setPadding(Padding.Builder().setStart(dp(12f)).build())
                .setSemantics(Semantics.builder()
                    .setContentDescription("Image description")
                    .build()
                ).build()
            ).build()
    

    Java

    private LayoutElement myImage() {
       return new Image.Builder()
               .setWidth(dp(24f))
               .setHeight(dp(24f))
               .setResourceId("image_id")
               .setModifiers(new Modifiers.Builder()
                       .setBackground(new Background.Builder().setColor(argb(0xFFFF0000)).build())
                       .setPadding(new Padding.Builder().setStart(dp(12f)).build())
                       .setSemantics(new Semantics.Builder()
                               .setContentDescription("Image description")
                               .build()
                       ).build()
               ).build();
    }
    

    Spannables

    A Spannable is a special type of container that lays out elements similarly to text. This is useful when you want to apply a different style to only one substring in a larger block of text, something that isn't possible with the Text element.

    A Spannable container is filled with Span children. Other children, or nested Spannable instances, aren't allowed.

    There are two types of Span children:

    For example, you could italicize “world” in a "Hello world" Tile and insert an image between the words, as shown in the following code sample:

    Kotlin

    private fun mySpannable(): LayoutElement =
        Spannable.Builder()
            .addSpan(SpanText.Builder()
                .setText("Hello ")
                .build()
            )
            .addSpan(SpanImage.Builder()
                .setWidth(dp(24f))
                .setHeight(dp(24f))
                .setResourceId("image_id")
                .build()
            )
            .addSpan(SpanText.Builder()
                .setText("world")
                .setFontStyle(FontStyle.Builder()
                    .setItalic(true)
                    .build())
                .build()
            ).build()
    

    Java

    private LayoutElement mySpannable() {
       return new Spannable.Builder()
            .addSpan(new SpanText.Builder()
                .setText("Hello ")
                .build()
            )
            .addSpan(new SpanImage.Builder()
                .setWidth(dp(24f))
                .setHeight(dp(24f))
                .setResourceId("image_id")
                .build()
            )
            .addSpan(new SpanText.Builder()
                .setText("world")
                .setFontStyle(newFontStyle.Builder()
                    .setItalic(true)
                    .build())
                .build()
            ).build();
    }
    

    Work with resources

    Tiles don't have access to any of your app's resources. This means that you can’t pass an Android image ID to an Image layout element and expect it to resolve. Instead, override the onResourcesRequest() method and provide any resources manually.

    There are two ways to provide images within the onResourcesRequest() method:

    Kotlin

    override fun onResourcesRequest(
        requestParams: ResourcesRequest
    ) = Futures.immediateFuture(
    Resources.Builder()
        .setVersion("1")
        .addIdToImageMapping("image_from_resource", ImageResource.Builder()
            .setAndroidResourceByResId(AndroidImageResourceByResId.Builder()
                .setResourceId(R.drawable.image_id)
                .build()
            ).build()
        )
        .addIdToImageMapping("image_inline", ImageResource.Builder()
            .setInlineResource(InlineImageResource.Builder()
                .setData(imageAsByteArray)
                .setWidthPx(48)
                .setHeightPx(48)
                .setFormat(ResourceBuilders.IMAGE_FORMAT_RGB_565)
                .build()
            ).build()
        ).build()
    )
    

    Java

    @Override
    protected ListenableFuture<Resources> onResourcesRequest(
           @NonNull ResourcesRequest requestParams
    ) {
    return Futures.immediateFuture(
        new Resources.Builder()
            .setVersion("1")
            .addIdToImageMapping("image_from_resource", new ImageResource.Builder()
                .setAndroidResourceByResId(new AndroidImageResourceByResId.Builder()
                    .setResourceId(R.drawable.image_id)
                    .build()
                ).build()
            )
            .addIdToImageMapping("image_inline", new ImageResource.Builder()
                .setInlineResource(new InlineImageResource.Builder()
                    .setData(imageAsByteArray)
                    .setWidthPx(48)
                    .setHeightPx(48)
                    .setFormat(ResourceBuilders.IMAGE_FORMAT_RGB_565)
                    .build()
                ).build()
            ).build()
    );
    }
    

    Interact with the Tile

    By adding the Clickable modifier to a layout element, you can react to a user tapping that layout element. As a reaction to a click event, you can perform two actions:

    • LaunchAction: launch an activity that is explicitly declared as android:exported="true" in AndroidManifest.xml.
    • LoadAction: force refreshes the Tile, calling onTileRequest().

    To set up a LaunchAction, pass the class name and package name of the activity you’d like to launch when the user taps the element, as shown in the following code sample:

    Kotlin

    private fun tappableElement(): LayoutElement =
        Text.Builder()
            .setText("Tap me!")
            .setModifiers(Modifiers.Builder()
                .setClickable(Clickable.Builder()
                    .setId("foo")
                    .setOnClick(LaunchAction.Builder()
                        .setAndroidActivity(AndroidActivity.Builder()
                            .setClassName(MyActivity::class.java.getName())
                            .setPackageName(this.packageName)
                            .build()
                        ).build()
                    ).build()
                ).build()
            ).build()
    

    Java

    private LayoutElement tappableElement() {
        return new Text.Builder()
            .setText("Tap me!")
            .setModifiers(new Modifiers.Builder()
                .setClickable(new Clickable.Builder()
                    .setId("foo")
                    .setOnClick(new LaunchAction.Builder()
                        .setAndroidActivity(new AndroidActivity.Builder()
                            .setClassName(MyActivity.class.getName())
                            .setPackageName(this.getPackageName())
                            .build()
                        ).build()
                    ).build()
                .build()
            .build()
        ).build();
    }
    

    Inside the launched activity, you can retrieve the ID that was used for the Tile:

    Kotlin

    class MyActivity : FragmentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val clickableId =
                intent.getStringExtra(TileService.EXTRA_CLICKABLE_ID)
            // clickableId will be "foo" when launched from the Tile
        }
    }
    

    Java

    public class MyActivity extends FragmentActivity {
       @Override
       public void onCreate(@Nullable Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           String clickableId =
               getIntent().getStringExtra(TileService.EXTRA_CLICKABLE_ID);
           // clickableId will be "foo" when launched from the Tile
       }
    }
    

    Note: You can start an Activity from your own application or from another application, as long as android:exported="true" is declared in your app's manifest.

    Alternatively, use LoadAction to refresh your Tile when the user taps your layout element, as shown in the following code sample:

    Kotlin

    private fun tappableElement(): LayoutElement =
        Text.Builder()
            .setText("Tap me!")
            .setModifiers(Modifiers.Builder()
                .setClickable(Clickable.Builder()
                    .setId("foo")
                    .setOnClick(LoadAction.Builder().build())
                    .build()
                ).build()
            ).build()
    

    Java

    private LayoutElement tappableElement() {
        return new Text.Builder()
            .setText("Tap me!")
            .setModifiers(new Modifiers.Builder()
                .setClickable(new Clickable.Builder()
                    .setId("foo")
                    .setOnClick(new LoadAction.Builder().build())
                    .build()
                ).build()
            ).build()
    }
    

    In this case, the clickable ID set in setId() is passed along to the onTileRequest() call, so you can render a different layout based on this ID, as shown in the following code sample:

    Kotlin

    override fun onTileRequest(requestParams: TileRequest) = Futures.immediateFuture(
        Tile.Builder()
            .setResourcesVersion("1")
            .setTimeline(Timeline.Builder()
                .addTimelineEntry(TimelineEntry.Builder()
                    .setLayout(Layout.Builder()
                        .setRoot(
                            when(requestParams.state.lastClickableId) {
                                "foo" -> myFooLayout()
                                else -> myOtherLayout()
                            }
                        ).build()
                    ).build()
                ).build()
            ).build()
    )
    

    Java

    @NonNull
    @Override
    protected ListenableFuture<Tile> onTileRequest(
       @NonNull TileRequest requestParams
    ) {
        LayoutElement root;
        if(requestParams.getState().getLastClickableId().equals("foo")) {
            root = myFooLayout();
        } else {
            root = myOtherLayout();
        }
        return Futures.immediateFuture(new Tile.Builder()
            .setResourcesVersion("1")
            .setTimeline(new Timeline.Builder()
                .addTimelineEntry(TimelineEntry.Builder()
                    .setLayout(Layout.Builder()
                        .setRoot(root)
                        .build()
                    ).build()
                ).build()
            ).build());
    }