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 acessar diretamente uma de 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 é necessário

  • Android Studio Dolphin (2021.3.1) ou uma versão mais recente
  • Dispositivo ou emulador do Wear OS

Caso não saiba usar o Wear OS, leia este guia rápido (link 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). Para conferir se o git está instalado, digite "git -versão" no terminal ou na linha de comando e verifique se ele é executado corretamente.

git clone https://github.com/android/codelab-wear-tiles.git
cd 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 duas funções:

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

A primeira mapeia IDs de string para um recurso de imagem. É aqui que fornecemos os recursos de imagem que vão ser usados 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 CoroutinesTileService, 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 CoroutinesTileService fornece duas funções de suspensão, que são versões 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 de bloco

É necessário registrar o serviço de bloco no manifesto para que o sistema saiba que ele existe. Depois de registrado, ele vai aparecer 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.

Em vez disso, vamos usar o Direct Surface Launch, um recurso introduzido com o Android Studio Dolphin, para criar uma nova configuração de execução a fim de iniciar o bloco diretamente no Android Studio. Selecione "Edit Configurations…" no menu suspenso no painel superior.

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 CoroutinesTileService 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.

Para qualquer trabalho potencialmente de longa duração, como chamadas de rede, é mais adequado usar algo como o WorkManager, porque as funções do serviço de bloco têm tempos limite relativamente curtos. Neste codelab, não vamos apresentar o WorkManager. Para testá-lo por conta própria, confira este codelab.

MessagingTileRenderer

MessagingTileRenderer estende a classe TileRenderer, 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 IU do bloco no Android Studio usando a TileLayoutPreview, e similares, da Horologist Tile. Isso diminui o ciclo de feedback ao desenvolver a IU, agilizando muito a iteração.

Vamos usar ferramentas do Jetpack Compose para conferir a visualização. É por isso que você vai notar a anotação @Composable na função de visualização abaixo. Saiba mais sobre as visualizações de composição. Não é necessário concluir este codelab.

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

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

@WearDevicePreview
@Composable
fun MessagingTileRendererPreview() {
    TileLayoutPreview(
        state = MessagingTileState(MessagingRepo.knownContacts),
        resourceState = emptyMap(),
        renderer = MessagingTileRenderer(LocalContext.current)
    )
}

Observe que a função de composição usa TileLayoutPreview. Não é possível visualizar layouts de blocos diretamente.

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.

Estamos transmitindo dados artificiais em MessagingTileState, e ainda não há nenhum estado de recurso para que possamos transmitir um mapa vazio.

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.tiles:tiles-material:$tilesVersion"

Dependendo da complexidade do design, pode ser útil posicionar o código do layout com o renderizador, usando funções de nível superior no mesmo arquivo para encapsular uma unidade lógica da IU.

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

@IconSizePreview
@Composable
private fun SearchButtonPreview() {
    LayoutElementPreview(
        searchLayout(
            context = LocalContext.current,
            clickable = emptyClickable
        )
    ) {
        addIdToImageMapping(
            MessagingTileRenderer.ID_IC_SEARCH,
            drawableResToImageResource(R.drawable.ic_search_24)
        )
    }
}

A LayoutElementPreview é semelhante à TileLayoutPreview, mas é usada para componentes individuais, como um botão, ícone ou rótulo. O lambda final permite especificar o mapeamento do ID de recurso para recursos de imagem. Portanto, aqui estamos mapeando ID_IC_SEARCH para o recurso de imagem de pesquisa.

No modo de edição "Split", podemos visualizar o botão de pesquisa:

Um conjunto vertical de visualizações empilhadas, o bloco na parte de cima e um botão de ícone de pesquisa abaixo.

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

Visualização do bloco com cinco botões em uma pirâmide 2 x 3. O segundo e o terceiro botões são círculos azuis, indicando imagens ausentes.

MultiButtonLayout oferece suporte para até sete botões e os mostra com o espaçamento adequado para você. Também vamos adicionar o ícone "New" ao PrimaryLayout no builder PrimaryLayout da 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()
)

prévia do bloco com cinco botões e um ícone compacto abaixo que mostra a mensagem "New"

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

8. Adicionar imagens.

Mostrar uma imagem local em um bloco é uma tarefa simples. Basta fornecer o mapeamento de um ID de string (que você usa no seu layout) para a imagem, usando a função de conveniência da Horologist Tile para carregar o drawable e o transformar em um recurso de imagem. Um exemplo está disponível na SearchButtonPreview:

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

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

Para o bloco de mensagens, também precisamos carregar imagens da rede e não apenas recursos locais. Para isso, usamos o Coil (link em inglês), um carregador de imagens do Kotlin baseado em corrotinas.

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 é totalmente síncrono, o serviço de bloco é o que 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: MutableList<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 ainda estamos transmitindo um emptyMap() ao resourceState.

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.

Atualize o MessagingTileRendererPreview() para fornecer bitmaps aos dois contatos que precisam de um:

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

@WearDevicePreview
@Composable
fun MessagingTileRendererPreview() {
    val state = MessagingTileState(MessagingRepo.knownContacts)
    val context = LocalContext.current
    TileLayoutPreview(
        state = state,
        resourceState = mapOf(
            state.contacts[1] to (context.getDrawable(R.drawable.ali) as BitmapDrawable).bitmap,
            state.contacts[2] to (context.getDrawable(R.drawable.taylor) as BitmapDrawable).bitmap,
        ),
        renderer = MessagingTileRenderer(context)
    )
}

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

visualização do bloco com cinco botões, desta vez com fotos nos dois botões que eram círculos azuis

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 CoroutinesTileService). 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. Adicione o parâmetro searchButtonClickable e transmita-o ao método searchLayout():

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 e clique no botão de pesquisa. A MainActivity vai ser aberta 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 e o Guia de blocos do Wear OS.