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 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
- 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 é 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 (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. Para verificar 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/googlecodelabs/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 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.
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 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:
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:
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()
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()
)
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:
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.