Controle de versões dos Blocos

Em dispositivos Wear OS, os blocos são renderizados por dois componentes principais com versões independentes. Para que os blocos do app funcionem corretamente em todos os dispositivos, é importante entender essa arquitetura subjacente.

  • Bibliotecas relacionadas a blocos do Jetpack: essas bibliotecas (incluindo Wear Tiles e Wear ProtoLayout) estão incorporadas ao app, e você, como desenvolvedor, controla as versões delas. O app usa essas bibliotecas para construir um TileBuilder.Tile objeto (a estrutura de dados que representa o bloco) em resposta à chamada onTileRequest() do sistema.
  • Renderizador ProtoLayout:esse componente do sistema é responsável por renderizar o objeto Tile na tela e processar as interações do usuário. A versão do renderizador não é controlada pelo desenvolvedor de apps e pode variar entre dispositivos, mesmo aqueles com hardware idêntico.

A aparência ou o comportamento de um bloco podem variar com base nas versões da biblioteca Jetpack Tiles do app e na versão do renderizador ProtoLayout no dispositivo do usuário. Por exemplo, um dispositivo pode oferecer suporte à rotação ou à exibição de dados de frequência cardíaca, enquanto outro não.

Este documento explica como tornar seu app compatível com diferentes versões da biblioteca Tiles e do renderizador ProtoLayout. Ele também explica como migrar para versões mais recentes da biblioteca Jetpack.

Considerar a compatibilidade

Para criar um bloco que funcione corretamente em vários dispositivos, considere o suporte a recursos variados. Você pode fazer isso usando duas estratégias principais: detectar os recursos do renderizador no momento da execução e fornecer substituições integradas.

Detectar recursos do renderizador

É possível mudar dinamicamente o layout do bloco com base nos recursos disponíveis em um determinado dispositivo.

Detectar a versão do renderizador

  • Use o getRendererSchemaVersion() método do DeviceParameters objeto transmitido ao seu onTileRequest() método. Esse método retorna os números de versão principal e secundária do renderizador ProtoLayout no dispositivo.
  • Em seguida, você pode usar a lógica condicional na implementação de onTileRequest() para adaptar o design ou o comportamento do bloco com base na versão do renderizador detectada.

A anotação @RequiresSchemaVersion

  • A anotação @RequiresSchemaVersion em métodos ProtoLayout indica a versão mínima do esquema do renderizador necessária para que esse método se comporte conforme documentado (exemplo).
    • Embora chamar um método que exige uma versão do renderizador mais recente do que a disponível no dispositivo não cause falha no app, isso pode fazer com que o conteúdo não seja exibido ou que o recurso seja ignorado.

Exemplo de detecção de versão

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()
    }

Fornecer substituições

Alguns recursos permitem definir uma substituição diretamente no builder. Isso geralmente é mais simples do que verificar a versão do renderizador e é a abordagem preferida quando disponível.

Um caso de uso comum é fornecer uma imagem estática como substituição para uma animação Lottie. Se o dispositivo não oferecer suporte a animações Lottie, ele vai renderizar a imagem 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()

Testar com diferentes versões do renderizador

Para testar os blocos em diferentes versões do renderizador, implante-os em diferentes versões do emulador do Wear OS. Em dispositivos físicos, as atualizações do renderizador ProtoLayout são entregues pela Google Play Store ou pelas atualizações do sistema. Não é possível forçar a instalação de uma versão específica do renderizador.

O recurso de visualização de blocos do Android Studio usa um renderizador incorporado à biblioteca Jetpack ProtoLayout de que seu código depende. Portanto, outra abordagem é depender de diferentes versões da biblioteca Jetpack ao testar blocos.

Migrar para Tiles 1.5 / ProtoLayout 1.3 (Material 3 Expressive)

Atualize as bibliotecas Jetpack Tile para aproveitar os aprimoramentos mais recentes, incluindo mudanças na interface para que os blocos sejam integrados perfeitamente ao sistema.

O Jetpack Tiles 1.5 e o Jetpack ProtoLayout 1.3 introduzem várias melhorias e mudanças notáveis. São elas:

  • Uma API semelhante ao Compose para descrever a interface.
  • Componentes do Material 3 Expressive, incluindo o botão de borda inferior e suporte a recursos visuais aprimorados: animações Lottie, mais tipos de gradiente e novos estilos de linha de arco. - Observação: alguns desses recursos também podem ser usados sem migrar para a nova API.

Recomendações

Siga estas recomendações ao migrar seus blocos:

  • Migre todos os blocos simultaneamente. Evite misturar versões de blocos no app. Embora os componentes do Material 3 residam em um artefato separado (androidx.wear.protolayout:protolayout-material3), o que torna tecnicamente possível usar blocos M2.5 e M3 no mesmo app, recomendamos não usar essa abordagem, a menos que seja absolutamente necessário (por exemplo, se o app tiver um grande número de blocos que não podem ser migrados de uma só vez).
  • Adote as orientações de UX do Tiles. Devido à natureza altamente estruturada e com modelos dos blocos, use os designs nos exemplos atuais como pontos de partida para seus próprios designs.
  • Teste em uma variedade de tamanhos de tela e fonte. Os blocos geralmente são densos em informações, o que torna o texto (especialmente quando colocado em botões) suscetível a estouro e recorte. Para minimizar isso, use os componentes pré-criados e evite personalizações extensas. Teste usando o recurso de visualização de blocos do Android Studio's e em vários dispositivos reais.

Processo de migração

Para migrar seus blocos, siga estas etapas:

Atualizar dependências

Primeiro, atualize o arquivo build.gradle.kts. Atualize as versões e mude a protolayout-material dependência para protolayout-material3, conforme mostrado:

// In build.gradle.kts

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

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

 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")
 }

O TileService permanece praticamente inalterado

As principais mudanças nessa migração afetam os componentes da interface. Consequentemente, a implementação do TileService, incluindo todos os mecanismos de carregamento de recursos, deve exigir modificações mínimas ou nenhuma.

A principal exceção envolve o rastreamento de atividades de blocos: se o app usar onTileEnterEvent() ou onTileLeaveEvent(), recomendamos que você migre para onRecentInteractionEventsAsync(). A partir da API 36, esses eventos serão agrupados.

Adaptar o código de geração de layout

No ProtoLayout 1.2 (M2.5), o onTileRequest() método retorna um TileBuilders.Tile. Esse objeto continha vários elementos, incluindo um TimelineBuilders.Timeline, que, por sua vez, continha o LayoutElement que descreve a interface do bloco.

Com o ProtoLayout 1.3 (M3), embora a estrutura e o fluxo de dados gerais não tenham mudado, o LayoutElement agora é construído usando uma abordagem inspirada no Compose com um layout baseado em slots definidos, que são (de cima para baixo) o titleSlot (opcional; normalmente para um título ou cabeçalho principal), mainSlot (obrigatório; para o conteúdo principal) e bottomSlot (opcional; geralmente para ações como um botão de borda ou informações complementares, como texto curto). Esse layout é construído pela primaryLayout() função.

O layout de um bloco que mostra mainSlot, titleSlot e bottomSlot
Figura 1.: Slots de um bloco.
Comparação das funções de layout M2.5 e 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)
                .build()
        )
        .build()

M3

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

Para destacar as principais diferenças:

  1. Eliminação de builders. O padrão de builder anterior para componentes da interface do Material Design foi substituído por uma sintaxe mais declarativa e inspirada no Compose. Os componentes não relacionados à interface, como String/Color/Modifiers, também recebem novos wrappers do Kotlin.
  2. Funções de inicialização e layout padronizadas. Os layouts do M3 dependem de funções de inicialização e estrutura padronizadas: materialScope() e primaryLayout(). Essas funções obrigatórias inicializam o ambiente M3 (temas, escopo de componentes usando materialScope) e definem o layout principal baseado em slots (usando primaryLayout). Ambos precisam ser chamados exatamente uma vez por layout.

Temas

O Material 3 introduz várias mudanças nos temas, incluindo cores dinâmicas e um conjunto expandido de opções de tipografia e forma.

Cor

Um recurso de destaque do Material 3 Expressive é o "tema dinâmico": os blocos que ativam esse recurso (ativado por padrão) serão exibidos no tema fornecido pelo sistema (a disponibilidade depende do dispositivo e da configuração do usuário).

Outra mudança no M3 é a expansão do número de tokens de cor, que aumentou de 4 para 29. Os novos tokens de cor podem ser encontrados na ColorScheme classe.

Tipografia

Semelhante ao M2.5, o M3 depende muito de constantes de tamanho de fonte predefinidas. Não é recomendável especificar um tamanho de fonte diretamente. Essas constantes estão localizadas na Typography classe e oferecem um intervalo ligeiramente expandido de opções mais expressivas.

Para mais detalhes, consulte a documentação de tipografia.

Forma

A maioria dos componentes do M3 pode variar na dimensão da forma e da cor.

Um textButton (no mainSlot) com a forma full:

Bloco com forma "cheia" (cantos mais arredondados)
Figura 2.: Bloco com forma "full"

O mesmo textButton com a forma small:

Bloco com formato "pequeno" (cantos menos arredondados)
Figura 3.: Bloco com forma "small"

Componentes

Os componentes do M3 são mais flexíveis e configuráveis do que os do M2.5. O M2.5 geralmente exigia componentes distintos para tratamentos visuais variados, enquanto o M3 costuma usar um componente de base generalizado e altamente configurável com bons padrões.

Esse princípio também se aplica ao layout raiz. No M2.5, ele era um PrimaryLayout ou um EdgeContentLayout. No M3, depois de você estabelecer um único MaterialScope de nível superior, você chama a primaryLayout() função. Essa função retorna o layout raiz diretamente (nenhum builder é necessário) e aceita LayoutElements para vários slots, como titleSlot, mainSlot e bottomSlot. É possível preencher esses slots com componentes de interface concretos, como os retornados por text(), button() ou card(), ou com estruturas de layout, como Row ou Column de LayoutElementBuilders.

Os temas são outro aprimoramento importante do M3. Por padrão, os elementos da interface seguem automaticamente as especificações de estilo do M3 e oferecem suporte a temas dinâmicos.

M2.5 M3
Elementos interativos
Button ou Chip
Texto
Text text()
Indicadores de progresso
CircularProgressIndicator circularProgressIndicator() ou segmentedCircularProgressIndicator()
Layout
PrimaryLayout ou EdgeContentLayout primaryLayout()
buttonGroup()
Imagens
icon(), avatarImage() ou backgroundImage()

Modificadores

No M3, Modifiers, que você usa para decorar ou aumentar um componente, são mais parecidos com o Compose. Essa mudança pode reduzir o código boilerplate construindo automaticamente os tipos internos apropriados. Essa mudança é ortogonal ao uso de componentes de interface do M3. Se necessário, você pode usar modificadores de estilo de builder do ProtoLayout 1.2 com componentes de interface do M3 e vice-versa.

M2.5

// Uses Builder-style modifier to set opacity
fun myModifier(): ModifiersBuilders.Modifiers =
    ModifiersBuilders.Modifiers.Builder()
        .setOpacity(TypeBuilders.FloatProp.Builder(0.5F).build())
        .build()

M3

// Uses Compose-like modifiers to set opacity
fun myModifier(): LayoutModifier = LayoutModifier.opacity(0.5F)

É possível construir modificadores usando qualquer estilo de API, e também é possível usar a toProtoLayoutModifiers() função de extensão para converter um LayoutModifier em um ModifiersBuilders.Modifier.

Funções auxiliares

Embora o ProtoLayout 1.3 permita que muitos componentes da interface sejam expressos usando uma API inspirada no Compose, elementos de layout fundamentais, como linhas e colunas de LayoutElementBuilders continuam usando o padrão de builder. Para preencher essa lacuna estilística e promover a consistência com as novas APIs de componentes do M3, considere o uso de funções auxiliares.

Sem auxiliares

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

Com auxiliares

// 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))
        }
    }
)

Migrar para Tiles 1.2 / ProtoLayout 1.0

Na versão 1.2 e mais recentes, a maioria das APIs de layout de blocos está no namespace androidx.wear.protolayout. Para usar as APIs mais recentes, siga as etapas de migração abaixo no seu código.

Atualizar dependências

No arquivo de build do módulo do app, faça estas mudanças:

Groovy

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

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

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

Kotlin

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

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

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

Atualizar namespaces

Nos arquivos de código com base em Kotlin e Java do seu app, faça estas mudanças: Como alternativa, você pode executar este script de renomeação de namespace.

  1. Substitua todas as importações androidx.wear.tiles.material.* por androidx.wear.protolayout.material.*. Conclua essa etapa para a biblioteca androidx.wear.tiles.material.layouts também.
  2. Substitua a maioria das outras importações androidx.wear.tiles.* por androidx.wear.protolayout.*.

    As importações para androidx.wear.tiles.EventBuilders, androidx.wear.tiles.RequestBuilders, androidx.wear.tiles.TileBuilders e androidx.wear.tiles.TileService precisam ser as mesmas.

  3. Renomeie alguns métodos descontinuados das classes TileService e TileBuilder:

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