Tornar apps adaptáveis e acessíveis com o Jetpack Compose

1. Introdução

Neste codelab, você aprenderá a criar apps adaptáveis para smartphones, tablets e dispositivos dobráveis e a manter a acessibilidade central com o Jetpack Compose. Você também vai aprender práticas recomendadas para usar componentes e temas do Material 3.

Antes de nos aprofundarmos, é importante entender o que queremos dizer com adaptabilidade e acessibilidade.

Adaptabilidade

A IU do app precisa ser responsiva para considerar diferentes tamanhos de tela, orientações e formatos. Um layout adaptável muda com base no espaço de tela disponível. Essas mudanças incluem desde simples ajustes de layout para preencher o espaço, escolher os respectivos estilos de navegação até a mudança completa de layouts para usar mais espaço.

Acessibilidade

Os apps Android precisam ser utilizáveis por todos, incluindo pessoas com necessidades de acessibilidade. Os apps precisam se ajustar a diferentes cenários para oferecer a melhor experiência do usuário, com contrastes de cores, acessibilidade e muito mais.

Neste codelab, você verá como usar e pensar em adaptabilidade e acessibilidade ao usar o Jetpack Compose. Você criará um aplicativo chamado REPLY que mostra como implementar adaptabilidade para todos os tipos de telas. Você verá como a adaptabilidade e a acessibilidade funcionam juntas para oferecer aos usuários uma experiência ideal.

O que você aprenderá

  • Como projetar seu app para segmentar todos os tamanhos de tela com o Jetpack Compose.
  • Como segmentar seu app para diferentes dispositivos dobráveis.
  • Como usar diferentes tipos de navegação para melhorar a acessibilidade e a acessibilidade.
  • Como criar esquemas de cores e temas dinâmicos do Material 3 para oferecer uma experiência de acessibilidade ideal.
  • Como usar os componentes do Material Design 3 para oferecer a melhor experiência para todos os tamanhos de tela.

Pré-requisitos

  • Android Studio Bumblebee
  • Conhecimento de Kotlin.
  • Noções básicas do Compose, como a anotação @Composable.
  • Ter noções básicas sobre os layouts do Compose (por exemplo, Row e Column).
  • Ter noções básicas sobre os modificadores (por exemplo, Modifier.padding.

Se você não estiver familiarizado com o Compose, faça o codelab de conceitos básicos do Jetpack Compose antes de concluir este.

O que você criará

  • Um app cliente interativo de e-mail de resposta usando as práticas recomendadas para o Material 3, temas dinâmicos e designs adaptáveis.

Demonstração de suporte para vários dispositivos que você terá neste codelab

2. Começar a configuração

Para fazer o download do app de exemplo, você pode:

ou clone o repositório do GitHub da linha de comando com este comando:

git clone https://github.com/googlecodelabs/android-compose-codelabs.git
cd android-compose-codelabs/ReplyAdaptabilityCodelab

Você pode executar qualquer um dos módulos no Android Studio a qualquer momento alterando a configuração de execução na barra de ferramentas.

b059413b0cf9113a.png

Abrir o projeto no Android Studio

  1. Na janela "Welcome to Android Studio", selecione c01826594f360d94.png Open an Existing Project.
  2. Selecione a pasta [Download Location]/ReplyAdaptabilityCodelab. Selecione o diretório ReplyAdaptabilityCodlab que contém build.gradle.
  3. Depois que o Android Studio importar o projeto, teste se você pode executar os módulos start e finished.

Explore o código inicial

O código inicial contém quatro pacotes:

  • MainActivity: atividade do ponto de entrada em que você inicia seu ReplyApp. Você fará alterações neste arquivo.
  • ui: contém temas, componentes e o ReplyApp em que a IU do Compose é iniciada. Você vai fazer mudanças neste pacote.
  • util: contém um código auxiliar para o projeto. Não é preciso editar este pacote.

Este codelab se concentra nos arquivos do pacote reply. No módulo start, há vários arquivos que você precisa conhecer.

Arquivos que serão editados ui pacote

  • MainActivity.kt: atividade do Android que será o ponto de partida em que iniciaremos nosso ReplyApp e transmitiremos as informações necessárias, como o estado da dobra, o tamanho e as informações de layout.
  • ReplyApp.kt: a estrutura principal da IU do app está no arquivo ReplyApp.kt, com que você vai trabalhar.
  • ReplyAppContent.kt: a implementação do Compose do conteúdo do app e os detalhes da lista são exibidos aqui.

Vamos nos concentrar primeiro no MainActivity.kt. Para o módulo start, o código já deve estar funcionando na sua atividade.

MainActicity.kt (link em alemão)

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

   setContent {
       ReplyTheme {
           val uiState = viewModel.uiState.collectAsState().value
           ReplyApp(uiState)
       }
   }
}

Se você executar esse app em qualquer tamanho de dispositivo, a mesma tela será esticada para preencher a área máxima sem mudanças nos elementos da IU. Configuração inicial do ReplyApp sem alterações.

Vamos tentar aprimorá-lo para aproveitar o espaço na tela e melhorar a experiência do usuário, mantendo a acessibilidade no centro.

3. Tornar os apps adaptáveis

Esta seção traz as características de apps adaptáveis e os componentes do Material 3 que facilitam esse processo para nós.

Também falaremos sobre os tipos de telas e estados que você segmentará, incluindo smartphones, tablets, tablets grandes e dispositivos dobráveis.

Como gerenciar tamanhos de janela

Antes de chegar ao app Reply, vamos ver os tipos de tamanhos e dispositivos existentes no mercado para os usuários usarem nossos apps.

Temos smartphones de 4 a 7 polegadas. Temos também tablets, que vão de tablets menores a tablets quase do mesmo tamanho.

Primeiro, vamos dividir esses tamanhos diferentes em três categorias com base em WIndowSizeClass. 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 da janela é sempre determinada pelo espaço de tela disponível para o app, que pode não ser toda a tela física para realizar várias tarefas ou outras segmentações.

Distribuição do tamanho do dispositivo de acordo com a WindowSizeClass

WindowStateUtils**.kt**

enum class WindowSize { COMPACT, MEDIUM, EXPANDED }

fun getWindowSizeClass(windowDpSize: DpSize): WindowSize = when {
   windowDpSize.width < 0.dp -> throw IllegalArgumentException("Dp value cannot be negative")
   windowDpSize.width < 600.dp -> WindowSize.COMPACT
   windowDpSize.width < 840.dp -> WindowSize.MEDIUM
   else -> WindowSize.EXPANDED
}

WindowStateUtils.kt fornece rememberWindowSizeClass(),, o que nos ajuda a criar um estado lembrado do Compose para que sempre que haja mudanças de configuração no tamanho, nossa árvore de IU seja renderizada novamente com base no novo tamanho.

WindowStateUtils.kt.

fun Activity.rememberWindowSizeClass(): WindowSize {
   // Get the size (in pixels) of the window
   val windowSize = rememberWindowSize()

   // Convert the window size to [Dp]
   val windowDpSize = with(LocalDensity.current) {
       windowSize.toDpSize()
   }

   // Calculate the window size class
   return getWindowSizeClass(windowDpSize)
}

Para começar a oferecer compatibilidade com tamanhos adaptáveis, basta adicionar rememberWindowSizeClass() ao início da IU do Compose e transmiti-la ao ReplyApp. Agora você pode fazer mudanças no MainActivity.kt para que ele fique assim.

MainActivity.kt

setContent {
   ReplyTheme(dynamicColor = false, darkTheme = false) {
       val windowSize = rememberWindowSizeClass()
       ReplyApp(windowSize, uiState)
   }
}

Com essas mudanças, você vê que o ReplyApp tem informações sobre o tamanho da janela mais recente para usar corretamente o espaço.

4. Processar estados da dobra

Também é importante que o app responda a mudanças de estado da dobra, não apenas ao tamanho da tela. Pode haver muitos estados de dobra, mas comece com ela para segmentar alguns casos. Eles já estão definidos na classe util.

WindowStateUtils.kt.

/**
* Information about the posture of the device
*/
sealed interface DevicePosture {
   object NormalPosture : DevicePosture

   data class TableTopPosture(
       val hingePosition: Rect
   ) : DevicePosture

   data class BookPosture(
       val hingePosition: Rect
   ) : DevicePosture
}

Você quer garantir que a IU reaja quando você mudar de uma posição dobrada para uma posição desdobrada. Também é necessário considerar o BookPosture e o TableTopPosture com a posição da articulação, já que você não quer renderizar o texto ou outras informações úteis na articulação.

Vamos conferir a postura da dobra no nosso ciclo de vida da atividade. Adicione este código ao método onCreate() da atividade antes de chamar setContent().

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

    /* Flow of [DevicePosture] that emits every time there is a change in the windowLayoutInfo
    */
   val devicePostureFlow =  WindowInfoTracker.getOrCreate(this).windowLayoutInfo(this)
       .flowWithLifecycle(this.lifecycle)
       .map { layoutInfo ->
           val foldingFeature =
               layoutInfo.displayFeatures.filterIsInstance<FoldingFeature>().firstOrNull()
           when {
               isTableTopPosture(foldingFeature) ->
                   DevicePosture.TableTopPosture(foldingFeature.bounds)
               isBookPosture(foldingFeature) ->
                   DevicePosture.BookPosture(foldingFeature.bounds)
               isSeparating(foldingFeature) ->
                   DevicePosture.Separating(foldingFeature.bounds, foldingFeature.orientation)
               else -> DevicePosture.NormalPosture
           }
       }
       .stateIn(
           scope = lifecycleScope,
           started = SharingStarted.Eagerly,
           initialValue = DevicePosture.NormalPosture
       )

Agora você pode apenas observar o fluxo de posição do dispositivo como um estado Compose, o que ajuda nossa IU a reagir às mudanças de estado da dobra. Adicione essas mudanças a setContent().

MainActivity.kt

setContent {
   ReplyTheme(dynamicColor = false, darkTheme = false) {
       val devicePosture = devicePostureFlow.collectAsState().value
       ReplyApp(windowSize, devicePosture, uiState)
   }
}

A IU do Compose agora está pronta para reagir às mudanças de tamanho e estado do dispositivo. Você pode continuar aqui para projetar a IU para diferentes estados. Sempre que há mudanças de estado da dobra, queremos que a IU reaja assim.

Adaptação da IU dobrável

5. Navegação dinâmica

Na última seção, você fez a IU reagir a mudanças de tamanho, configuração e estado da dobra. Agora você precisa entender como adaptar a interação do usuário a diferentes dispositivos quando eles passam por estados diferentes.

Vamos começar com a navegação, que é a primeira coisa com que os usuários vão interagir. Os usuários têm diferentes tipos de dispositivo. Vamos ver alguns componentes de navegação do Material.kube.

Navegação inferior

A navegação inferior é perfeita para tamanhos compactos, já que seguramos naturalmente o dispositivo em que nosso polegar pode alcançar facilmente todos os pontos de contato de navegação inferior. Use-a sempre que tiver um dispositivo compacto ou dobrável em um estado compacto dobrado.

Para dispositivos de tamanho médio ou a maioria dos smartphones na orientação paisagem, a coluna de navegação é ótima para facilitar a navegação e a acessibilidade porque o polegar fica naturalmente no canto superior esquerdo do dispositivo. Você também pode usar a gaveta de navegação e o painel de navegação para mostrar mais informações.

A gaveta de navegação oferece uma maneira fácil de ver informações detalhadas sobre guias de navegação e pode ser acessada com facilidade quando você usa tablets ou dispositivos maiores. Você pode usar a gaveta de navegação junto com a coluna de navegação, Navegação inferior e usar uma gaveta de navegação permanente para navegação fixa em dispositivos muito amplos.

Agora, vamos alternar entre diferentes tipos de navegação à medida que o estado e o tamanho do dispositivo mudam, mantendo a interação e a acessibilidade do usuário no centro.

Vamos adicionar a navegação dinâmica ao aplicativo. Abra o ReplyApp.kt e adicione-o ao elemento que pode ser composto de ReplyApp,

ReplyApp.kt.

/**
* This will help us select type of navigation depending on window size and
* fold state of the device.
*/
val navigationType: ReplyNavigationType

when (windowSize) {
   WindowSize.COMPACT -> {
       navigationType = ReplyNavigationType.BOTTOM_NAVIGATION
   }
   WindowSize.MEDIUM -> {
       navigationType = ReplyNavigationType.NAVIGATION_RAIL
   }
   WindowSize.EXPANDED -> {
       navigationType = if (foldingDevicePosture is DevicePosture.BookPosture) {
           ReplyNavigationType.NAVIGATION_RAIL
       } else {
           ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER
       }
   }
}

Como a gaveta de navegação funciona como a IU do contêiner para ReplyAppContent,, você precisa incluí-la em uma gaveta de navegação permanente ou modal dependendo do navigationType. ,

ReplyApp.kt.

if (navigationType == ReplyNavigationType.PERMANENT_NAVIGATION_DRAWER) {
   PermanentNavigationDrawer(drawerContent = {                    NavigationDrawerContent(selectedDestination) }) {
       ReplyAppContent(navigationType, contentType, replyHomeUIState)
   }
} else {
   ModalNavigationDrawer(
       drawerContent = {
           NavigationDrawerContent(
               selectedDestination,
               onDrawerClicked = {
                   scope.launch {
                       drawerState.close()
                   }
               }
           )
       },
       drawerState = drawerState
   ) {
       ReplyAppContent(navigationType, contentType, replyHomeUIState,
           onDrawerClicked = {
               scope.launch {
                   drawerState.open()
               }
           }
       )
   }
}

Agora você tem um NavigationType dinâmico que pode ser usado para mudar a navegação sempre que houver mudanças de configuração. Vamos adicionar navigationType a ReplyAppContent() para tornar a navegação dinâmica,

ReplyApp.kt.

@Composable
fun ReplyAppContent(
   navigationType: ReplyNavigationType,
   contentType: ReplyContentType,
   replyHomeUIState: ReplyHomeUIState,
   onDrawerClicked: () -> Unit = {}
) {
   Row(modifier = Modifier.fillMaxSize()) {
       AnimatedVisibility(visible = navigationType == ReplyNavigationType.NAVIGATION_RAIL) {
           ReplyNavigationRail(
               onDrawerClicked = onDrawerClicked
           )
       }
       Column(modifier = Modifier
           .fillMaxSize()
           .background(MaterialTheme.colorScheme.inverseOnSurface)
       ) {
           // Reply List content

           AnimatedVisibility(visible = navigationType == ReplyNavigationType.BOTTOM_NAVIGATION) {
               ReplyBottomNavigationBar()
           }
       }
   }
}

Execute o app novamente para testar a navegação dinâmica

Ao executar o app novamente, você verá que sempre que a configuração da tela mudar ou se você desdobrar um dispositivo dobrável, a navegação mudará para o tipo apropriado para esse tamanho.

Exibindo mudanças de adaptabilidade para diferentes tamanhos de dispositivos.

Parabéns! Você aprendeu sobre diferentes tipos de navegação para oferecer compatibilidade com diferentes tipos de tamanhos e estados de tela.

Na próxima seção, você verá como aproveitar qualquer área restante da tela em vez de estender o mesmo item da lista de ponta a ponta.

6. Uso de espaço da tela

Você pode ver no app que a tela está esticada para preencher o espaço restante, seja um tablet pequeno, um dispositivo desdobrado ou um tablet grande. Você quer aproveitar esse espaço da tela para mostrar mais informações aos usuários.

Assim como no navigationType, você criará uma contentType que nos ajudará a decidir entre exibir somente um conteúdo de lista ou mostrar a lista e os detalhes em mudanças no estado da tela

ReplyApp.kt.

val contentType: ReplyContentType
when (windowSize) {
   WindowSize.COMPACT -> {
       contentType = ReplyContentType.LIST_ONLY
   }
   WindowSize.MEDIUM -> {
       contentType = if (foldingDevicePosture != DevicePosture.NormalPosture) {
           ReplyContentType.LIST_AND_DETAIL
       } else {
           ReplyContentType.LIST_ONLY
       }
   }
   WindowSize.EXPANDED -> {
       contentType = ReplyContentType.LIST_AND_DETAIL
   }
}

Agora você pode transmitir esse tipo de conteúdo para ReplyAppContent, e, sempre que houver mudanças de configuração, ele se adaptará ao layout correto. Também é possível considerar a posição da dobra e a posição da articulação para decidir o posicionamento da lista e o layout dos detalhes para evitar conteúdo na posição da articulação.

ReplyApp.kt.

@Composable
fun ReplyAppContent(
   navigationType: ReplyNavigationType,
   contentType: ReplyContentType,
   replyHomeUIState: ReplyHomeUIState,
   onDrawerClicked: () -> Unit = {}
) {
   Row(modifier = Modifier.fillMaxSize()) {
       Column(modifier = Modifier
           .fillMaxSize()
           .background(MaterialTheme.colorScheme.inverseOnSurface)
       ) {
           if (contentType == ReplyContentType.LIST_AND_DETAIL) {
               ReplyListAndDetailContent(
                   replyHomeUIState = replyHomeUIState,
                   modifier = Modifier.weight(1f),
               )
           } else {
               ReplyListOnlyContent(replyHomeUIState = replyHomeUIState, modifier = Modifier.weight(1f))
           }
       }
   }
}

Visualização final do ReplyApp depois de adicionar todas as alterações

Executar o app novamente para testar o app totalmente adaptável

Execute o app novamente e observe que, sempre que a configuração da tela mudar, ou se desdobrar um dispositivo dobrável, o conteúdo da navegação e da tela mudará dinamicamente em resposta às mudanças de estado do dispositivo. O Jetpack Compose facilita muito a criação desses tipos de mudanças em um padrão declarativo.

Parabéns! Você tornou seu app adaptável para todos os tipos de estados e tamanhos de dispositivo. Teste o app em dispositivos dobráveis, tablets ou outros dispositivos móveis.

Nas próximas seções, você verá como essas mudanças na capacidade de adaptação nos ajudam a estabelecer a estrutura de acessibilidade.

7. Melhorar a acessibilidade

Acessibilidade

A acessibilidade é a capacidade de navegar ou usar um dispositivo sem precisar de posições extremas ou mudar de posição para iniciar qualquer interação com um app.

Na seção "Navegação dinâmica" do app "Responder", você adicionou vários modos de navegação para serem usados de acordo com o estado da tela. Componentes do Material Design, como a barra de navegação inferior, a coluna de navegação e a gaveta de navegação, facilitam o acesso à navegação com base em como seguramos dispositivos de diferentes formatos.

Demonstração de acessibilidade mostrando a coluna de navegação e a gaveta de navegação para diferentes tamanhos de tablet.

Também adicionamos um formato de lista e detalhes que permite aos usuários alternar facilmente entre as conversas e rolar por elas em dispositivos grandes com as mãos esquerda e direita sem mudar de posição.

Contraste da cor

O app Reply é compatível com temas dinâmicos no Android 12 e versões mais recentes, em que o esquema de cores é gerado pela seleção de plano de fundo e outras configurações de personalização. Os produtos que usam cores dinâmicas atendem aos requisitos de acessibilidade porque as combinações algorítmicas que um usuário final pode experimentar foram projetadas para atender a esses padrões.

Para saber mais, consulte Cores dinâmicas.

Esquema de cores do Material 3 para os modos claro e escuro.

Para esse app, também estamos usando um esquema de cores do Material 3 que foi projetado para atender aos padrões de acessibilidade de contraste de cores. O sistema de paletas de tons é fundamental para tornar qualquer esquema de cores acessível por padrão.

Demonstração de contraste de cores com temas do Material 3.

Combinar cores com base em tonalidade, em vez de valor hexadecimal ou matiz, é um dos sistemas-chave que tornam qualquer saída de cor acessível. Você pode criar seu esquema de cores completo do Material 3 escolhendo o conjunto certo de cores primárias, secundárias e terciárias e usando o builder de tema do Material 3 para criar um esquema de cores do Material 3 para variações claras e escuras. A variação gerada já está de acordo com os padrões de acessibilidade de contraste de cores.

No Android 11 e versões anteriores, em que os temas dinâmicos não estão disponíveis, usamos um esquema de cores fixo do Material 3 gerado com o builder de tema do Material.

Você pode testar novos temas de cores usando o Criador de tema do Material Design (link em inglês).

É possível colocar a cor gerada diretamente no arquivo ui/theme/Color.kt para vê-la em ação.

8. Parabéns

Parabéns! Você concluiu este codelab e aprendeu a projetar apps adaptáveis e acessíveis usando o Jetpack Compose.

Você aprendeu a verificar o tamanho e dobramento de um dispositivo e atualizar a IU, a navegação e outras funções do app. Você também aprendeu a aproveitar o esquema de cores e a tipografia do Material 3 para melhorar a experiência do usuário e a acessibilidade.

A seguir

Confira os outros codelabs no módulo do Compose.

App de exemplo

  • Os exemplos de aplicativos são uma coleção de muitos aplicativos que incorporam as práticas recomendadas explicadas em codelabs.

Documentos de referência