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:
- Launch an activity directly: Use
launchAction()
for cases where you need to open an activity immediately. - Delegate to your tile service: Use
loadAction()
to trigger logic within yourTileService
. 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:
textButton( labelContent = { text("Deep Link me!".layoutString, typography = BODY_LARGE) }, onClick = clickable(id = "foo", action = loadAction()), )
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.
override fun onTileRequest( requestParams: RequestBuilders.TileRequest ): ListenableFuture<Tile?> { val lastClickableId = requestParams.currentState.lastClickableId if (lastClickableId == "foo") { TaskStackBuilder.create(this) .addNextIntentWithParentStack( Intent( Intent.ACTION_VIEW, "googleandroidsnippets://app/message_detail/1".toUri(), this, TileActivity::class.java, ) ) .startActivities() } // ... User didn't tap a button (either first load or tapped somewhere else) // ... }
Then, in TileActivity
, configure your navigation to match the
googleandroidsnippets://app/message_detail/{id}
pattern.
AppScaffold { val navController = rememberSwipeDismissableNavController() SwipeDismissableNavHost( navController = navController, startDestination = "message_list", ) { // ... composable( route = "message_detail/{id}", deepLinks = listOf( navDeepLink { uriPattern = "googleandroidsnippets://app/message_detail/{id}" } ), ) { val id = it.arguments?.getString("id") ?: "0" MessageDetails(details = "message $id") } } }
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")] } // ... }