1. Introdução
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
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
- Noções básicas do Kotlin
2. Etapas da configuração
Nesta etapa, você configurará seu ambiente e fará o download de um projeto inicial.
O que é preciso
- Atualização de recursos do Android Studio Koala | 2024.1.2 Canary 1 ou mais recente
- Dispositivo ou emulador do Wear OS
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 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.
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.
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.
5. Criar um bloco de mensagens
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:
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()
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()
)
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:
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.
- Adicione um novo parâmetro,
searchButtonClickable
(do tipoModifiersBuilders.Clickable
). - 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.