Redimensionamento de apps Android

1. Introdução

O ecossistema de dispositivos Android está sempre em evolução. Dos teclados físicos de antigamente até os modernos dispositivos dobráveis, tablets e janelas com redimensionamento livre, os apps Android nunca funcionaram em uma variedade tão diversificada de aparelhos como hoje.

Embora essa seja uma ótima notícia para os desenvolvedores, algumas otimizações de apps são necessárias para atender às expectativas de usabilidade e proporcionar uma excelente experiência aos usuários em diferentes tamanhos de tela. Em vez de focar cada novo dispositivo, uma UI responsiva/adaptável e uma arquitetura resiliente podem ajudar a deixar seu app bonito e funcional onde quer que seus usuários atuais e futuros estejam: em aparelhos de todos os tamanhos e formatos!

A chegada de ambientes Android com redimensionamento livre é uma ótima maneira de testar a capacidade de resposta/adaptação da UI e se preparar para todos os tipos de dispositivos. Este codelab vai explicar as implicações do redimensionamento, bem como a implementação de algumas práticas recomendadas para fazer com que seu app seja redimensionado de forma consistente e simples.

O que você vai criar

Você vai saber mais sobre as implicações do redimensionamento livre. Além disso, vai otimizar um app Android para demonstrar as práticas recomendadas de redimensionamento. Esse app vai:

Ter um manifesto compatível

  • Remova as restrições que impedem o app de ser redimensionado livremente

Manter o estado ao redimensionar

  • Mantenha o estado da UI durante o redimensionamento com rememberSaveable
  • Evite a duplicação desnecessária do trabalho em segundo plano para inicializar a UI

O que você vai precisar

  1. Conhecimento sobre como criar apps Android básicos
  2. Conhecimento de ViewModel e State no Compose
  3. Um dispositivo de teste que permita o redimensionamento livre de janelas, como uma das seguintes opções:

Se você encontrar algum problema (bugs no código, erros gramaticais, instruções pouco claras etc.) neste codelab, informe o problema no link Informar um erro no canto inferior esquerdo do codelab.

2. Como começar

Clone o repositório do GitHub (link em inglês).

git clone https://github.com/android/large-screen-codelabs/

… ou faça o download de um arquivo ZIP do repositório e o extraia.

Importar o projeto

  • Abra o Android Studio.
  • Selecione Import Project ou File->New->Import Project.
  • Acesse o local em que você clonou ou extraiu o projeto.
  • Abra a pasta resizing.
  • Abra o projeto na pasta start, que tem o código inicial.

Testar o app

  • Crie e execute o app.
  • Tente redimensionar o app.

O que você acha?

Dependendo do suporte à compatibilidade do dispositivo de teste, é provável que você tenha percebido que a experiência do usuário não é a ideal. O app não pode ser redimensionado e fica preso na proporção inicial de tela. O que está acontecendo?

Restrições do manifesto

Se você analisar o arquivo AndroidManifest.xml do app, existem algumas restrições que impedem o bom funcionamento em um ambiente de redimensionamento de janela livre.

AndroidManifest.xml

            android:maxAspectRatio="1.4"
            android:resizeableActivity="false"
            android:screenOrientation="portrait">

Tente remover essas três linhas problemáticas do manifesto, recrie o app e faça um novo teste no dispositivo. Você vai perceber que agora é possível fazer o redimensionamento livre. Remover restrições como essas do manifesto é uma etapa importante na otimização do app para o redimensionamento livre de janelas.

3. Mudanças de configuração ao redimensionar

Quando uma janela é redimensionada, a Configuração do app é atualizada. Essas modificações afetam o app. Por isso, entender e antecipar mudanças ajuda a proporcionar uma ótima experiência aos usuários. As alterações mais óbvias são a largura e a altura da janela do app, mas a proporção e a orientação também mudam.

Observar as mudanças de configuração

Para conferir essas mudanças em um app criado com o sistema de visualização do Android, substitua View.onConfigurationChanged. No Jetpack Compose, você tem acesso a LocalConfiguration.current, que é atualizado automaticamente sempre que View.onConfigurationChanged é chamado.

Para conferir essas mudanças de configuração em um app de exemplo, adicione um combinável ao app que exiba valores de LocalConfiguration.current. Outra opção é criar um projeto de exemplo com esse combinável. Consulte um exemplo de UI semelhante:

val configuration = LocalConfiguration.current
val isPortrait = configuration.orientation ==
    Configuration.ORIENTATION_PORTRAIT
val screenLayoutSize =
        when (configuration.screenLayout and
                Configuration.SCREENLAYOUT_SIZE_MASK) {
            SCREENLAYOUT_SIZE_SMALL -> "SCREENLAYOUT_SIZE_SMALL"
            SCREENLAYOUT_SIZE_NORMAL -> "SCREENLAYOUT_SIZE_NORMAL"
            SCREENLAYOUT_SIZE_LARGE -> "SCREENLAYOUT_SIZE_LARGE"
            SCREENLAYOUT_SIZE_XLARGE -> "SCREENLAYOUT_SIZE_XLARGE"
            else -> "undefined value"
        }
Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier.fillMaxWidth()
) {
    Text("screenWidthDp: ${configuration.screenWidthDp}")
    Text("screenHeightDp: ${configuration.screenHeightDp}")
    Text("smallestScreenWidthDp: ${configuration.smallestScreenWidthDp}")
    Text("orientation: ${if (isPortrait) "portrait" else "landscape"}")
    Text("screenLayout SIZE: $screenLayoutSize")
}

Confira uma implementação de exemplo na pasta do projeto observing-configuration-changes. Tente adicionar isso à UI do app, execute no dispositivo de teste e observe a atualização da UI conforme a configuração do app muda.

À medida que o app é redimensionado, as informações de configuração alteradas são exibidas na interface em tempo real.

Essas mudanças na configuração do app permitem simular rapidamente os extremos, desde a tela dividida em um celular pequeno até a tela cheia em um tablet ou computador. Isso não apenas é uma boa maneira de testar o layout do app em diferentes telas, mas também mostra como ele lida com mudanças rápidas na configuração.

4. Registrar eventos do ciclo de vida da atividade

Outra implicação do redimensionamento livre de janela para o app são as várias mudanças no ciclo de vida de Activity. Para conferir essas alterações em tempo real, adicione um observador de ciclo de vida ao método onCreate e registre cada novo evento de ciclo de vida pela substituição de onStateChanged.

lifecycle.addObserver(object : LifecycleEventObserver {
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        Log.d("resizing-codelab-lifecycle", "$event was called")
    }
})

Com esse registro implementado, execute o app de novo no dispositivo de teste e analise o Logcat enquanto minimiza e traz o app de volta para o primeiro plano.

O app é pausado quando minimizado, e retomado quando colocado mais uma vez em primeiro plano. Isso tem implicações para o app que vão ser explicadas na próxima seção deste codelab, focada na continuidade.

Logcat mostra métodos do ciclo de vida da atividade invocados durante o redimensionamento

Agora confira no Logcat quais callbacks do ciclo de vida da atividade são chamados quando você redimensiona o app do menor tamanho possível para o maior.

Dependendo do dispositivo de teste, ocorrem comportamentos diferentes, mas você provavelmente notou que a atividade é destruída e recriada quando o tamanho da janela do app muda muito, mas não quando muda pouco. Isso ocorre porque, na API 24+, apenas grandes mudanças de tamanho resultam na recriação de Activity.

Você viu algumas das mudanças de configuração comuns que pode esperar em um ambiente de redimensionamento livre de janela, mas há outras de que precisa estar ciente. Por exemplo, se você tiver um monitor externo conectado ao dispositivo de teste, Activity vai ser destruída e recriada para processar as mudanças de configuração, como densidade da exibição.

Para abstrair parte da complexidade associada às mudanças de configuração, use APIs de nível superior, como WindowSizeClass, para implementar a UI adaptável. Consulte também Compatibilidade com diferentes tamanhos de tela.

5. Continuidade: manter o estado interno dos combináveis ao redimensionar

Na seção anterior, você conferiu algumas das mudanças de configuração que podem ocorrer no app em um ambiente de redimensionamento de janela livre. Nesta seção, o estado da UI do app vai permanecer o mesmo ao longo dessas mudanças.

Comece com a expansão da função combinável NavigationDrawerHeader (encontrada em ReplyHomeScreen.kt) para mostrar o endereço de e-mail quando clicada.

@Composable
private fun NavigationDrawerHeader(
    modifier: Modifier = Modifier
) {
    var showDetails by remember { mutableStateOf(false) }
    Column(
        modifier = modifier.clickable {
                showDetails = !showDetails
            }
    ) {


        Row(
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            ReplyLogo(
                modifier = Modifier
                    .size(dimensionResource(R.dimen.reply_logo_size))
            )
            ReplyProfileImage(
                drawableResource = LocalAccountsDataProvider
                    .userAccount.avatar,
                description = stringResource(id = R.string.profile),
                modifier = Modifier
                    .size(dimensionResource(R.dimen.profile_image_size))
            )
        }
        AnimatedVisibility (showDetails) {
            Text(
                text = stringResource(id = LocalAccountsDataProvider
                        .userAccount.email),
                style = MaterialTheme.typography.labelMedium,
                modifier = Modifier
                    .padding(
                        start = dimensionResource(
                            R.dimen.drawer_padding_header),
                        end = dimensionResource(
                            R.dimen.drawer_padding_header),
                        bottom = dimensionResource(
                            R.dimen.drawer_padding_header)
                ),


            )
        }
    }
}

Depois de adicionar o cabeçalho expansível ao app:

  1. execute o app no dispositivo de teste;
  2. toque no cabeçalho para expandir;
  3. tente redimensionar a janela.

O cabeçalho perde o estado quando é muito redimensionado.

Um toque no cabeçalho na gaveta de navegação faz o app expandir, mas ele é recolhido depois que o app é redimensionado

A UI perde o estado porque remember ajuda a manter o estado entre recomposições, mas não entre recriações de atividades ou processos. Uma solução comum é usar a elevação de estado, que move o estado para o autor da chamada ao combinável, tornando-o assim sem estado, o que evita esse problema por completo. Dito isso, ainda é possível usar remember em casos específicos para manter o estado de elementos da interface dentro das funções combináveis.

Para resolver esses problemas, substitua remember por rememberSaveable. Isso funciona porque rememberSaveable salva e restaura o valor lembrado em savedInstanceState. Mude remember para rememberSaveable, execute o app no dispositivo de teste e tente redimensionar o app de novo. Você vai perceber que o estado do cabeçalho expansível é preservado durante todo o redimensionamento, como pretendido.

6. Evitar a duplicação desnecessária do trabalho em segundo plano

Você já sabe como usar rememberSaveable para preservar o estado interno da UI dos combináveis por mudanças de configuração, que podem acontecer com frequência devido ao redimensionamento livre de janelas. No entanto, em geral, o app precisa elevar o estado e a lógica da UI para fora dos combináveis. Mover a propriedade do estado para um ViewModel é uma das melhores maneiras de preservar o estado durante o redimensionamento. Ao elevar o estado para ViewModel, talvez você encontre problemas com trabalhos em segundo plano de longa execução, como acesso intenso ao sistema de arquivos ou chamadas de rede necessárias para inicializar a tela.

Para conferir um exemplo dos tipos de problemas que podem ocorrer, adicione uma log statement ao método initializeUIState em ReplyViewModel.

fun initializeUIState() {
    Log.d("resizing-codelab", "initializeUIState() called in the viewmodel")
    val mailboxes: Map<MailboxType, List<Email>> =
        LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
    _uiState.value =
        ReplyUiState(
            mailboxes = mailboxes,
            currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
                ?: LocalEmailsDataProvider.defaultEmail
        )
}

Agora execute o app no dispositivo de teste e tente redimensionar a janela várias vezes.

No Logcat, o app mostra que o método de inicialização foi realizado diversas vezes. Isso pode ser um problema no caso das tarefas que você só quer fazer uma vez para inicializar a UI. As chamadas de rede adicionais, a E/S de arquivo ou outros trabalhos podem prejudicar o desempenho do dispositivo e causar outros problemas não intencionais.

Para evitar trabalho desnecessário em segundo plano, remova a chamada para initializeUIState() do método onCreate() da atividade. Em vez disso, inicialize os dados no método init de ViewModel. Assim você garante que o método de inicialização seja executado apenas uma vez, quando ReplyViewModel é instanciado pela primeira vez:

init {
    initializeUIState()
}

Execute o app de novo e você vai perceber que a tarefa de simulação de inicialização desnecessária é realizada apenas uma vez, não importa quantas vezes você redimensione a janela. Isso ocorre porque os ViewModels persistem além do ciclo de vida de Activity. Ao executar o código de inicialização apenas uma vez na criação de ViewModel, você o separa de todas as recriações de Activity e evita trabalho desnecessário. Se essa fosse realmente uma chamada cara ao servidor ou uma operação intensa de E/S de arquivo para inicializar a UI, você economizaria muitos recursos e melhoraria a experiência do usuário.

7. PARABÉNS!

Você conseguiu! Bom trabalho! Você implementou algumas práticas recomendadas para permitir que os apps Android sejam redimensionados corretamente no Chrome OS e em outros ambientes multitelas ou com várias janelas.

Exemplo de código-fonte

Clone o repositório do GitHub

git clone https://github.com/android/large-screen-codelabs/

… ou faça o download de um arquivo ZIP do repositório e o extraia.