Cómo crear tu primera tarjeta en Wear OS

1. Introducción

reloj animado; usuario que desliza la cara de reloj hasta la primera tarjeta, que es una previsión; luego, a una tarjeta del temporizador y de vuelta a la primera

Las tarjetas de Wear OS brindan un acceso sencillo a la información y a las acciones que los usuarios necesitan para realizar tareas. Con solo deslizar el dedo sobre la cara de reloj, el usuario puede consultar el pronóstico actualizado o iniciar el cronómetro.

Una tarjeta se ejecuta como parte de la IU del sistema en lugar de ejecutarse en el propio contenedor de la aplicación. Usamos un Servicio para describir el diseño y el contenido de la tarjeta. Luego, la IU del sistema renderizará la tarjeta cuando sea necesario.

Actividades

35a459b77a2c9d52.png

Compilarás la tarjeta de una app de mensajería que muestra conversaciones recientes. Desde esa pantalla, el usuario puede pasar a tres tareas comunes:

  • Abrir una conversación
  • Buscar una conversación
  • Redactar un mensaje nuevo

Qué aprenderás

En este codelab, aprenderás a escribir tu propia tarjeta de Wear OS, incluido lo siguiente:

  • Cómo crear un TileService
  • Cómo probar una tarjeta en un dispositivo
  • Cómo obtener una vista previa de la IU de una tarjeta en Android Studio
  • Cómo desarrollar la IU para una tarjeta
  • Cómo agregar imágenes
  • Cómo controlar interacciones

Requisitos previos

  • Conocimientos básicos sobre Kotlin

2. Cómo prepararte

En este paso, configurarás tu entorno y descargarás un proyecto inicial.

Requisitos

Si no estás familiarizado con el uso de Wear OS, lee esta guía rápida antes de comenzar. Se incluyen instrucciones para configurar un emulador de Wear OS y se describe cómo navegar por el sistema.

Cómo descargar el código

Si ya instalaste git, solo ejecuta el siguiente comando para clonar el código de este repositorio.

git clone https://github.com/android/codelab-wear-tiles.git
cd codelab-wear-tiles

Si no tienes git, puedes hacer clic en el siguiente botón para descargar todo el código de este codelab:

Abre el proyecto en Android Studio

En la ventana "Welcome to Android Studio", selecciona c01826594f360d94.png Open an Existing Project o File > Open y selecciona la carpeta [Ubicación de descarga].

3. Cómo crear una tarjeta básica

El punto de entrada para una tarjeta es el servicio de mosaicos para mapas. En este paso, registrarás un servicio de este tipo y definirás un diseño para la tarjeta.

HelloWorldTileService

Una clase que implementa TileService debe especificar dos métodos:

  • onTileResourcesRequest(requestParams: ResourcesRequest): ListenableFuture<Resources>
  • onTileRequest(requestParams: TileRequest): ListenableFuture<Tile>

El primer método devuelve un objeto Resources que asigna IDs de cadenas a los recursos de imagen que usaremos en la tarjeta.

El segundo devuelve la descripción de una tarjeta, incluido su diseño. Aquí es donde definimos el diseño de la tarjeta y cómo se vinculan los datos a ella.

Abre HelloWorldTileService.kt desde el módulo start. Todos los cambios que hagas estarán en este módulo. También hay un módulo finished si quieres ver el resultado de este codelab.

HelloWorldTileService extiende SuspendingTileService, un wrapper compatible con corrutinas de Kotlin de la biblioteca de tarjetas de Horologist. Horologist es un grupo de bibliotecas de Google que tiene como objetivo aportar a los desarrolladores de Wear OS un complemento con funciones que comúnmente requieren, pero que aún no están disponibles en Jetpack.

SuspendingTileService proporciona dos funciones de suspensión, que son versiones de corrutinas equivalentes de las funciones de TileService:

  • suspend resourcesRequest(requestParams: ResourcesRequest): Resources
  • suspend tileRequest(requestParams: TileRequest): Tile

Si deseas obtener más información sobre las corrutinas, consulta la documentación sobre corrutinas de Kotlin en Android.

HelloWorldTileService aún no está completo. Debemos registrar el servicio en nuestro manifiesto y también proporcionar una implementación para tileLayout.

Cómo registrar el servicio de tarjetas

Una vez que se registre el servicio de tarjetas en el manifiesto, se mostrará en la lista de tarjetas disponibles que puede agregar el usuario.

Agrega el <service> dentro del elemento <application>:

start/src/main/AndroidManifest.xml

<service
    android:name="com.example.wear.tiles.hello.HelloWorldTileService"
    android:icon="@drawable/ic_waving_hand_24"
    android:label="@string/hello_tile_label"
    android:description="@string/hello_tile_description"
    android:exported="true"
    android:permission="com.google.android.wearable.permission.BIND_TILE_PROVIDER">
    
    <intent-filter>
        <action android:name="androidx.wear.tiles.action.BIND_TILE_PROVIDER" />
    </intent-filter>

    <!-- The tile preview shown when configuring tiles on your phone -->
    <meta-data
        android:name="androidx.wear.tiles.PREVIEW"
        android:resource="@drawable/tile_hello" />
</service>

El ícono y la etiqueta se usan (como marcador de posición) cuando la tarjeta se carga por primera vez o si hay un error al momento de cargarla. Los metadatos al final definen una imagen de vista previa que se muestra en el carrusel cuando el usuario agrega una tarjeta.

Cómo definir un diseño para la tarjeta

HelloWorldTileService tiene una función llamada tileLayout con un TODO() como cuerpo. Ahora vamos a reemplazar eso con una implementación en la que definimos el diseño de nuestra tarjeta y vincularemos datos:

start/src/main/java/com/example/wear/tiles/hello/HelloWorldTileService.kt

private fun tileLayout(): LayoutElement {
    val text = getString(R.string.hello_tile_body)
    return LayoutElementBuilders.Box.Builder()
        .setVerticalAlignment(LayoutElementBuilders.VERTICAL_ALIGN_CENTER)
        .setWidth(DimensionBuilders.expand())
        .setHeight(DimensionBuilders.expand())
        .addContent(
            LayoutElementBuilders.Text.Builder()
                .setText(text)
                .build()
        )
        .build()
}

Creamos un elemento Text y lo configuramos dentro de un Box para poder realizar una alineación básica.

Ya creaste tu primera tarjeta de Wear OS. Instalemos esta tarjeta y veamos cómo se ve.

4. Cómo probar tu tarjeta en un dispositivo

Con el módulo de partida seleccionado en el menú desplegable de la configuración de ejecución, puedes instalar la app (el módulo start) en tu dispositivo o emulador, y, luego, instalar manualmente la tarjeta, como lo haría un usuario.

En su lugar, usemos Direct Surface Launch, una función que se introdujo en Android Studio Dolphin, para crear una nueva configuración de ejecución y lanzar la tarjeta directamente desde Android Studio. Selecciona "Edit Configurations…" en el menú desplegable del panel superior.

Menú desplegable de configuración de ejecución desde el panel superior de Android Studio. La opción Edit configurations aparece destacada.

Haz clic en el botón "Add new configuration" y elige "Wear OS Tile". Agrega un nombre descriptivo y, luego, selecciona el módulo Tiles_Code_Lab.start y la tarjeta HelloWorldTileService.

Presiona "OK" para terminar.

Menú de Edit Configuration desde el que se puede configurar una tarjeta de Wear OS llamada HelloTile.

Direct Surface Launch nos permite probar rápidamente tarjetas en un emulador de Wear OS o en un dispositivo físico. Ejecuta "HelloTile" para probarlo. Debería verse como en la siguiente captura de pantalla.

Reloj redondo que muestra el mensaje "Time to create a tile!" escrito en blanco sobre un fondo negro

5. Cómo crear una tarjeta de mensajes

Reloj redondo que muestra 5 botones redondos dispuestos en una pirámide de 2 x 3. El primer y el tercer botón muestran las iniciales en letras de color violeta, el segundo y el cuarto muestran las fotos de perfil, y el último botón es un ícono de búsqueda. Debajo de los botones, hay un pequeño chip violeta que dice "New" en letras negras.

La tarjeta de mensajería que vamos a crear se parece más a una tarjeta del mundo real. A diferencia del ejemplo de HelloWorld, esta carga datos desde un repositorio local, recupera imágenes para mostrar desde la red y controla interacciones para abrir la app, y lo hace directamente desde la tarjeta.

MessagingTileService

MessagingTileService extiende la clase SuspendingTileService que vimos antes.

La principal diferencia entre este ejemplo y el anterior es que ahora se observan datos del repositorio y también se obtienen datos de imágenes de la red.

MessagingTileRenderer

MessagingTileRenderer extiende la clase SingleTileLayoutRenderer (otra abstracción de las tarjetas de Horologist). Es completamente síncrono: el estado se pasa a las funciones del renderizador, lo que facilita su uso en pruebas y vistas previas de Android Studio.

En el siguiente paso, veremos cómo agregar vistas previas de Android Studio para tarjetas.

6. Cómo agregar funciones de vista previa

Podemos obtener una vista previa de la IU de tarjetas en Android Studio con las funciones de vista previa de tarjetas que se lanzaron en la versión 1.4 de la biblioteca de tarjetas de Jetpack (actualmente en versión alfa). Eso reduce el ciclo de retroalimentación cuando se desarrolla la IU, lo que aumenta la velocidad de desarrollo.

Agrega una vista previa de tarjeta para el elemento MessagingTileRenderer al final del archivo.

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

@Preview(device = WearDevices.SMALL_ROUND)
@Preview(device = WearDevices.LARGE_ROUND)
fun messagingTileLayoutPreview(context: Context): TilePreviewData {
    return TilePreviewData { request ->
        MessagingTileRenderer(context).renderTimeline(
            MessagingTileState(knownContacts),
            request
        )
    }
}

Ten en cuenta que la anotación @Composable no se proporciona. Aunque las tarjetas usan la misma IU de vista previa que las Funciones de componibilidad, las tarjetas no usan Compose y no son componibles.

Usa el modo de editor "Pantalla dividida" para obtener una vista previa de la tarjeta:

Vista de pantalla dividida de Android Studio con el código de la vista previa a la izquierda y una imagen de la tarjeta a la derecha.

En el siguiente paso, usaremos Tiles Material para actualizar el diseño.

7. Cómo agregar Tiles Material

Tiles Material proporciona componentes de Material y diseños predefinidos, lo que te permite crear tarjetas que contengan los últimos diseños de materiales para Wear OS.

Agrega la dependencia de Tiles Material a tu archivo build.gradle:

start/build.gradle

implementation "androidx.wear.protolayout:protolayout-material:$protoLayoutVersion"

Agrega el código del botón en la parte inferior del archivo del renderizador y también en la vista previa:

start/src/main/java/MessagingTileRenderer.kt

private fun searchLayout(
    context: Context,
    clickable: ModifiersBuilders.Clickable,
) = Button.Builder(context, clickable)
    .setContentDescription(context.getString(R.string.tile_messaging_search))
    .setIconContent(MessagingTileRenderer.ID_IC_SEARCH)
    .setButtonColors(ButtonColors.secondaryButtonColors(MessagingTileTheme.colors))
    .build()

También podemos hacer algo similar para crear el diseño de los contactos:

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

private fun contactLayout(
    context: Context,
    contact: Contact,
    clickable: ModifiersBuilders.Clickable,
) = Button.Builder(context, clickable)
    .setContentDescription(contact.name)
    .apply {
        if (contact.avatarUrl != null) {
            setImageContent(contact.imageResourceId())
        } else {
            setTextContent(contact.initials)
            setButtonColors(ButtonColors.secondaryButtonColors(MessagingTileTheme.colors))
        }
    }
    .build()

Tiles Material no solo incluye componentes. En lugar de usar una serie de columnas y filas anidadas, podemos usar diseños de Tiles Material para lograr el aspecto deseado rápidamente.

Aquí podemos usar PrimaryLayout y MultiButtonLayout para ordenar 4 contactos y el botón de búsqueda. Actualiza la función messagingTileLayout() en MessagingTileRenderer con estos diseños:

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

private fun messagingTileLayout(
    context: Context,
    deviceParameters: DeviceParametersBuilders.DeviceParameters,
    state: MessagingTileState
) = PrimaryLayout.Builder(deviceParameters)
    .setResponsiveContentInsetEnabled(true)
    .setContent(
        MultiButtonLayout.Builder()
            .apply {
                // In a PrimaryLayout with a compact chip at the bottom, we can fit 5 buttons.
                // We're only taking the first 4 contacts so that we can fit a Search button too.
                state.contacts.take(4).forEach { contact ->
                    addButtonContent(
                        contactLayout(
                            context = context,
                            contact = contact,
                            clickable = emptyClickable
                        )
                    )
                }
            }
            .addButtonContent(searchLayout(context, emptyClickable))
            .build()
    )
    .build()

96fee80361af2c0f.png

MultiButtonLayout admite hasta 7 botones y los distribuirá con el espaciado adecuado para ti.

Agreguemos un CompactChip "nuevo" como el chip "principal" de PrimaryLayout en la función messagingTileLayout():

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

.setPrimaryChipContent(
        CompactChip.Builder(
            /* context = */ context,
            /* text = */ context.getString(R.string.tile_messaging_create_new),
            /* clickable = */ emptyClickable,
            /* deviceParameters = */ deviceParameters
        )
            .setChipColors(ChipColors.primaryChipColors(MessagingTileTheme.colors))
            .build()
    )

2041bdca8a46458b.png

En el siguiente paso, corregiremos las imágenes faltantes.

8. Cómo agregar imágenes

A un alto nivel, las tarjetas constan de dos elementos: elementos de diseño (que hacen referencia a recursos por IDs de cadenas) y los recursos en sí (que pueden ser imágenes).

Hacer que una imagen local esté disponible es una tarea sencilla; si bien no puedes usar los recursos de elementos de diseño de Android directamente, puedes convertirlos de forma trivial al formato requerido con una función conveniente que proporciona Horologist. Luego, usa la función addIdToImageMapping para asociar la imagen con el identificador de recursos. Por ejemplo:

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

addIdToImageMapping(
    ID_IC_SEARCH,
    drawableResToImageResource(R.drawable.ic_search_24)
)

Para imágenes remotas, usa Coil, un cargador de imágenes basado en corrutinas de Kotlin, para cargar las imágenes a través de la red.

El código ya está escrito para esto:

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileService.kt

override suspend fun resourcesRequest(requestParams: ResourcesRequest): Resources {
    val avatars = imageLoader.fetchAvatarsFromNetwork(
        context = this@MessagingTileService,
        requestParams = requestParams,
        tileState = latestTileState()
    )
    return renderer.produceRequestedResources(avatars, requestParams)
}

Como el renderizador de tarjetas es completamente síncrono, el servicio de tarjetas recupera mapas de bits de la red. Al igual que antes, según el tamaño de la imagen, podría resultar más apropiado usar WorkManager para recuperar las imágenes con anticipación. Sin embargo, para este codelab, las recuperamos de forma directa.

Pasamos el mapa de avatars (de Contact a Bitmap) al renderizador como "estado" para los recursos. Ahora, el renderizador puede transformar estos mapas de bits en recursos de imagen para tarjetas.

Este código también está escrito:

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

override fun ResourceBuilders.Resources.Builder.produceRequestedResources(
    resourceState: Map<Contact, Bitmap>,
    deviceParameters: DeviceParametersBuilders.DeviceParameters,
    resourceIds: List<String>
) {
    addIdToImageMapping(
        ID_IC_SEARCH,
        drawableResToImageResource(R.drawable.ic_search_24)
    )

    resourceState.forEach { (contact, bitmap) ->
        addIdToImageMapping(
            /* id = */ contact.imageResourceId(),
            /* image = */ bitmap.toImageResource()
        )
    }
}

Entonces, si el servicio recupera los mapas de bits y el renderizador los transforma en recursos de imagen, ¿por qué la tarjeta no muestra imágenes?

¡Sí las muestra! Si ejecutas la tarjeta en un dispositivo (con acceso a Internet), deberías ver que las imágenes efectivamente se cargan. El problema se encuentra solo en nuestra vista previa porque no pasamos ningún recurso a TilePreviewData().

Para la tarjeta real, recuperaremos mapas de bits de la red y los asignaremos a diferentes contactos, pero no es necesario acceder a la red para las vistas previas ni las pruebas.

Debemos hacer dos cambios. Primero, crea una función previewResources() que muestre un objeto Resources:

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

private fun previewResources() = Resources.Builder()
    .addIdToImageMapping(ID_IC_SEARCH, drawableResToImageResource(R.drawable.ic_search_24))
    .addIdToImageMapping(knownContacts[1].imageResourceId(), drawableResToImageResource(R.drawable.ali))
    .addIdToImageMapping(knownContacts[2].imageResourceId(), drawableResToImageResource(R.drawable.taylor))
    .build()

Luego, actualiza messagingTileLayoutPreview() para pasar los recursos:

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

@Preview(device = WearDevices.SMALL_ROUND)
@Preview(device = WearDevices.LARGE_ROUND)
fun messagingTileLayoutPreview(context: Context): TilePreviewData {
    return TilePreviewData({ previewResources() }) { request ->
        MessagingTileRenderer(context).renderTimeline(
            MessagingTileState(knownContacts),
            request
        )
    }
}

Ahora, si actualizamos la vista previa, las imágenes deberían mostrar lo siguiente:

3142b42717407059.png

En el siguiente paso, controlaremos los clics en cada uno de los elementos.

9. Cómo controlar interacciones

Una de las cosas más útiles que podemos hacer con una tarjeta es proporcionar accesos directos a los recorridos críticos del usuario. Esto es diferente del selector de aplicaciones, que solo abre la app. Aquí tenemos espacio para brindar accesos directos contextuales a una pantalla específica de tu app.

Hasta ahora, usamos emptyClickable para el chip y cada uno de los botones. Esto está bien para las vistas previas, que no son interactivas, pero veamos cómo agregar acciones para los elementos.

Dos constructores de la clase "ActionBuilders" definen las acciones en las que se puede hacer clic: LoadAction y LaunchAction.

LoadAction

Se puede usar LoadAction si deseas realizar una lógica en el servicio de mosaicos para mapas cuando el usuario hace clic en un elemento, p. ej., incrementando un contador.

.setClickable(
    Clickable.Builder()
        .setId(ID_CLICK_INCREMENT_COUNTER)
        .setOnClick(ActionBuilders.LoadAction.Builder().build())
        .build()
    )
)

Cuando hagas clic en esta opción, se llamará a onTileRequest en tu servicio (tileRequest en SuspendingTileService), por lo que es una buena oportunidad para actualizar la IU de la tarjeta:

override suspend fun tileRequest(requestParams: TileRequest): Tile {
    if (requestParams.state.lastClickableId == ID_CLICK_INCREMENT_COUNTER) {
        // increment counter
    }
    // return an updated tile
}

LaunchAction

Se puede usar LaunchAction para iniciar una actividad. En MessagingTileRenderer, actualicemos el botón de búsqueda en el que se puede hacer clic.

El botón de búsqueda se define con la función searchLayout() en MessagingTileRenderer. Ya toma un Clickable como parámetro, pero hasta ahora hemos pasado emptyClickable, una implementación no-op que no hace nada cuando se hace clic en el botón.

Actualicemos messagingTileLayout() para que pase una acción de clic real.

  1. Agrega un parámetro nuevo, searchButtonClickable (de tipo ModifiersBuilders.Clickable).
  2. Pasa esto a la función searchLayout() existente.

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

private fun messagingTileLayout(
    context: Context,
    deviceParameters: DeviceParametersBuilders.DeviceParameters,
    state: MessagingTileState,
    searchButtonClickable: ModifiersBuilders.Clickable
...
    .addButtonContent(searchLayout(context, searchButtonClickable))

También necesitamos actualizar renderTile, que es donde llamamos a messagingTileLayout, ya que agregamos un parámetro nuevo (searchButtonClickable). Usaremos la función launchActivityClickable() para crear un nuevo elemento en el que se pueda hacer clic y pasar openSearch() ActionBuilder como acción:

start/src/main/java/com/example/wear/tiles/messaging/tile/MessagingTileRenderer.kt

override fun renderTile(
    state: MessagingTileState,
    deviceParameters: DeviceParametersBuilders.DeviceParameters
): LayoutElementBuilders.LayoutElement {
    return messagingTileLayout(
        context = context,
        deviceParameters = deviceParameters,
        state = state,
        searchButtonClickable = launchActivityClickable("search_button", openSearch())
    )
}

Abre launchActivityClickable para ver cómo funcionan estas funciones (ya definidas):

start/src/main/java/com/example/wear/tiles/messaging/tile/ClickableActions.kt

internal fun launchActivityClickable(
    clickableId: String,
    androidActivity: ActionBuilders.AndroidActivity
) = ModifiersBuilders.Clickable.Builder()
    .setId(clickableId)
    .setOnClick(
        ActionBuilders.LaunchAction.Builder()
            .setAndroidActivity(androidActivity)
            .build()
    )
    .build()

Es muy similar a LoadAction. La diferencia principal es que llamamos a setAndroidActivity. En el mismo archivo, tenemos varios ejemplos de ActionBuilder.AndroidActivity.

En el caso de openSearch, que usamos para este elemento en el que se puede hacer clic, llamamos a setMessagingActivity y pasamos una cadena adicional para identificar con qué botón se hizo clic.

start/src/main/java/com/example/wear/tiles/messaging/tile/ClickableActions.kt

internal fun openSearch() = ActionBuilders.AndroidActivity.Builder()
    .setMessagingActivity()
    .addKeyToExtraMapping(
        MainActivity.EXTRA_JOURNEY,
        ActionBuilders.stringExtra(MainActivity.EXTRA_JOURNEY_SEARCH)
    )
    .build()

...

internal fun ActionBuilders.AndroidActivity.Builder.setMessagingActivity(): ActionBuilders.AndroidActivity.Builder {
    return setPackageName("com.example.wear.tiles")
        .setClassName("com.example.wear.tiles.messaging.MainActivity")
}

Ejecuta la tarjeta (asegúrate de ejecutar la tarjeta "mensajería", no la tarjeta "hola") y haz clic en el botón de búsqueda. Debería abrirse la MainActivity y mostrar texto para confirmar que se hizo clic en el botón de búsqueda.

Agregar acciones para los demás botones es similar. ClickableActions contiene las funciones que necesitas. Si necesitas una pista, consulta MessagingTileRenderer en el módulo finished.

10. Felicitaciones

¡Felicitaciones! Ya aprendiste a crear una tarjeta para Wear OS.

¿Qué sigue?

Para obtener más información, consulta las implementaciones de tarjetas doradas en GitHub, la guía de tarjetas de Wear OS y los lineamientos de diseño.