Interact with tiles

Tiles can do more than just display information; they can also be interactive. To make an element such as textButton() respond to taps, generate a click handler using clickable() and associate it with the layout element.

You can configure a Clickable to trigger an action in two main ways:

  1. Launch an activity directly: Use launchAction() for cases where you need to open an activity immediately.
  2. Delegate to your tile service: Use loadAction() to trigger logic within your TileService. This is a more flexible approach that lets you refresh the tile's content, update its state, or launch a more complex activity.

Launch an exported activity

If a user tap should immediately launch an activity, use launchAction(). Provide a ComponentName to identify the activity. The activity must be exported. With this approach, you can pass Intent extras with the action. However, it's not possible to set custom Intent flags.

The following example shows how to create a Clickable to launch TileActivity with two extras, name and age:

textButton(
    labelContent = {
        text("launchAction()".layoutString, typography = BODY_LARGE)
    },
    onClick =
    clickable(
        action =
        launchAction(
            ComponentName(
                "com.example.wear",
                "com.example.wear.snippets.m3.tile.TileActivity",
            ),
            mapOf(
                "name" to ActionBuilders.stringExtra("Bartholomew"),
                "age" to ActionBuilders.intExtra(21),
            ),
        )
    ),
)

Inside the launched activity, retrieve the values from the intent extras:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // When this activity is launched from the tile InteractionLaunchAction,
    // "name" will be "Bartholomew" and "age" will be 21
    val name = intent.getStringExtra("name")
    val age = intent.getStringExtra("age")

    // ...
}

Handle interactions in your tile service

For more flexible interactions, use loadAction(). When a user taps an element configured with loadAction, the system re-invokes your TileService.onTileRequest(). This lets you run logic in your service to update the tile, change its state, and perform more complex tasks.

Refresh the tile's content

The simplest use of loadAction is to signal a refresh. Call loadAction with no arguments. When tapped, the system calls onTileRequest(), allowing your service to return a new layout with updated content.

textButton(
    onClick = clickable(loadAction()),
    labelContent = { text("Refresh".layoutString) },
)

Distinguish among multiple interactive elements

If your tile contains multiple interactive elements, you can associate an ID with the Clickable modifier:

Inside onTileRequest(), you can check this ID using requestParams.currentState.lastClickableId to decide what action to perform.

Example: Launching an activity with a deep link

This pattern is ideal for launching an activity with a deep link. The user tap reloads the tile, your service inspects the ID, and then launches the new activity. To control the back stack, use a TaskStackBuilder to provide a better navigation experience for the user. When the user taps the element, they are taken directly to the deep-linked screen (the message_detail/1 screen from the example). Because .addNextIntentWithParentStack() was used, the parent activity is also added to the back stack. This means if the user swipes back, they will navigate up to the app's main screen (MessageList in the example) instead of immediately exiting to the tile. Swiping back a second time returns them to the tile.

Then, in TileActivity, configure your navigation to match the googleandroidsnippets://app/message_detail/{id} pattern.

Use TaskStackBuilder to provide a better navigation experience for the user. When the user taps the element, they are taken directly to the deep-linked screen—in this example, that's the message_detail/1 screen. Because .addNextIntentWithParentStack() was used, the parent activity is also added to the back stack. This means if the user swipes back, they will navigate up to the app's main screen—MessageList in the example—instead of immediately exiting to the tile. Swiping back a second time returns them to the tile.

Update state within the tile

Your tile has a StateBuilders.State object that stores key-value pairs and persists across reloads. You can use loadAction() to update this state when a user interacts with the tile.

To do this, pass a DynamicDataMap to loadAction() containing the new state values.

textButton(
    labelContent = {
        text("loadAction()".layoutString, typography = BODY_LARGE)
    },
    onClick =
    clickable(
        action =
        loadAction(
            dynamicDataMapOf(
                stringAppDataKey("name") mapTo "Javier",
                intAppDataKey("age") mapTo 37,
            )
        )
    ),
)

When onTileRequest() is triggered by this action, you can read the updated data from requestParams.currentState.stateMap. This is useful for interactions that directly modify data on the tile, like incrementing a counter or toggling a setting.

override fun onTileRequest(
    requestParams: RequestBuilders.TileRequest
): ListenableFuture<Tile> {

    // When triggered by loadAction(), "name" will be "Javier", and "age" will
    // be 37.
    with(requestParams.currentState.stateMap) {
        val name = this[stringAppDataKey("name")]
        val age = this[intAppDataKey("age")]
    }

    // ...
}