1. Introdução
Neste codelab, você aprenderá a criar apps adaptáveis para smartphones, tablets e dispositivos dobráveis e verá como eles melhoram a acessibilidade com o Jetpack Compose. Você também vai aprender as práticas recomendadas para usar os componentes e temas do Material 3.
Antes de começar, é importante entender o que significa adaptabilidade.
Adaptabilidade
A interface do app precisa ser responsiva para considerar diferentes tamanhos de janela, orientações e formatos. Um layout adaptável muda com base no espaço de tela disponível. Essas mudanças vão desde ajustes simples para preencher a área e escolher os respectivos estilos de navegação até mudanças completas de layout para usar mais espaço.
Para saber mais, confira Design adaptável (link em inglês).
Neste codelab, você vai aprender a usar e pensar em adaptabilidade ao usar o Jetpack Compose. Você vai criar um app chamado "Reply", que mostra como implementar a adaptabilidade para todos os tipos de tela e como a adaptabilidade e a acessibilidade funcionam juntas para dar aos usuários uma experiência ideal.
O que você vai aprender
- Como projetar seu app para todos os tamanhos de janela com o Jetpack Compose.
- Como destinar seu app a diferentes dispositivos dobráveis.
- Como usar diferentes tipos de navegação para melhor adaptabilidade e acessibilidade.
- Como usar os componentes do Material 3 para oferecer a melhor experiência em todos os tamanhos de janela.
O que é necessário
- A versão estável mais recente do Android Studio.
- Um dispositivo virtual redimensionável do Android 13.
- Conhecimento sobre Kotlin.
- Noções básicas do Compose, como a anotação
@Composable
. - Conhecimento básico dos layouts do Compose (por exemplo,
Row
eColumn
). - Conhecimento básico sobre os modificadores, como
Modifier.padding()
.
Neste codelab, você vai usar o emulador redimensionável, que permite alternar entre diferentes tipos de dispositivos e tamanhos de janela.
Se você não conhece o Compose, faça o codelab Noções básicas do Jetpack Compose antes deste.
O que você vai criar
- Um app cliente de e-mail interativo chamado Reply, que segue as práticas recomendadas para designs adaptáveis, diferentes navegações do Material Design e uso ideal do espaço na tela.
2. Começar a configuração
Para acessar o código deste codelab, clone o repositório do GitHub na linha de comando:
git clone https://github.com/android/codelab-android-compose.git cd codelab-android-compose/AdaptiveUiCodelab
Se preferir, baixe o repositório como um arquivo ZIP:
Recomendamos que você comece com o código na ramificação main e siga todas as etapas do codelab no seu ritmo.
Abrir o projeto no Android Studio
- Na janela Welcome to Android Studio, selecione
Open an Existing Project.
- Selecione a pasta
<Download Location>/AdaptiveUiCodelab
. Dica: você precisa selecionar o diretórioAdaptiveUiCodelab
que contémbuild.gradle
. - Depois que o Android Studio tiver importado o projeto, teste se você consegue executar a ramificação
main
.
Conhecer o código inicial
O código da ramificação main contém o pacote ui
. Você vai trabalhar com os seguintes arquivos nesse pacote:
MainActivity.kt
: atividade do ponto de entrada em que você inicia o app.ReplyApp.kt
: contém os elementos combináveis da interface da tela principal.ReplyHomeViewModel.kt
: fornece os dados e o estado da interface para o conteúdo do app.ReplyListContent.kt
: contém elementos combináveis para fornecer listas e telas de detalhes.
Se você executar esse app em um emulador redimensionável e testar tipos de dispositivos diferentes, como um smartphone ou tablet, a interface vai se expandir para o espaço fornecido em vez de aproveitar o espaço da tela ou fornecer ergonomia de acessibilidade.
Você fará a atualização para aproveitar o espaço na tela, aumentar a usabilidade e melhorar a experiência geral do usuário.
3. Tornar apps adaptáveis
Esta seção apresenta o que significa tornar os apps adaptáveis e quais componentes o Material 3 oferece para facilitar isso. Ele também abrange os tipos de tela e os estados para os quais você quer destinar o app, incluindo smartphones, tablets, tablets grandes e dispositivos dobráveis.
Você começará analisando as noções básicas sobre tamanhos de janela, posições de dobra e diferentes tipos de opções de navegação. Em seguida, é possível usar essas APIs no app para torná-lo mais adaptável.
Tamanhos das janelas
Existem dispositivos Android de todos os formatos e tamanhos, sejam smartphones, dobráveis, tablets ou dispositivos ChromeOS. Para oferecer suporte ao maior número possível de tamanhos de janela, a interface precisa ser responsiva e adaptável. Para ajudar você a encontrar o limite certo para mudar a interface do app, definimos valores de ponto de interrupção que ajudam a classificar dispositivos em classes de tamanho predefinidas (compactas, médias e expandidas), chamadas de classes de tamanho de janelas. Esses são um conjunto de pontos de interrupção específicos da janela de visualização que ajudam você a projetar, desenvolver e testar layouts responsivos e adaptáveis para seu app.
As categorias foram escolhidas especificamente para equilibrar a simplicidade do layout com a flexibilidade de otimizar o app para casos únicos. A classe de tamanho de janela sempre é determinada pelo espaço na tela disponível para o app, que pode não ocupar a tela física inteira para multitarefas ou outras segmentações.
A largura e a altura são classificadas separadamente. A todo momento, o app tem duas classes de tamanho de janela: uma para largura e outra para altura. A largura disponível geralmente é mais importante que a altura disponível devido à onipresença da rolagem vertical. Portanto, nesse caso, você também vai usar classes de tamanho de largura.
Estados de dobra
Os dispositivos dobráveis apresentam mais situações às quais o app pode se adaptar devido aos diversos tamanhos e à presença de articulações. As dobradiças podem ocultar parte da tela, tornando essa área inadequada para mostrar conteúdo. eles também podem estar separados, o que significa que há duas telas físicas separadas quando o dispositivo é desdobrado.
Além disso, o usuário pode olhar para a tela interna enquanto a articulação está parcialmente aberta, resultando em diferentes posições físicas com base na orientação da dobra: posição de mesa (dobra horizontal, mostrada à direita na imagem acima) e posição de livro (dobra vertical).
Leia mais sobre articulações e posições da dobra (link em inglês).
Todas essas coisas precisam ser consideradas ao implementar layouts adaptáveis compatíveis com dispositivos dobráveis.
Receber informações adaptáveis
A biblioteca adaptive
do Material3 oferece acesso conveniente a informações sobre a janela em que o app está sendo executado.
- Adicione entradas para este artefato e sua versão ao arquivo de catálogo de versões:
gradle/libs.versions.toml
[versions]
material3Adaptive = "1.0.0-beta01"
[libraries]
androidx-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3Adaptive" }
- No arquivo de build do módulo do app, adicione a nova dependência de biblioteca e execute uma sincronização com o Gradle:
app/build.gradle.kts
dependencies {
implementation(libs.androidx.material3.adaptive)
}
Agora, em qualquer escopo combinável, você pode usar currentWindowAdaptiveInfo()
para acessar um objeto WindowAdaptiveInfo
contendo informações como a classe de tamanho da janela atual e se o dispositivo está em uma posição dobrável, como a de mesa.
Você pode testar isso agora nas MainActivity
.
- No
onCreate()
dentro do blocoReplyTheme
, acesse as informações adaptáveis da janela e mostre as classes de tamanho em um elemento combinávelText
. É possível adicioná-lo após o elementoReplyApp()
:
MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ReplyTheme {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
ReplyApp(
replyHomeUIState = uiState,
onEmailClick = viewModel::setSelectedEmail
)
val adaptiveInfo = currentWindowAdaptiveInfo()
val sizeClassText =
"${adaptiveInfo.windowSizeClass.windowWidthSizeClass}\n" +
"${adaptiveInfo.windowSizeClass.windowHeightSizeClass}"
Text(
text = sizeClassText,
color = Color.Magenta,
modifier = Modifier.padding(20.dp)
)
}
}
}
A execução do app agora vai mostrar as classes de tamanho de janela impressas sobre o conteúdo do app. Fique à vontade para explorar o que mais é fornecido nas informações de adaptação da janela. Depois disso, é possível remover esse Text
, já que ele cobre o conteúdo do app e não será necessário para as próximas etapas.
4. Navegação dinâmica
Agora você vai adaptar a navegação do app à medida que o estado e o tamanho do dispositivo mudarem para facilitar o uso.
Quando os usuários seguram um smartphone, os dedos geralmente ficam na parte de baixo da tela. Quando os usuários seguram um dispositivo dobrável aberto ou um tablet, os dedos geralmente ficam perto das laterais. Seus usuários precisam conseguir navegar ou iniciar uma interação com o app sem exigir posições extremas ou mudanças na posição das mãos.
Ao projetar o app e decidir onde colocar elementos interativos da interface no layout, considere as implicações ergonômicas das diferentes regiões da tela.
- Quais áreas são confortáveis para alcançar ao segurar o dispositivo?
- Que áreas podem ser alcançadas somente esticando os dedos, o que pode ser inconveniente?
- Quais áreas são difíceis de alcançar ou estão longe de onde o usuário segura o dispositivo?
A navegação é a primeira coisa com que os usuários interagem e contém ações de alta importância relacionadas a jornadas críticas do usuário, por isso ela deve ser colocada em áreas mais fáceis de alcançar. A biblioteca adaptativa do Material Design oferece vários componentes que ajudam a implementar a navegação, dependendo da classe de tamanho da janela do dispositivo.
Navegação inferior
A navegação na parte de baixo é perfeita para tamanhos compactos, porque seguramos naturalmente o dispositivo de maneira que o polegar alcance com facilidade todos os pontos de contato da navegação na parte de baixo. Use essa opção sempre que tiver um dispositivo compacto ou um dobrável em um estado compacto dobrado.
Coluna de navegação
Para um tamanho de janela com largura média, a coluna de navegação é ideal para acessibilidade, já que o polegar cai naturalmente na lateral do dispositivo. Você também pode combinar uma coluna com uma gaveta de navegação para mostrar mais informações.
Gaveta de navegação
A gaveta de navegação oferece uma maneira fácil de mostrar informações detalhadas das guias de navegação e pode ser acessada com facilidade ao usar tablets ou dispositivos maiores. Há dois tipos de gaveta de navegação disponíveis: uma modal e uma permanente.
Gaveta de navegação modal
Você pode usar uma gaveta de navegação modal para tablets e smartphones de tamanho compacto a médio, porque ela pode ser aberta ou ocultada como uma sobreposição no conteúdo. Às vezes, isso pode ser combinado com uma coluna de navegação.
Gaveta de navegação permanente
Você pode usar uma gaveta de navegação permanente para navegação fixa em tablets grandes, Chromebooks e computadores.
Implementar a navegação dinâmica
Agora, você alternará entre diferentes tipos de navegação à medida que o estado e o tamanho do dispositivo mudarem.
No momento, o app sempre mostra uma NavigationBar
abaixo do conteúdo da tela, independente do estado do dispositivo. Em vez disso, você pode usar o componente NavigationSuiteScaffold
do Material Design para alternar automaticamente entre os diferentes componentes de navegação com base em informações como a classe de tamanho de janela atual.
- Adicione a dependência do Gradle para receber esse componente atualizando o catálogo de versões e o script de build do app. Em seguida, execute uma sincronização do Gradle:
gradle/libs.versions.toml
[versions]
material3AdaptiveNavSuite = "1.3.0-beta01"
[libraries]
androidx-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3AdaptiveNavSuite" }
app/build.gradle.kts
dependencies {
implementation(libs.androidx.material3.adaptive.navigation.suite)
}
- Encontre a função combinável
ReplyNavigationWrapper()
noReplyApp.kt
e substitua aColumn
e o conteúdo dela por umNavigationSuiteScaffold
:
ReplyApp.kt
@Composable
private fun ReplyNavigationWrapperUI(
content: @Composable () -> Unit = {}
) {
var selectedDestination: ReplyDestination by remember {
mutableStateOf(ReplyDestination.Inbox)
}
NavigationSuiteScaffold(
navigationSuiteItems = {
ReplyDestination.entries.forEach {
item(
selected = it == selectedDestination,
onClick = { /*TODO update selection*/ },
icon = {
Icon(
imageVector = it.icon,
contentDescription = stringResource(it.labelRes)
)
},
label = {
Text(text = stringResource(it.labelRes))
},
)
}
}
) {
content()
}
}
O argumento navigationSuiteItems
é um bloco que permite adicionar itens usando a função item()
, semelhante à adição de itens a uma LazyColumn
. Na lambda final, esse código chama o content()
transmitido como um argumento para ReplyNavigationWrapperUI()
.
Execute o app no emulador e tente mudar os tamanhos entre smartphone, dobrável e tablet. A barra de navegação vai mudar para uma coluna de navegação e voltar.
Em janelas muito largas, como em um tablet no modo paisagem, convém mostrar a gaveta de navegação permanente. O NavigationSuiteScaffold
não oferece suporte à exibição de uma gaveta permanente, embora ela não seja mostrada em nenhum dos valores WindowWidthSizeClass
atuais. No entanto, é possível fazer isso com uma pequena mudança.
- Adicione o código abaixo logo antes da chamada para
NavigationSuiteScaffold
:
ReplyApp.kt
@Composable
private fun ReplyNavigationWrapperUI(
content: @Composable () -> Unit = {}
) {
var selectedDestination: ReplyDestination by remember {
mutableStateOf(ReplyDestination.Inbox)
}
val windowSize = with(LocalDensity.current) {
currentWindowSize().toSize().toDpSize()
}
val layoutType = if (windowSize.width >= 1200.dp) {
NavigationSuiteType.NavigationDrawer
} else {
NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(
currentWindowAdaptiveInfo()
)
}
NavigationSuiteScaffold(
layoutType = layoutType,
...
) {
content()
}
}
Primeiro, esse código recebe o tamanho da janela e o converte em unidades de DP usando currentWindowSize()
e LocalDensity.current
. Em seguida, esse código compara a largura da janela para decidir o tipo de layout da interface de navegação. Se a largura da janela for pelo menos 1200.dp
, ela usará NavigationSuiteType.NavigationDrawer
. Caso contrário, o sistema retorna ao cálculo padrão.
Quando você executar o app novamente no emulador redimensionável e testar tipos diferentes, observe que sempre que a configuração da tela mudar ou quando você desdobrar um dispositivo dobrável, a navegação mudará para o tipo adequado para esse tamanho.
Parabéns! Você aprendeu sobre os diferentes tipos de navegação para oferecer suporte a diferentes tipos de tamanhos e estados de janela.
Na próxima seção, você vai aprender a aproveitar qualquer área de tela restante em vez de estender o mesmo item da lista de uma borda à outra.
5. Uso do espaço na tela
Não importa se você está executando o app em um tablet pequeno, um dispositivo desdobrado ou um tablet grande, a tela é esticada para preencher o espaço restante. Você pode aproveitar esse espaço na tela para mostrar mais informações. No caso deste app, por exemplo, mostrando e-mails e conversas aos usuários na mesma página.
O Material 3 define três layouts canônicos (link em inglês), cada um com configurações para classes de tamanho de janela compacta, média e expandida. O layout canônico Detalhes da lista é perfeito para esse caso de uso e está disponível no Compose como ListDetailPaneScaffold
.
- Para acessar esse componente, adicione as seguintes dependências e execute uma sincronização do Gradle:
gradle/libs.versions.toml
[libraries]
androidx-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "material3Adaptive" }
androidx-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "material3Adaptive" }
app/build.gradle.kts
dependencies {
implementation(libs.androidx.material3.adaptive.layout)
implementation(libs.androidx.material3.adaptive.navigation)
}
- Encontre a função combinável
ReplyAppContent()
emReplyApp.kt
, que atualmente mostra apenas o painel de lista chamandoReplyListPane()
. Substitua essa implementação porListDetailPaneScaffold
inserindo o código a seguir. Como essa é uma API experimental, você também vai adicionar a anotação@OptIn
na funçãoReplyAppContent()
:
ReplyApp.kt
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
replyHomeUIState: ReplyHomeUIState,
onEmailClick: (Email) -> Unit,
) {
val navigator = rememberListDetailPaneScaffoldNavigator<Long>()
ListDetailPaneScaffold(
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = {
ReplyListPane(replyHomeUIState, onEmailClick)
},
detailPane = {
ReplyDetailPane(replyHomeUIState.emails.first())
}
)
}
Esse código primeiro cria um navegador usando rememberListDetailPaneNavigator
. O Navigator fornece certo controle sobre qual painel é exibido e qual conteúdo deve ser representado nesse painel, o que será demonstrado mais tarde.
ListDetailPaneScaffold
vai mostrar dois painéis quando a classe de tamanho de largura da janela for expandida. Caso contrário, ele mostrará um painel ou o outro com base nos valores fornecidos para dois parâmetros: a diretiva scaffold e o valor. Para conseguir o comportamento padrão, esse código usa a diretiva scaffold e o valor de scaffold fornecido pelo Navigator.
Os outros parâmetros necessários são lambdas combináveis para os painéis. ReplyListPane()
e ReplyDetailPane()
(encontrados em ReplyListContent.kt
) são usados para preencher os papéis dos painéis de lista e de detalhes, respectivamente. ReplyDetailPane()
espera um argumento de e-mail. Por isso, por enquanto, este código usa o primeiro e-mail da lista de e-mails em ReplyHomeUIState
.
Execute o app e mude a visualização do emulador para dobrável ou tablet (talvez também seja necessário mudar a orientação) para conferir o layout de dois painéis. Isso já está muito melhor do que antes!
Agora, vamos abordar alguns dos comportamentos desejados dessa tela. Quando o usuário toca em um e-mail no painel de lista, ele é mostrado no painel de detalhes com todas as respostas. No momento, o app não monitora qual e-mail foi selecionado, e tocar em um item não faz nada. O melhor lugar para manter essas informações é com o restante do estado da interface em ReplyHomeUIState
.
- Abra
ReplyHomeViewModel.kt
e encontre a classe de dadosReplyHomeUIState
. Adicione uma propriedade para o e-mail selecionado, com um valor padrão denull
:
ReplyHomeViewModel.kt
data class ReplyHomeUIState(
val emails : List<Email> = emptyList(),
val selectedEmail: Email? = null,
val loading: Boolean = false,
val error: String? = null
)
- No mesmo arquivo,
ReplyHomeViewModel
tem uma funçãosetSelectedEmail()
que é chamada quando o usuário toca em um item da lista. Modifique esta função para copiar o estado da interface e registrar o e-mail selecionado:
ReplyHomeViewModel.kt
fun setSelectedEmail(email: Email) {
_uiState.update {
it.copy(selectedEmail = email)
}
}
É preciso considerar o que acontece antes de o usuário tocar em qualquer item e o e-mail selecionado for null
. O que deve ser exibido no painel de detalhes? Há várias maneiras de lidar com esse caso, como mostrar o primeiro item da lista por padrão.
- No mesmo arquivo, modifique a função
observeEmails()
. Quando a lista de e-mails for carregada, se o estado anterior da interface não tiver um e-mail selecionado, defina-o como o primeiro item:
ReplyHomeViewModel.kt
private fun observeEmails() {
viewModelScope.launch {
emailsRepository.getAllEmails()
.catch { ex ->
_uiState.value = ReplyHomeUIState(error = ex.message)
}
.collect { emails ->
val currentSelection = _uiState.value.selectedEmail
_uiState.value = ReplyHomeUIState(
emails = emails,
selectedEmail = currentSelection ?: emails.first()
)
}
}
}
- Volte para
ReplyApp.kt
e use o e-mail selecionado, se disponível, para preencher o conteúdo do painel de detalhes:
ReplyApp.kt
ListDetailPaneScaffold(
// ...
detailPane = {
if (replyHomeUIState.selectedEmail != null) {
ReplyDetailPane(replyHomeUIState.selectedEmail)
}
}
)
Execute o app novamente, mude o emulador para o tamanho do tablet. Tocar em um item da lista atualiza o conteúdo do painel de detalhes.
Isso funciona muito bem quando os dois painéis estão visíveis, mas quando a janela só tem espaço para mostrar um painel, parece que nada acontece quando você toca em um item. Tente mudar a visualização do emulador para um smartphone ou dispositivo dobrável no modo retrato e observe que apenas o painel de lista fica visível, mesmo depois de tocar em um item. Isso ocorre porque, mesmo que o e-mail selecionado seja atualizado, o ListDetailPaneScaffold
está mantendo o foco no painel de lista nessas configurações.
- Para corrigir isso, insira o seguinte código como a lambda transmitida para
ReplyListPane
:
ReplyApp.kt
ListDetailPaneScaffold(
// ...
listPane = {
ReplyListPane(
replyHomeUIState = replyHomeUIState,
onEmailClick = { email ->
onEmailClick(email)
navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
}
)
},
// ...
)
Essa lambda usa o navegador criado anteriormente para adicionar outro comportamento quando um item é clicado. Ele vai chamar o lambda original transmitido para essa função e, em seguida, chamar navigator.navigateTo()
especificando qual painel vai ser mostrado. Cada painel do scaffold tem um papel associado e, para o painel de detalhes, é ListDetailPaneScaffoldRole.Detail
. Em janelas menores, isso dará a aparência de que o app navegou para frente.
O app também precisa processar o que acontece quando o usuário pressiona o botão "Voltar" no painel de detalhes, e esse comportamento será diferente dependendo se há um ou dois painéis visíveis.
- Ofereça suporte à navegação de retorno adicionando o código a seguir.
ReplyApp.kt
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
replyHomeUIState: ReplyHomeUIState,
onEmailClick: (Email) -> Unit,
) {
val navigator = rememberListDetailPaneScaffoldNavigator<Long>()
BackHandler(navigator.canNavigateBack()) {
navigator.navigateBack()
}
ListDetailPaneScaffold(
directive = navigator.scaffoldDirective,
value = navigator.scaffoldValue,
listPane = {
AnimatedPane {
ReplyListPane(
replyHomeUIState = replyHomeUIState,
onEmailClick = { email ->
onEmailClick(email)
navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
}
)
}
},
detailPane = {
AnimatedPane {
if (replyHomeUIState.selectedEmail != null) {
ReplyDetailPane(replyHomeUIState.selectedEmail)
}
}
}
)
}
O navegador sabe o estado completo da ListDetailPaneScaffold
, se a navegação de retorno é possível e o que fazer em todos esses cenários. Esse código cria um BackHandler
que é ativado sempre que o navegador pode voltar e, dentro da lambda, chama navigateBack()
. Além disso, para tornar a transição entre painéis muito mais suave, cada painel é encapsulado em um elemento combinável AnimatedPane()
.
Execute o app novamente em um emulador redimensionável para todos os diferentes tipos de dispositivo e observe que sempre que a configuração da tela muda ou quando você desdobra um dispositivo dobrável, a navegação e o conteúdo da tela mudam dinamicamente em resposta às mudanças de estado do dispositivo. Além disso, toque nos e-mails no painel de lista e confira como o layout se comporta em diferentes telas, mostrando os dois painéis lado a lado ou criando uma animação entre eles.
Parabéns! Você tornou seu app adaptável a todos os tipos de estados e tamanhos de dispositivo. Você já pode testar esse app em dobráveis, tablets ou outros dispositivos móveis.
6. Parabéns
Parabéns! Você concluiu este codelab e aprendeu a tornar os apps adaptáveis com o Jetpack Compose.
Você aprendeu a verificar o tamanho e o estado de dobra de um dispositivo e atualizar de forma adequada a interface, a navegação e outras funções do app. Você também aprendeu como a adaptabilidade melhora a acessibilidade e a experiência do usuário.
A seguir
Confira os outros codelabs no Programa de treinamentos do Compose.
Apps de exemplo
- Os exemplos do Compose (link em inglês) são uma coleção de vários apps que incorporam as práticas recomendadas explicadas nos codelabs.