DEV Community

Cover image for Criando aplicativos para Android Wear OS com Jetpack Compose
Tiago Danin
Tiago Danin

Posted on • Updated on

Criando aplicativos para Android Wear OS com Jetpack Compose

Jetpack Compose no Wear OS

A Google recentemente anunciou na Google I/O o beta do Jetpack Compose para o Wear OS, a versão do Android para Smartwatch. Um dos principais foco do Compose é a criação de interface de maneira rápida usando sintaxe declarativa, semelhante ao que há no iOS com SwiftUI, hoje demostrarei a criação de um simples aplicativo em poucos minutos utilizado o que há de mais moderno para desenvolvimento de aplicativos para Android.

O Aplicativo

O aplicativo que vamos desenvolver consiste em controlar a quantidade de copos de água você tomou ao logo do dia, para isso vamos precisar da versão mais recente do Android Studio, no meu caso estou utilizado o Android Studio Electric Eel (2022.1.1) Canary 5

Iniciando um novo projeto

Com Android Studio aberto vamos em criar um novo projeto
Image description

Na lista de template vai ter um chamando Wear OS >> Empty Compose Activity, selecionaremos e vamos continuar.

Image description

O próximo passo é configurar o nosso app com nome, id, localização do projeto, e versão mínima do Android Wear OS. Para esse exemplo utilizaremos a API Level 30, conforme na imagem acima.

Image description

Vai ser gerado as seguintes arquivos e pastas, a customização no Compose não acontecer via os arquivos de resources (RES) em formato XML, em vez disso para diminuir a curva de aprendizagem boa parte do app é feito usado Kotlin, como a personalização do app que agora fica localizado em /theme, por enquanto vamos apenas trocar as cores.

package com.tiagodanin.waterwearos.presentation.theme

import ...

val Blue700 = Color(0xFF1976d2)
val Blue900 = Color(0xFF0d47a1)
val DeepPurple200 = Color(0xFFb39ddb)
val DeepPurple400 = Color(0xFF512da8)

internal val wearColorPalette: Colors = Colors(
    primary = Blue700,
    primaryVariant = Blue900,
    secondary = DeepPurple200,
    secondaryVariant = DeepPurple400,
    error = Color.Red,
    onPrimary = Color.Black,
    onSecondary = Color.Black,
    onError = Color.Black
)
Enter fullscreen mode Exit fullscreen mode

Para isso basta informar na referência da variável primary, por exemplo, a nova cor que deseja. Nesse caso vou usar um Azul pois remete a água.

OBS: A função Color é a representação da cor em ARGB, exemplo vermelho (FFFF0000), é representado como Color(0xFFFF0000).

Criando a Tela

Image description

A tela principal já é criada por padrão ele fica localizando no MainActivity.kt, se rodamos o app no emulador (ou dispositivo físico) vamos ter a seguinte tela:

Image description

O primeiro passo agora é criarmos a tela principal do nosso jeito, mas antes de adicionarmos elementos relacionados ao nosso aplicativo, precisamos mostra elemento comuns do sistema, nos smartphones temos a StatusBar (onde mostrar a hora) e NavigationBar, no Wear OS também temos algo semelhante que pode mostrar horas, indicador de página entre outros aspectos, ele é conhecido como Scaffold:

Image description

@OptIn(ExperimentalWearMaterialApi::class)
@Composable
fun WearApp() {
    WaterWearOSTheme {
        Scaffold(
            modifier = Modifier
                .fillMaxSize()
                .background(MaterialTheme.colors.background),
            timeText = {
                TimeText()
            },
        ) {}
    }
}
Enter fullscreen mode Exit fullscreen mode

Nosso Layout principal é montado da seguinte maneira:

  • WaterWearOSTheme { UI Compose } é o nosso utilitário para aplicar o tema em todos os elementos de UI que tem dentro dele
  • Scaffold(Props) { UI Compose} criar uma estrutura base de layout
    • modifier: Modifier Modificadores de layout e elementos comuns
    • timeText = { UI Compose } informar qual relógio será mostrado no topo
  • TimeText() É layout padrão do relógio que fica no topo

Uma das vantagens do Jetpack Compose é a fácil customização de elementos já existentes, uma das maneiras mais simples de fazer isso é usando os modificadores (Modifier) que estão disponíveis na maioria dos componentes, como no caso do Scaffold, onde foi definido que ele ocuparia o máximo da tela (.fillMaxSize()) e teria um fundo na cor preta (.background(MaterialTheme.colors.background)). O Resultado que vamos obter é:

Image description

Podemos prosseguir agora o app, vamos criar uma nova função de Compose para uma barra de progresso de quantidade de água tomada no dia, mas antes é preciso importar ela dentro do corpo do Scaffold.

@OptIn(ExperimentalWearMaterialApi::class)
@Composable
fun WearApp() {
    WaterWearOSTheme {
        Scaffold(
            modifier = Modifier
                .fillMaxSize()
                .background(MaterialTheme.colors.background),
            timeText = {
                TimeText()
            },
        ) {
            ProgressIndicatorWater() // Corpo
        }
    }
}

@Composable
fun ProgressIndicatorWater() {} // Nova UI Compose
Enter fullscreen mode Exit fullscreen mode

Como já é de esperado há uma maneira de fazer a barra de progresso usando os componentes padrão do Compose, assim como no Jetpack Compose para smartphone, o nome dela é [CircularProgressIndicator](https://developer.android.com/reference/kotlin/androidx/wear/compose/material/package-summary#CircularProgressIndicator(kotlin.Float,androidx.compose.ui.Modifier,kotlin.Float,kotlin.Float,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,androidx.compose.ui.unit.Dp), e o que diferencia no Wear são poucas coisas, uma delas e a disposição dos elementos sobre a tela.

Vamos olhar uma parte da assinatura desse componente que estamos usando, no momento estamos da seguinte maneira o codigo

@Composable
fun ProgressIndicatorWater() {
    CircularProgressIndicator(
        startAngle = 360f,
        endAngle = 0f,
        progress = 0.5f,
        strokeWidth = 5.dp,
        modifier = Modifier
            .fillMaxSize()
            .padding(all = 10.dp)
    )
}
Enter fullscreen mode Exit fullscreen mode

Image description

O que precisamos para criar a nossa barra é saber onde posicionar, para isso precisamos entender algumas das propriedades da nossa tela.

  • startAngle Ângulo de começo da barra
  • endAngle Ângulo de fim da barra
  • progress O progresso total, onde 1.0f é 100% e 0.0f é 0%

A disposição dos ângulos é dada da seguinte maneira:
Image description

Com base nisso podemos finalizar a barra antes do texto da hora, configurando da seguinte maneira startAngle = 295f, endAngle = 245f:
Image description

Estamos perto do final, agora precisamos adicionar informações (litros tomados) e ação (beber água) para cada copo de água, para isso vamos fazer uma nova função de UI Compose e usar um elemento de layout flexível para alinhamento:

Image description

Os três principais elementos são:

  • Box: elementos dentro do outros / Sobrepostos
  • Column: Um em baixo do outro (Coluna)
  • Row: Um do lado do outro (Linha)

Queremos a seguinte estrutura no ProgressIndicatorWater
Image description

Para isso podemos usar o Box.

@Composable
fun ProgressIndicatorWater() {
    // Box para centralizar
    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        CircularProgressIndicator(
            startAngle = 295f,
            endAngle = 245f,
            progress = 0.5f,
            strokeWidth = 5.dp,
            modifier = Modifier
                .fillMaxSize()
                .padding(all = 10.dp)
        )
        InfoWater() // Importação do novo Compose
    }
}

@Composable
fun InfoWater() {}
Enter fullscreen mode Exit fullscreen mode

Já dentro do InfoWater precisamos de algo como Column
Image description

@Composable
fun InfoWater() {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

E por fim adicionaremos os dois elementos de composição que planejavamos, nada diferente do que já é no Android Mobile com Jetpack Compose. O Botão e o Texto.

@Composable
fun InfoWater() {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        // Nosso texto
        Text(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 30.dp),
            textAlign = TextAlign.Center,
            color = MaterialTheme.colors.primary,
            text = "Você já bebeu 1 litro de água hoje" // Texto
        )

        // Nosso botão
        Button(
            modifier = Modifier.padding(top = 5.dp),
            onClick = {
                      // Ação do click
            },
        ) {
            Icon(
                painter = painterResource(id = R.drawable.cup_water),
                contentDescription = "Cup Water",
                modifier = Modifier
                    .size(ButtonDefaults.DefaultButtonSize)
                    .wrapContentSize(align = Alignment.Center),
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

O ícone em questão que importei não é padrão do kit de desenvolvimento, você pode baixa ele por fora, eu utilizo o Material Design Icons Community.

Depois de feito o donwload do SVG dele, no menu de opções da pasta res, selecione:
Image description

Image description

Lembrando de que para importar você pode usar o mesmo nome, exemplo R.drawable.[nome].

Lógica

É por fim a parte lógica, criaremos um estado para quantidade de litros:
private val count: MutableState<Float> = mutableStateOf(0f)

e vamos modificar a nossa barra de progresso para levar em conta que 3 litros é o ideal.

@Composable
fun ProgressIndicatorWater() {
    val recomedByDay = 3.0f // 3 Litros
    // Progresso = Todal do dia / Recomendação
    val progressOfDay: Float = count.value / recomedByDay

    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        CircularProgressIndicator(
            startAngle = 295f,
            endAngle = 245f,
            progress = progressOfDay, // Valor do progresso
            strokeWidth = 5.dp,
            modifier = Modifier
                .fillMaxSize()
                .padding(all = 10.dp)
        )
        InfoWater()
    }
}
Enter fullscreen mode Exit fullscreen mode

O mesmo com nosso texto e botão, vamos mostrar a quantidade e no evento do clique do botão considera 500ml água ingerida.

@Composable
fun InfoWater() {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 30.dp),
            textAlign = TextAlign.Center,
            color = MaterialTheme.colors.primary,
            text = "Você já bebeu ${count.value} litro de água hoje" // Resultado
        )
        Button(
            modifier = Modifier.padding(top = 5.dp),
            onClick = { count.value += 0.5f }, // Ação
        ) {
            Icon(
                painter = painterResource(id = R.drawable.cup_water),
                contentDescription = "airplane",
                modifier = Modifier
                    .size(ButtonDefaults.DefaultButtonSize)
                    .wrapContentSize(align = Alignment.Center),
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Se abrimos o app novamente vamos o seguinte resultado
Image description


Image description

No desenvolvimento desse app foram usados alguns dos componentes disponíveis do Jetpack Compose para Wear OS, no link abaixo você pode conferir diversos outros UI Compose:

developer.android.com/training/wearables/compose


Você pode seguir as implementações adicionado novas funcionalidade como persitencia de dados😉

Link do repositório: Github.com/TiagoDanin/WearOS-Count-Water-App

Oldest comments (2)

Collapse
 
rohlacanna profile image
Rômulo Silva

Ótimo artigo, parabéns Tiago! 👏

Collapse
 
ewertonbello profile image
Ewerton Belo

Muito bom artigo!