Criar seu primeiro Bloco no Wear OS

1. Introdução

relógio animado, usuário deslizando o mostrador do relógio para o primeiro bloco, que é de previsão do tempo, depois para um bloco de timer e depois deslizando de volta

Os Blocos do Wear OS oferecem fácil acesso às informações e ações de que os usuários precisam para realizar tarefas. Com um simples gesto de deslizar no mostrador do relógio, o usuário pode ver a previsão mais recente ou iniciar um timer.

Um bloco é executado como parte da IU do sistema em vez de ser executado no próprio contêiner do aplicativo. Usamos um Serviço para descrever o layout e o conteúdo do bloco. A IU do sistema vai renderizar o bloco quando necessário.

O que você vai fazer

35a459b77a2c9d52.png

Você vai criar um bloco para um app de mensagens que mostra conversas recentes. Nele, o usuário pode realizar três tarefas comuns:

  • Abrir uma conversa
  • Pesquisar uma conversa
  • Escrever uma nova mensagem

O que você vai aprender

Neste codelab, você vai aprender a criar seu próprio bloco do Wear OS, incluindo como:

  • Criar um TileService.
  • Testar um bloco em um dispositivo.
  • Visualizar a IU de um bloco no Android Studio.
  • Desenvolver a IU de um bloco.
  • Adicionar imagens
  • Processar interações

Pré-requisitos

2. Etapas da configuração

Nesta etapa, você configurará seu ambiente e fará o download de um projeto inicial.

O que é preciso

Caso não saiba usar o Wear OS, leia este guia rápido (em inglês) antes de começar. Ele inclui instruções para a configuração do emulador do Wear OS e descreve como navegar pelo sistema.

Fazer o download do código

Se você tiver o git instalado, execute o comando abaixo para clonar o código deste repositório (link em inglês).

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

Caso você não tenha o git, clique no botão abaixo para fazer o download de todo o código para este codelab:

Abrir o projeto no Android Studio

Na janela "Welcome to Android Studio", selecione c01826594f360d94.png Open an Existing Project ou File > Open e selecione a pasta [Download Location]

3. Criar um bloco básico

O ponto de entrada de um bloco é o serviço de bloco. Nesta etapa, você vai registrar um serviço de bloco e definir um layout para o bloco.

HelloWorldTileService

Uma classe que implementa o TileService precisa especificar dois métodos:

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

O primeiro método retorna um objeto Resources que mapeia IDs de string para os recursos de imagem que vamos usar no bloco.

A segunda retorna uma descrição de um bloco, incluindo o layout dele. É aqui que definimos o layout de um bloco e como os dados são vinculados a ele.

Abra HelloWorldTileService.kt no módulo start. Todas as mudanças vão ser feitas neste módulo. Há também um módulo finished se você quiser dar uma olhada no resultado deste codelab.

O HelloWorldTileService estende o SuspendingTileService, um wrapper com suporte para corrotinas do Kotlin da biblioteca Horologist Tiles (link em inglês). O Horologist é um grupo de bibliotecas do Google que visa fornecer aos desenvolvedores do Wear OS recursos que normalmente são exigidos por eles, mas que ainda não estão disponíveis no Jetpack.

O SuspendingTileService fornece duas funções de suspensão, que são equivalentes de corrotina das funções do TileService:

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

Para saber mais sobre corrotinas, consulte a documentação sobre Corrotinas do Kotlin no Android.

O HelloWorldTileService ainda não foi concluído. Precisamos registrar o serviço no nosso manifesto e fornecer uma implementação para o tileLayout.

Registrar o serviço do bloco

Depois que o serviço do bloco é registrado no manifesto, ele aparece na lista de blocos disponíveis para o usuário adicionar.

Adicione o <service> ao 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>

O ícone e o rótulo são usados, como um marcador, quando o bloco é carregado pela primeira vez ou se ocorre um erro ao carregar o bloco. Os metadados no fim definem uma imagem de visualização que é mostrada no carrossel quando o usuário está adicionando um bloco.

Definir um layout para o bloco

O HelloWorldTileService tem uma função com o nome tileLayout com uma TODO() como o corpo. Vamos substituir isso por uma implementação em que definimos o layout do bloco e vinculamos os dados:

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

Criamos um elemento Text e o definimos dentro de uma Box para que possamos fazer um alinhamento básico.

Você criou seu primeiro bloco do Wear OS. Vamos instalar esse bloco e ver a aparência dele.

4. Testar o bloco em um dispositivo

Com o módulo inicial selecionado no menu suspenso para a configuração de execução, você pode instalar o app (o módulo start) no dispositivo ou emulador e instalar manualmente o bloco, como um usuário faria.

No entanto, para desenvolvimento, vamos usar o Direct Surface Launch, um recurso introduzido com o Android Studio Dolphin, para criar uma nova configuração de execução que inicia o bloco diretamente no Android Studio. Selecione "Edit Configurations…" no menu suspenso no painel de cima.

Menu suspenso para a configuração de execução no painel superior do Android Studio. A opção "Edit Configurations…" está destacada.

Clique no botão "Add new configuration" e escolha a opção "Wear OS Tile". Adicione um nome descritivo e selecione o módulo Tiles_Code_Lab.start e o bloco HelloWorldTileService.

Pressione "OK" para concluir.

Menu "Edit Configuration" com um bloco do Wear OS chamado "HelloTile" sendo configurado.

O Direct Surface Launch permite testar os blocos rapidamente em um emulador ou dispositivo físico do Wear OS. Para testar, execute o "HelloTile". Ele vai ficar parecido com a captura de tela abaixo.

Relógio redondo mostrando a mensagem "Time to create a tile" ("Hora de criar um Bloco") escrita em branco sobre um fundo preto

5. Criar um bloco de mensagens

Relógio redondo mostrando cinco botões redondos organizados em uma pirâmide 2 x 3. O primeiro e o terceiro botão mostram as iniciais em um texto roxo, o segundo e o quarto mostram fotos do perfil, e o último botão é um ícone de pesquisa. Abaixo dos botões, há um ícone compacto roxo que mostra a mensagem "New" em texto preto.

O bloco de mensagens que estamos prestes a criar é mais parecido com um bloco do mundo real. Ao contrário do exemplo HelloWorld, ele carrega dados de um repositório local, busca imagens para mostrar na rede e processa as interações para abrir o app, diretamente do bloco.

MessagingTileService

MessagingTileService estende a classe SuspendingTileService que conferimos anteriormente.

A principal diferença entre este e o exemplo anterior é que agora estamos observando os dados do repositório e buscando dados de imagem da rede.

MessagingTileRenderer

MessagingTileRenderer estende a classe SingleTileLayoutRenderer, que é outra abstração do Horologist Tiles. Ela é completamente síncrona: o estado é transmitido para as funções do renderizador, o que facilita o uso em testes e visualizações do Android Studio.

Na próxima etapa, vamos aprender como adicionar visualizações de blocos do Android Studio.

6. Adicionar funções de visualização

Podemos conferir a interface do bloco no Android Studio usando as funções de visualização de blocos lançadas na versão 1.4 da biblioteca Jetpack Tiles (atualmente na versão alfa). Isso encurta o ciclo de feedback ao desenvolver a interface, aumentando a velocidade de desenvolvimento.

Adicione uma visualização de bloco para o MessagingTileRenderer no final do arquivo.

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

Observe que a anotação @Composable não é fornecida, embora os blocos usem a mesma interface de visualização das Funções combináveis. Os blocos não usam o Compose e não são combináveis.

Use o modo de edição "Split" para conferir o bloco:

tela dividida do Android Studio com o código de visualização à esquerda e uma imagem do bloco à direita.

Na próxima etapa, vamos usar o Tiles Material para atualizar o layout.

7. Adicionar o Tiles Material

O Tiles Material oferece componentes e layouts do Material Design pré-criados, permitindo que você crie blocos que usam o Material Design mais recente para Wear OS.

Adicione a dependência do Tiles Material ao arquivo build.gradle:

start/build.gradle

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

Adicione a visualização e o código do botão na parte de baixo do arquivo do renderizador:

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

Também podemos fazer algo semelhante para criar o layout dos contatos:

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

O Tiles Material não inclui apenas componentes. Em vez de usar uma série de colunas e linhas aninhadas, podemos usar layouts do Tiles Material para alcançar rapidamente a aparência desejada.

Aqui, podemos usar PrimaryLayout e MultiButtonLayout para organizar quatro contatos e o botão de pesquisa. Atualize a função messagingTileLayout() no MessagingTileRenderer com estes layouts:

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 oferece suporte para até sete botões e os mostra com o espaçamento adequado para você.

Vamos adicionar um CompactChip "New" como o ícone "principal" do PrimaryLayout na função 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

Na próxima etapa, vamos corrigir as imagens ausentes.

8. Adicionar imagens

De modo geral, os blocos consistem em dois itens: elementos de layout (que fazem referência a recursos por IDs de string) e os próprios recursos (que podem ser imagens).

Disponibilizar uma imagem local é uma tarefa simples: embora não seja possível usar recursos drawable do Android diretamente, é possível convertê-los no formato necessário usando uma função de conveniência fornecida pelo Horologist. Em seguida, use a função addIdToImageMapping para associar a imagem ao identificador de recursos. Exemplos:

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

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

Para imagens remotas, use o Coil (link em inglês), um carregador de imagens Kotlin baseado em corrotinas, para carregar pela rede.

O código já está pronto:

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 o renderizador de blocos é completamente síncrono, o serviço de blocos busca bitmaps na rede. Como antes, dependendo do tamanho da imagem, pode ser mais apropriado usar o WorkManager para buscar as imagens com antecedência, mas neste codelab, vamos fazer a busca direta.

O mapa avatars (Contact para o Bitmap) é transmitido para o renderizador como "state" para os recursos. Agora, o renderizador pode transformar esses bitmaps em recursos de imagem para blocos.

Este código também já 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()
        )
    }
}

Portanto, se o serviço está buscando os bitmaps, e o renderizador está transformando esses bitmaps em recursos de imagem, por que o bloco não está mostrando imagens?

Ele está! Se você executa o bloco em um dispositivo com acesso à Internet, as imagens são carregadas. O problema está apenas na visualização, porque não transmitimos recursos para TilePreviewData().

Para o bloco real, estamos buscando bitmaps da rede e os mapeando para diferentes contatos. No entanto, para visualizações e testes, não é necessário acessar a rede.

Precisamos fazer duas mudanças. Primeiro, crie uma função previewResources() que retorne um 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()

Depois, atualize messagingTileLayoutPreview() para transmitir os 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
        )
    }
}

Agora, se atualizarmos a visualização, as imagens vão aparecer:

3142b42717407059.png

Na próxima etapa, vamos processar os cliques em cada um dos elementos.

9. Processar interações

Uma das coisas mais úteis que podemos fazer com um bloco é fornecer atalhos para as jornadas ideais do usuário. Isso é diferente do Acesso rápido aos apps, que apenas abre o app. Aqui, temos espaço para fornecer atalhos contextuais a uma tela específica do app.

Até agora, usamos emptyClickable para o ícone e cada um dos botões. Isso é bom para visualizações, que não são interativas, mas vamos conferir como adicionar ações aos elementos.

Dois builders da classe "ActionBuilders" definem ações clicáveis: LoadAction e LaunchAction.

LoadAction

Uma LoadAction pode ser usada se você quer executar a lógica no serviço de bloco quando o usuário clica em um elemento, por exemplo, para incrementar um contador.

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

Quando ele for clicado, onTileRequest vai ser chamado no serviço (tileRequest em SuspendingTileService). Portanto, essa é uma boa oportunidade para atualizar a IU do bloco:

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

LaunchAction

LaunchAction pode ser usada para iniciar uma atividade. Em MessagingTileRenderer, vamos atualizar o recurso clicável para o botão de pesquisa.

O botão de pesquisa é definido pela função searchLayout() no MessagingTileRenderer. Ele já usa um Clickable como parâmetro. Porém, até agora, estavamos transmitindo emptyClickable, uma implementação que não faz nada quando o botão é clicado.

Vamos atualizar o messagingTileLayout() para que ele transmita uma ação de clique real.

  1. Adicione um novo parâmetro, searchButtonClickable (do tipo ModifiersBuilders.Clickable).
  2. Transmita-o para a função searchLayout() atual.

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

Também precisamos atualizar renderTile, que é o lugar em que chamamos messagingTileLayout, já que adicionamos um novo parâmetro (searchButtonClickable). Vamos usar a função launchActivityClickable() para criar um novo elemento clicável, transmitindo openSearch() ActionBuilder como a ação:

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

Abra launchActivityClickable para conferir como essas funções (já definidas) funcionam:

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

Ela é muito semelhante a LoadAction, a principal diferença é que chamamos o setAndroidActivity. No mesmo arquivo, temos vários exemplos de ActionBuilder.AndroidActivity.

Para a openSearch, que estamos usando para este elemento clicável, chamamos setMessagingActivity e transmitimos uma string extra para identificar qual foi o botão clicado.

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

Execute o bloco "mensagens", não o bloco "olá", e clique no botão de pesquisa. A MainActivity vai abrir e mostrar o texto para confirmar que o botão de pesquisa foi clicado.

O processo para adicionar ações aos outros é semelhante. As ClickableActions contêm as funções necessárias. Se precisar de uma dica, confira MessagingTileRenderer no módulo finished.

10. Parabéns

Parabéns! Você aprendeu a criar um bloco para Wear OS.

Qual é a próxima etapa?

Para mais informações, confira as Implementações de blocos dourados no GitHub (em inglês), o Guia de blocos do Wear OS e as diretrizes de design.