Control de versiones de tarjetas

En los dispositivos Wear OS, las tarjetas se renderizan con dos componentes clave con versiones independientes. Para garantizar que los mosaicos de tus apps funcionen correctamente en todos los dispositivos, es importante que comprendas esta arquitectura subyacente.

  • Bibliotecas relacionadas con tarjetas de Jetpack: Estas bibliotecas (incluidas Wear Tiles y Wear ProtoLayout) están incorporadas en tu app, y tú, como desarrollador, controlas sus versiones. Tu app usa estas bibliotecas para construir un objeto TileBuilder.Tile (la estructura de datos que representa tu tarjeta) en respuesta a la llamada onTileRequest() del sistema.
  • Renderizador de ProtoLayout: Este componente del sistema es responsable de renderizar el objeto Tile en la pantalla y controlar las interacciones del usuario. El desarrollador de la app no controla la versión del renderizador, y esta puede variar entre dispositivos, incluso entre aquellos con hardware idéntico.

La apariencia o el comportamiento de una tarjeta pueden variar según las versiones de la biblioteca de tarjetas de Jetpack de tu app y la versión del renderizador de ProtoLayout en el dispositivo del usuario. Por ejemplo, un dispositivo puede admitir la rotación o la visualización de datos de frecuencia cardíaca, y otro no.

En este documento, se explica cómo garantizar que tu app sea compatible con diferentes versiones de la biblioteca de Tiles y el renderizador de ProtoLayout, y cómo migrar a versiones más recientes de la biblioteca de Jetpack.

Considera la compatibilidad

Para crear una tarjeta que funcione correctamente en una variedad de dispositivos, ten en cuenta la compatibilidad con diferentes funciones. Puedes hacerlo a través de dos estrategias principales: detectar las capacidades del renderizador en el tiempo de ejecución y proporcionar alternativas integradas.

Detecta las capacidades del renderizador

Puedes cambiar de forma dinámica el diseño de la tarjeta según las funciones disponibles en un dispositivo determinado.

Cómo detectar la versión del renderizador

  • Usa el método getRendererSchemaVersion() del objeto DeviceParameters que se pasa a tu método onTileRequest(). Este método devuelve los números de versión principal y secundaria del renderizador de ProtoLayout en el dispositivo.
  • Luego, puedes usar la lógica condicional en tu implementación de onTileRequest() para adaptar el diseño o el comportamiento de la tarjeta según la versión del renderizador detectada.

La anotación @RequiresSchemaVersion

  • La anotación @RequiresSchemaVersion en los métodos de ProtoLayout indica la versión mínima del esquema del renderizador que se requiere para que ese método se comporte como se documenta (ejemplo).
    • Si bien llamar a un método que requiere una versión del renderizador más alta que la disponible en el dispositivo no provocará que falle tu app, podría hacer que no se muestre el contenido o que se ignore la función.

Ejemplo de detección de versiones

val rendererVersion = requestParams.deviceConfiguration.rendererSchemaVersion

val arcElement =
    // DashedArcLine has the annotation @RequiresSchemaVersion(major = 1, minor = 500)
    // and so is supported by renderer versions 1.500 and greater
    if (
        rendererVersion.major > 1 ||
        (rendererVersion.major == 1 && rendererVersion.minor >= 500)
    ) {
        // Use DashedArcLine if the renderer supports it …
        DashedArcLine.Builder()
            .setLength(degrees(270f))
            .setThickness(8f)
            .setLinePattern(
                LayoutElementBuilders.DashedLinePattern.Builder()
                    .setGapSize(8f)
                    .setGapInterval(10f)
                    .build()
            )
            .build()
    } else {
        // … otherwise use ArcLine.
        ArcLine.Builder().setLength(degrees(270f)).setThickness(dp(8f)).build()
    }

Proporciona alternativas

Algunos recursos te permiten definir una alternativa directamente en el compilador. Suele ser más simple que verificar la versión del renderizador y es el enfoque preferido cuando está disponible.

Un caso de uso común es proporcionar una imagen estática como resguardo para una animación de Lottie. Si el dispositivo no admite animaciones de Lottie, renderizará la imagen estática.

val lottieImage =
    ResourceBuilders.ImageResource.Builder()
        .setAndroidLottieResourceByResId(
            ResourceBuilders.AndroidLottieResourceByResId.Builder(R.raw.lottie)
                .setStartTrigger(createOnVisibleTrigger())
                .build()
        )
        // Fallback if lottie is not supported
        .setAndroidResourceByResId(
            ResourceBuilders.AndroidImageResourceByResId.Builder()
                .setResourceId(R.drawable.lottie_fallback)
                .build()
        )
        .build()

Realiza pruebas con diferentes versiones del renderizador

Para probar tus tarjetas en diferentes versiones del renderizador, impleméntalas en diferentes versiones del emulador de Wear OS. (En los dispositivos físicos, Play Store o las actualizaciones del sistema proporcionan las actualizaciones del renderizador de ProtoLayout. No es posible forzar la instalación de una versión específica del renderizador.

La función de vista previa de la tarjeta de Android Studio usa un renderizador integrado en la biblioteca de Jetpack ProtoLayout de la que depende tu código, por lo que otro enfoque es depender de diferentes versiones de la biblioteca de Jetpack cuando pruebas tarjetas.

Migra a Tiles 1.5 o ProtoLayout 1.3 (Material 3 Expressive)

Actualiza tus bibliotecas de Jetpack Tile para aprovechar las mejoras más recientes, incluidos los cambios en la IU para que tus Tiles se integren sin problemas con el sistema.

Jetpack Tiles 1.5 y Jetpack ProtoLayout 1.3 presentan varias mejoras y cambios notables. Por ejemplo:

  • Una API similar a Compose para describir la IU.
  • Componentes Material 3 Expressive, incluido el botón de borde que se ajusta a la parte inferior y compatibilidad con elementos visuales mejorados: animaciones de Lottie, más tipos de gradientes y nuevos estilos de líneas de arco. - Nota: Algunas de estas funciones también se pueden usar sin migrar a la nueva API.

Recomendaciones

  • Migra todas tus tarjetas de forma simultánea. Evita mezclar versiones de tarjetas en tu app. Si bien los componentes de Material 3 residen en un artefacto independiente (androidx.wear.protolayout:protolayout-material3), lo que hace que técnicamente sea posible usar tarjetas de M2.5 y M3 en la misma app, te recomendamos que no uses este enfoque, a menos que sea absolutamente necesario (por ejemplo, si tu app tiene una gran cantidad de tarjetas que no se pueden migrar todas a la vez).
  • Adopta la orientación de UX de las tarjetas. Dada la naturaleza altamente estructurada y basada en plantillas de las tarjetas, usa los diseños de los ejemplos existentes como puntos de partida para tus propios diseños.
  • Realiza pruebas en una variedad de tamaños de pantalla y fuente. Los mosaicos suelen contener mucha información, por lo que el texto (en especial, cuando se coloca en botones) es susceptible a desbordarse y recortarse. Para minimizar esto, usa los componentes creados previamente y evita la personalización extensa. Realiza pruebas con la función de vista previa de la tarjeta de Android Studio y en varios dispositivos reales.

Proceso de migración

Actualiza las dependencias

Primero, actualiza tu archivo build.gradle.kts. Actualiza las versiones y cambia la dependencia protolayout-material a protolayout-material3, como se muestra a continuación:

// In build.gradle.kts

//val tilesVersion = "1.4.1"
//val protoLayoutVersion = "1.2.1"

// Use these versions for M3.
val tilesVersion = "1.5.0-rc01"
val protoLayoutVersion = "1.3.0-rc01"

 dependencies {
     // Use to implement support for wear tiles
     implementation("androidx.wear.tiles:tiles:$tilesVersion")

     // Use to utilize standard components and layouts in your tiles
     implementation("androidx.wear.protolayout:protolayout:$protoLayoutVersion")

     // Use to utilize components and layouts with Material Design in your tiles
     // implementation("androidx.wear.protolayout:protolayout-material:$protoLayoutVersion")
     implementation("androidx.wear.protolayout:protolayout-material3:$protoLayoutVersion")

     // Use to include dynamic expressions in your tiles
     implementation("androidx.wear.protolayout:protolayout-expression:$protoLayoutVersion")

     // Use to preview wear tiles in your own app
     debugImplementation("androidx.wear.tiles:tiles-renderer:$tilesVersion")

     // Use to fetch tiles from a tile provider in your tests
     testImplementation("androidx.wear.tiles:tiles-testing:$tilesVersion")
 }

TileService permanece casi sin cambios

Los cambios principales en esta migración afectan a los componentes de la IU. Por lo tanto, tu implementación de TileService, incluidos los mecanismos de carga de recursos, debería requerir modificaciones mínimas o nulas.

La principal excepción involucra el seguimiento de la actividad de la tarjeta: si tu app usa onTileEnterEvent() o onTileLeaveEvent(), debes migrar a onRecentInteractionEventsAsync(). A partir de la API 36, estos eventos se procesarán por lotes.

Adapta tu código de generación de diseño

En ProtoLayout 1.2 (M2.5), el método onTileRequest() devuelve un TileBuilders.Tile. Este objeto contenía varios elementos, incluido un TimelineBuilders.Timeline, que a su vez contenía el LayoutElement que describía la IU de la tarjeta.

Con ProtoLayout 1.3 (M3), si bien no cambiaron el flujo ni la estructura de datos generales, el LayoutElement ahora se construye con un enfoque inspirado en Compose con un diseño basado en ranuras definidas que son (de arriba hacia abajo) el titleSlot (opcional; por lo general, para un título o encabezado principal), el mainSlot (obligatorio; para el contenido principal) y el bottomSlot (opcional; a menudo, para acciones como un botón de borde o información complementaria, como texto breve). Este diseño se construye con la función primaryLayout().

Diseño de una tarjeta que muestra mainSlot, titleSlot y bottomSlot
Figura 1: Son las ranuras de una tarjeta.
Comparación de las funciones de diseño de M2.5 y M3

M2.5

fun myLayout(
    context: Context,
    deviceConfiguration: DeviceParametersBuilders.DeviceParameters
) =
    PrimaryLayout.Builder(deviceConfiguration)
        .setResponsiveContentInsetEnabled(true)
        .setContent(
            Text.Builder(context, "Hello World!")
                .setTypography(Typography.TYPOGRAPHY_BODY1)
                .setColor(argb(0xFFFFFFFF.toInt()))
                .build()
        )
        .build()

M3

fun myLayout(
    context: Context,
    deviceConfiguration: DeviceParametersBuilders.DeviceParameters,
) =
    materialScope(context, deviceConfiguration) {
        primaryLayout(mainSlot = { text("Hello, World!".layoutString) })
    }

Para destacar las diferencias clave, haz lo siguiente:

  1. Eliminación de Builders El patrón de compilador tradicional para los componentes de IU de Material 3 se reemplaza por una sintaxis más declarativa inspirada en Compose. (Los componentes que no son de la IU, como String, Color y Modifiers, también obtienen nuevos wrappers de Kotlin).
  2. Funciones de diseño y de inicialización estandarizadas. Los diseños de M3 se basan en funciones de inicialización y estructura estandarizadas: materialScope() y primaryLayout(). Estas funciones obligatorias inicializan el entorno de M3 (tematización, alcance de componentes a través de materialScope) y definen el diseño principal basado en ranuras (a través de primaryLayout). Ambas deben llamarse exactamente una vez por diseño.

Temas

Color

Una función destacada de Material 3 Expressive es el "tema dinámico": Los mosaicos que habilitan esta función (activada de forma predeterminada) se mostrarán en el tema proporcionado por el sistema (la disponibilidad depende del dispositivo y la configuración del usuario).

Otro cambio en M3 es la expansión de la cantidad de tokens de color, que aumentó de 4 a 29. Los nuevos tokens de color se pueden encontrar en la clase ColorScheme.

Tipografía

Al igual que M2.5, M3 se basa en gran medida en constantes de tamaño de fuente predefinidas, por lo que no se recomienda especificar un tamaño de fuente directamente. Estas constantes se encuentran en la clase Typography y ofrecen un rango ligeramente más amplio de opciones más expresivas.

Para obtener información detallada, consulta la documentación sobre tipografía.

Forma

La mayoría de los componentes de M3 pueden variar en la dimensión de forma y color.

Un textButton (en mainSlot) con la forma full:

Tarjeta con forma "completa" (esquinas más redondeadas)
Figura 2.: Mosaico con forma "completa"

El mismo textButton con la forma small:

Tarjeta con forma "pequeña" (esquinas menos redondeadas)
Figura 3: Mosaico con forma “pequeña”

Componentes

Los componentes de M3 son mucho más flexibles y configurables que sus contrapartes de M2.5. Mientras que M2.5 a menudo requería componentes distintos para los diferentes tratamientos visuales, M3 suele emplear un componente "base" generalizado, pero altamente configurable, con buenos valores predeterminados.

Este principio se aplica al diseño "raíz". En M2.5, podía ser un PrimaryLayout o un EdgeContentLayout. En M3, después de que se establece un solo MaterialScope de nivel superior, se llama a la función primaryLayout(). Esto devuelve el diseño raíz directamente (no se necesitan compiladores) y acepta LayoutElements para varias “ranuras”, como titleSlot, mainSlot y bottomSlot. Estas ranuras se pueden completar con componentes de IU concretos, como los que devuelven text(), button() o card(), o con estructuras de diseño, como Row o Column de LayoutElementBuilders.

Los temas representan otra mejora clave de M3. De forma predeterminada, los elementos de la IU cumplen automáticamente con las especificaciones de diseño de M3 y admiten la aplicación de temas dinámicos.

M2.5 M3
Elementos interactivos
Button o Chip
Texto
Text text()
Indicadores de progreso
CircularProgressIndicator circularProgressIndicator() o segmentedCircularProgressIndicator()
Diseño
PrimaryLayout o EdgeContentLayout primaryLayout()
buttonGroup()
Imágenes
icon(), avatarImage() o backgroundImage()

Modificadores

En M3, los Modifiers, que usas para decorar o aumentar un componente, se parecen más a Compose. Este cambio puede reducir el código repetitivo, ya que construye automáticamente los tipos internos adecuados. (Este cambio es ortogonal al uso de componentes de la IU de M3; si es necesario, puedes usar modificadores de estilo de compilador de ProtoLayout 1.2 con componentes de la IU de M3 y viceversa).

M2.5

// A Builder-style modifier to set the opacity of an element to 0.5
fun myModifier(): ModifiersBuilders.Modifiers =
    ModifiersBuilders.Modifiers.Builder()
        .setOpacity(TypeBuilders.FloatProp.Builder(0.5F).build())
        .build()

M3

// The equivalent Compose-like modifier is much simpler
fun myModifier(): LayoutModifier = LayoutModifier.opacity(0.5F)

Puedes construir modificadores con cualquiera de los estilos de API y también puedes usar la función de extensión toProtoLayoutModifiers() para convertir un LayoutModifier en un ModifiersBuilders.Modifier.

Funciones auxiliares

Si bien ProtoLayout 1.3 permite expresar muchos componentes de la IU con una API inspirada en Compose, los elementos de diseño fundamentales, como filas y columnas de LayoutElementBuilders, siguen usando el patrón de compilador. Para subsanar esta brecha de estilo y promover la coherencia con las nuevas APIs de componentes de M3, considera usar funciones auxiliares.

Sin ayuda

primaryLayout(
    mainSlot = {
        LayoutElementBuilders.Column.Builder()
            .setWidth(expand())
            .setHeight(expand())
            .addContent(text("A".layoutString))
            .addContent(text("B".layoutString))
            .addContent(text("C".layoutString))
            .build()
    }
)

Con ayudas

// Function literal with receiver helper function
fun column(builder: Column.Builder.() -> Unit) =
    Column.Builder().apply(builder).build()

primaryLayout(
    mainSlot = {
        column {
            setWidth(expand())
            setHeight(expand())
            addContent(text("A".layoutString))
            addContent(text("B".layoutString))
            addContent(text("C".layoutString))
        }
    }
)

Migra a Tiles 1.2 o ProtoLayout 1.0

A partir de la versión 1.2, la mayoría de las APIs de diseño de tarjetas está en el espacio de nombres androidx.wear.protolayout. Para usar las APIs más recientes, completa los siguientes pasos de migración en tu código.

Actualiza las dependencias

En el archivo de compilación del módulo de tu app, realiza los siguientes cambios:

Groovy

  // Remove
  implementation 'androidx.wear.tiles:tiles-material:version'

  // Include additional dependencies
  implementation "androidx.wear.protolayout:protolayout:1.3.0"
  implementation "androidx.wear.protolayout:protolayout-material:1.3.0"
  implementation "androidx.wear.protolayout:protolayout-expression:1.3.0"

  // Update
  implementation "androidx.wear.tiles:tiles:1.5.0"

Kotlin

  // Remove
  implementation("androidx.wear.tiles:tiles-material:version")

  // Include additional dependencies
  implementation("androidx.wear.protolayout:protolayout:1.3.0")
  implementation("androidx.wear.protolayout:protolayout-material:1.3.0")
  implementation("androidx.wear.protolayout:protolayout-expression:1.3.0")

  // Update
  implementation("androidx.wear.tiles:tiles:1.5.0")

Actualiza los espacios de nombres

En los archivos de código basados en Kotlin y Java de tu app, realiza las siguientes actualizaciones. También puedes ejecutar esta secuencia de comandos de cambio de nombres de los espacios de nombres.

  1. Reemplaza todas las importaciones de androidx.wear.tiles.material.* por androidx.wear.protolayout.material.*. Realiza también este paso para la biblioteca androidx.wear.tiles.material.layouts.
  2. Reemplaza la mayoría de las demás importaciones de androidx.wear.tiles.* por androidx.wear.protolayout.*.

    Las importaciones de androidx.wear.tiles.EventBuilders, androidx.wear.tiles.RequestBuilders, androidx.wear.tiles.TileBuilders y androidx.wear.tiles.TileService deben permanecer iguales.

  3. Cambia el nombre de algunos métodos obsoletos de las clases TileService y TileBuilder:

    1. TileBuilders: getTimeline() por getTileTimeline() y setTimeline() por setTileTimeline()
    2. TileService: onResourcesRequest() por onTileResourcesRequest()
    3. RequestBuilders.TileRequest: getDeviceParameters() por getDeviceConfiguration(), setDeviceParameters() por setDeviceConfiguration(), getState() por getCurrentState() y setState() por setCurrentState()