1. Introdução
Neste codelab, você vai aprender a criar apps adaptáveis para smartphones, tablets e dispositivos dobráveis e vai descobrir 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, consulte Design adaptável.
Neste codelab, você vai aprender a usar e pensar em adaptabilidade ao utilizar 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, como
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, conclua o codelab de 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ê vai atualizar para aproveitar o espaço da 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. Também aborda os tipos de tela e estados para os quais você quer destinar o app, incluindo smartphones, tablets, tablets grandes e dispositivos dobráveis.
Você vai 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, você pode usar essas APIs no seu app para que ele fique mais adaptável.
Tamanhos de janela
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 em que a interface do app precisa mudar, 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, você também vai usar classes de tamanho de largura nesse caso.
Estados de dobra
Os dispositivos dobráveis apresentam ainda mais situações para que o app possa se adaptar devido aos tamanhos variados e à presença de dobradiças. As articulações podem obscurecer parte da tela, tornando essa área inadequada para mostrar conteúdo. Elas também podem estar separadas, o que significa que há duas telas físicas separadas quando o dispositivo é aberto.
Além disso, o usuário pode estar olhando para a tela interna enquanto a dobradiça está parcialmente aberta, resultando em diferentes posturas físicas com base na orientação da dobra: postura de mesa (dobra horizontal, mostrada à direita na imagem acima) e postura de livro (dobra vertical).
Leia mais sobre articulações e posições da dobra (link em inglês).
Todas essas são coisas a serem consideradas ao implementar layouts adaptáveis que oferecem suporte a dispositivos dobráveis.
Receber informações adaptativas
A biblioteca adaptive
do Material3 oferece acesso conveniente a informações sobre a janela em que o app está sendo executado.
- Adicione entradas para esse artefato e a versão dele ao arquivo de catálogo de versões:
gradle/libs.versions.toml
[versions]
material3Adaptive = "1.0.0"
[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 realize uma sincronização do Gradle:
app/build.gradle.kts
dependencies {
implementation(libs.androidx.material3.adaptive)
}
Agora, em qualquer escopo combinável, é possível usar currentWindowAdaptiveInfo()
para receber um objeto WindowAdaptiveInfo
com informações como a classe de tamanho de janela atual e se o dispositivo está em uma posição dobrável, como a posição de mesa.
Teste isso agora em MainActivity
.
- Em
onCreate()
, dentro do blocoReplyTheme
, receba as informações de adaptação da janela e mostre as classes de tamanho em um elemento combinávelText
. É possível adicionar isso depois do 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(
WindowInsets.safeDrawing.asPaddingValues()
)
)
}
}
}
A execução do app agora vai mostrar as classes de tamanho de janela impressas sobre o conteúdo do app. Fique à vontade para conferir o que mais é fornecido nas informações de adaptação da janela. Depois, você pode remover esse Text
, já que ele abrange 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 conforme o estado e o tamanho do dispositivo mudam 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 próximos às laterais. Os usuários precisam poder navegar ou iniciar uma interação com um app sem precisar de posições extremas ou mudar a 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?
- Quais áreas só podem ser alcançadas 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 de usuários importantes. Por isso, ela precisa ser colocada nas áreas mais fáceis de acessar. 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 uma janela de tamanho médio, a coluna de navegação é ideal para acessibilidade, já que o polegar fica naturalmente ao lado 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 ficar oculta ou ser aberta como uma sobreposição no conteúdo. Às vezes, isso pode ser combinado com um trilho 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ê vai alternar entre diferentes tipos de navegação conforme o estado e o tamanho do dispositivo mudam.
No momento, o app sempre mostra um NavigationBar
abaixo do conteúdo da tela, independentemente 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 e realize uma sincronização do Gradle:
gradle/libs.versions.toml (link em inglês)
[versions]
material3AdaptiveNavSuite = "1.3.0"
[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()
emReplyApp.kt
e substitua oColumn
e o conteúdo dele 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
. Dentro da 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. Você vai notar que a barra de navegação muda para uma coluna de navegação e vice-versa.
Em janelas muito largas, como em um tablet no modo paisagem, é recomendável mostrar a gaveta de navegação permanente. NavigationSuiteScaffold
oferece suporte à exibição de uma gaveta permanente, mas não é mostrado em nenhum dos valores atuais de WindowWidthSizeClass
. 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()
}
}
Esse código primeiro extrai o tamanho da janela e o converte em unidades de DP usando currentWindowSize()
e LocalDensity.current
. Em seguida, compara a largura da janela para decidir o tipo de layout da IU de navegação. Se a largura da janela for pelo menos 1200.dp
, ela usará NavigationSuiteType.NavigationDrawer
. Caso contrário, ele volta 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 realize 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 só mostra o painel de lista chamandoReplyListPane()
. Substitua essa implementação porListDetailPaneScaffold
inserindo o seguinte código. Como essa é uma API experimental, também é necessário adicionar a anotação@OptIn
à 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 navegador oferece algum controle sobre qual painel é exibido e qual conteúdo deve ser representado nele, o que será demonstrado mais adiante.
ListDetailPaneScaffold
vai mostrar dois painéis quando a classe de tamanho de largura da janela for expandida. Caso contrário, um ou outro painel será mostrado com base nos valores fornecidos para dois parâmetros: a diretiva de estrutura e o valor da estrutura. Para conseguir o comportamento padrão, esse código usa a diretiva de esqueleto e o valor de esqueleto fornecido pelo navegador.
Os parâmetros obrigatórios restantes 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, o 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 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 rastreia 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 essa função para copiar o estado da interface e gravar o e-mail selecionado:
ReplyHomeViewModel.kt
fun setSelectedEmail(email: Email) {
_uiState.update {
it.copy(selectedEmail = email)
}
}
Algo a considerar é o que acontece antes de o usuário tocar em qualquer item e o e-mail selecionado ser null
. O que precisa ser mostrado 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 IU 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 tem espaço apenas para um painel, parece que nada acontece quando você toca em um item. Tente mudar a visualização do emulador para um smartphone ou um 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 acontece porque, embora o e-mail selecionado seja atualizado, o ListDetailPaneScaffold
mantém 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 a lambda original transmitida para essa função e também chamar navigator.navigateTo()
, especificando qual painel será mostrado. Cada painel do scaffold tem um papel associado e, para o painel de detalhes, é ListDetailPaneScaffoldRole.Detail
. Em janelas menores, isso vai dar a impressão 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. Esse comportamento será diferente dependendo se há um ou dois painéis visíveis.
- Adicione o código abaixo para oferecer suporte à navegação de retorno.
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 conhece o estado completo do ListDetailPaneScaffold
, se a navegação de volta é 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 os painéis muito mais suave, cada painel é envolvido 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. Tente também tocar em e-mails no painel de lista e conferir como o layout se comporta em diferentes telas, mostrando os dois painéis lado a lado ou animando entre eles sem problemas.
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. Além disso, você 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.