Detentores de estado e estado da IU

Mantenha tudo organizado com as coleções Salve e categorize o conteúdo com base nas suas preferências.

O Guia da camada de IU aborda o fluxo de dados unidirecional (UDF, na sigla em inglês) como uma forma de produzir e gerenciar o estado da IU para a camada da IU.

Os dados fluem unidirecionalmente da camada de dados para a IU.
Figura 1: fluxo de dados unidirecional.

Ele também destaca os benefícios de delegar o gerenciamento do UDF a uma classe especial, conhecida como um detentor de estado. É possível implementar um detentor de estado usando um ViewModel ou uma classe simples. Este documento analisa melhor os detentores de estado e o papel que desempenham na camada da IU.

Ao final do documento, você vai compreender como gerenciar o estado do aplicativo na camada da IU. Esse é o pipeline de produção do estado da IU. Você vai aprender sobre as informações abaixo:

  • Os tipos de estado da IU que existem na camada da IU.
  • Os tipos de lógica que operam nesses estados da interface do usuário na camada da IU.
  • Como escolher a implementação adequada de um detentor de estado, como um ViewModel ou uma classe simples.

Elementos do pipeline de produção do estado da IU

O estado da IU e a lógica de produção dele definem a camada da IU.

Estado da IU

O estado da IU é a propriedade que descreve a IU. Há dois tipos de estados da IU:

  • O estado da IU da tela é o que você precisa mostrar na tela. Por exemplo, uma classe NewsUiState pode conter as matérias de notícias e outras informações necessárias para renderizar a IU. Esse estado geralmente está conectado a outras camadas da hierarquia, já que ele contém dados do app.
  • O estado do elemento da IU se refere a propriedades intrínsecas aos elementos da IU que influenciam a renderização. Um elemento da IU pode ser mostrado ou ocultado e pode ter um determinado tipo, tamanho ou cor da fonte. No Android, a visualização gerencia esse estado por conta própria, já que ela tem um estado inerente e expõe métodos de modificação ou consulta. Um exemplo disso são os métodos get e set da classe TextView para o texto. No Jetpack Compose, o estado é externo ao elemento de composição, e você pode até elevar o estado para fora da proximidade do elemento e o enviar até a função de composição que fez a chamada ou um detentor de estado. Um exemplo disso é o ScaffoldState do elemento de composição Scaffold.

Lógica

O estado da IU não é uma propriedade estática, já que os dados do app e os eventos do usuário fazem mudanças nele com o tempo. A lógica determina as especificidades da mudança, incluindo quais partes do estado da IU mudaram, por quais motivos a alteração ocorreu e quando ela precisa mudar.

A lógica produz o estado da IU
Figura 2: a lógica como a produtora do estado da IU

A lógica pode ser de negócios ou de IU:

  • A lógica de negócios é a implementação de requisitos de produtos para dados do app. Por exemplo, adicionar uma matéria aos favoritos em um app de notícias quando o usuário toca no botão. Essa lógica para salvar um favorito em um arquivo ou banco de dados geralmente é colocada nas camadas de domínio ou de dados. O detentor do estado geralmente delega essa lógica a essas camadas chamando os métodos que elas expõem.
  • A lógica da IU está relacionada a como mostrar o estado da IU na tela. Por exemplo, acessar a dica correta da barra de pesquisa quando o usuário seleciona uma categoria, rolar para um determinado item em uma lista ou a lógica de navegação para uma tela específica quando o usuário clica em um botão.

Ciclo de vida do Android e os tipos de estado e lógica da IU

A camada da IU tem duas partes: uma dependente e a outra independente do ciclo de vida da IU. Essa separação determina as fontes de dados disponíveis para cada parte e exige diferentes tipos de estado e lógica da IU.

  • Independente do ciclo de vida da IU: essa parte da camada da IU lida com as camadas de produção de dados do app (camadas de dados ou de domínio) e é definida pela lógica de negócios. O ciclo de vida, as mudanças de configuração e a recriação da Activity na IU podem afetar a atividade do pipeline de produção de estado da IU, mas não afetam a validade dos dados produzidos.
  • Dependente do ciclo de vida da IU: essa parte da camada da interface do usuário lida com a lógica da IU e é diretamente influenciada por mudanças de ciclo de vida ou configuração. Essas mudanças afetam diretamente a validade das fontes de dados lidas na IU, e, como resultado, o estado só pode mudar quando o ciclo de vida está ativo. Exemplos incluem permissões de execução e de recebimento de recursos dependentes de configuração, como strings localizadas.

Confira um resumo na tabela abaixo:

Independente do ciclo de vida da IU Dependente do ciclo de vida da IU
Lógica de negócios Lógica da IU
Estado da IU na tela

O pipeline de produção do estado da IU

O pipeline de produção do estado da IU se refere às etapas realizadas para produzir o estado da IU. Essas são as etapas da aplicação dos tipos de lógica definidos anteriormente e são completamente dependentes das necessidades da IU. Algumas IUs podem se beneficiar das partes independentes e dependentes do ciclo de vida da IU do pipeline ao mesmo tempo, de apenas uma delas ou de nenhuma.

Ou seja, estas permutações do pipeline da camada da IU são válidas:

  • Estado da IU produzido e gerenciado pela própria IU. Por exemplo, um contador básico simples e reutilizável:

    @Composable
    fun Counter() {
        // The UI state is managed by the UI itself
        var count by remember { mutableStateOf(0) }
        Row {
            Button(onClick = { ++count }) {
                Text(text = "Increment")
            }
            Button(onClick = { --count }) {
                Text(text = "Decrement")
            }
        }
    }
    
  • Lógica da IU → IU. Por exemplo, mostrar ou ocultar um botão que permite que o usuário volte para o topo de uma lista.

    @Composable
    fun ContactsList(contacts: List<Contact>) {
        val listState = rememberLazyListState()
        val isAtTopOfList by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex < 3
            }
        }
    
        // Create the LazyColumn with the lazyListState
        ...
    
        // Show or hide the button (UI logic) based on the list scroll position
        AnimatedVisibility(visible = !isAtTopOfList) {
            ScrollToTopButton()
        }
    }
    
  • Lógica de negócios → IU. Um elemento da IU mostrando a foto do usuário atual na tela.

    @Composable
    fun UserProfileScreen(viewModel: UserProfileViewModel = hiltViewModel()) {
        // Read screen UI state from the business logic state holder
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
        // Call on the UserAvatar Composable to display the photo
        UserAvatar(picture = uiState.profilePicture)
    }
    
  • Lógica de negócios → lógica da IU → IU. Um elemento da IU que rola para mostrar as informações corretas na tela de um determinado estado da IU.

    @Composable
    fun ContactsList(viewModel: ContactsViewModel = hiltViewModel()) {
        // Read screen UI state from the business logic state holder
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
        val contacts = uiState.contacts
        val deepLinkedContact = uiState.deepLinkedContact
    
        val listState = rememberLazyListState()
    
        // Create the LazyColumn with the lazyListState
        ...
    
        // Perform UI logic that depends on information from business logic
        if (deepLinkedContact != null && contacts.isNotEmpty()) {
            LaunchedEffect(listState, deepLinkedContact, contacts) {
                val deepLinkedContactIndex = contacts.indexOf(deepLinkedContact)
                if (deepLinkedContactIndex >= 0) {
                  // Scroll to deep linked item
                  listState.animateScrollToItem(deepLinkedContactIndex)
                }
            }
        }
    }
    

Nos casos em que os dois tipos de lógica são aplicados ao pipeline de produção do estado da IU, a lógica de negócios precisa ser sempre aplicada antes da lógica da IU. Tentar aplicar na ordem inversa implica que a lógica de negócios depende da lógica da IU. As próximas seções abordam por que isso é um problema em uma análise detalhada de diferentes tipos de lógica e dos detentores de estado delas.

Fluxos de dados da camada de produção de dados para a IU
Figura 3: aplicação da lógica na camada da IU.

Detentores de estado e as responsabilidades deles

A responsabilidade de um detentor de estado é armazenar o estado para que ele possa ser lido pelo app. Nos casos em que a lógica é necessária, ela atua como intermediária e fornece acesso às fontes de dados que hospedam a lógica exigida. Dessa forma, o detentor do estado delega a lógica à fonte de dados adequada.

Isso gera os benefícios abaixo:

  • IUs simples: a IU se vincula apenas ao estado dela.
  • Facilidade de manutenção: a lógica definida no detentor de estado pode ser iterada sem mudar a IU em si.
  • Facilidade de testagem: a IU e a lógica de produção do estado dela podem ser testadas de forma independente.
  • Legibilidade: pessoas que lerem o código podem notar claramente as diferenças entre o código de apresentação da IU e o código de produção do estado.

Independente do tamanho ou escopo, cada elemento da IU tem uma relação individual com o detentor de estado correspondente. Além disso, um detentor de estado precisa aceitar e processar qualquer ação do usuário que possa resultar em uma mudança de estado da IU e produzir a mudança de estado subsequente.

Tipos de detentores de estado

De forma semelhante aos tipos de estado e lógica, há dois tipos de detentores de estado definidos pela relação com o ciclo de vida da IU:

  • O detentor do estado da lógica de negócios.
  • O detentor de estado da lógica da IU.

As seções abaixo detalham os tipos de detentores de estado, começando com o detentor da lógica de negócios.

A lógica de negócios e o detentor de estado dela

Os detentores de estado da lógica de negócios processam eventos do usuário e transformam os dados das camadas de dados ou de domínios no estado da IU da tela. Para fornecer uma experiência do usuário ideal ao considerar as mudanças de configuração do ciclo de vida e do app Android, os detentores de estado que usam a lógica de negócios precisam ter estas propriedades:

Propriedade Detalhes
Produz o estado da IU Os detentores de estado da lógica de negócios são responsáveis por produzir o estado da IU para as próprias IUs. Geralmente, esse estado da IU é o resultado do processamento de eventos do usuário e da leitura de dados do domínio e das camadas de dados.
Mantido após a recriação da atividade Os detentores de estado da lógica de negócios mantêm os estados e pipelines de processamento após todas as recriações de Activity, ajudando a fornecer uma experiência do usuário perfeita. Nos casos em que o detentor de estado não pode ser retido e recriado (geralmente após o encerramento do processo), ele precisa poder recriar facilmente o último estado para garantir uma experiência do usuário consistente.
Possui um estado de longa duração Muitas vezes, os detentores de estado da lógica de negócios são usados para gerenciar o estado dos destinos de navegação. Como resultado, eles costumam preservar o estado em todas as mudanças de navegação até serem removidos do gráfico.
É exclusivo da IU e não pode ser reutilizado Os detentores de estado da lógica de negócios geralmente produzem o estado de uma determinada função do app, por exemplo, um TaskEditViewModel ou TaskListViewModel. Portanto, eles são aplicáveis apenas à função correspondente. O mesmo detentor de estado pode oferecer suporte a essas funções do app em diferentes formatos. Por exemplo, as versões do app para dispositivos móveis, TVs e tablets podem reutilizar o mesmo detentor de estado da lógica de negócios.

Por exemplo, considere o destino de navegação do autor no app Now in Android (link em inglês):

O app Now in Android demonstra como um destino de navegação que representa uma função importante do app precisa ter
o próprio detentor de estado da lógica de negócios exclusivo.
Figura 4: o app Now in Android.

Ao atuar como detentor de estado da lógica de negócios, o AuthorViewModel (link em inglês) produz o estado da IU neste caso:

@HiltViewModel
class AuthorViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val authorsRepository: AuthorsRepository,
    newsRepository: NewsRepository
) : ViewModel() {

    val uiState: StateFlow<AuthorScreenUiState> = …

    // Business logic
    fun followAuthor(followed: Boolean) {
      …
    }
}

AuthorViewModel tem os atributos descritos anteriormente:

Propriedade Detalhes
Produz AuthorScreenUiState O AuthorViewModel lê dados do AuthorsRepository e do NewsRepository e os usa para produzir o AuthorScreenUiState. Ele também aplica uma lógica de negócios quando o usuário quer seguir ou deixar de seguir um Author delegando ao AuthorsRepository.
Tem acesso à camada de dados Uma instância do AuthorsRepository e NewsRepository é transmitida a ele no construtor, permitindo que ele implemente a lógica de negócios de seguir um Author.
Sobrevive à recriação da Activity Por ser implementado com um ViewModel, ele vai ser mantido após a recriação rápida da Activity. No caso de encerramento do processo, o objeto SavedStateHandle pode ser lido para fornecer a quantidade mínima de informações necessárias para restaurar o estado da IU da camada de dados.
Possui um estado de longa duração O ViewModel tem o escopo definido para o gráfico de navegação. Portanto, a menos que o destino do autor seja removido do gráfico, o estado da IU no uiState StateFlow permanece na memória. O uso do StateFlow também adiciona o benefício de fazer a aplicação da lógica de negócios que produz o estado lento, já que o estado só é produzido quando há um coletor do estado da IU.
É exclusivo da IU O AuthorViewModel só é aplicável ao destino de navegação do autor e não pode ser reutilizado em outro lugar. Se alguma lógica de negócios for reutilizada em destinos de navegação, ela vai precisar ser encapsulada em um componente com escopo da camada de dados ou de domínio.

O ViewModel como um detentor de estado da lógica de negócios

Os benefícios dos ViewModels no desenvolvimento Android os tornam adequados para fornecer acesso à lógica de negócios e preparar os dados do aplicativo para serem apresentados na tela. Esses benefícios incluem:

  • Operações acionadas por ViewModels sobrevivem a mudanças de configuração.
  • Integração com o Navigation:
    • A navegação armazena em cache os ViewModels enquanto a tela está na backstack. Isso é importante para disponibilizar os dados já carregados instantaneamente ao retornar ao destino. É mais difícil fazer isso com um detentor de estado que segue o ciclo de vida da tela de composição.
    • O ViewModel também é apagado quando o destino é retirado da backstack, garantindo que o estado seja limpo automaticamente. Isso é diferente da detecção do descarte de elementos de composição, que pode ocorrer por vários motivos, como a abertura de uma nova tela devido a uma mudança de configuração, entre outros.
  • Integração com outras bibliotecas do Jetpack, como a Hilt.

O detentor de estado da lógica da IU

A lógica da IU é uma lógica que opera nos dados fornecidos pela própria IU. Ela pode ficar no estado de elementos da IU ou em fontes de dados da IU, como a API de permissões ou Resources. Os detentores de estado que utilizam a lógica da IU normalmente têm as propriedades abaixo:

  • Produz o estado da IU e gerencia o estado dos elementos da IU.
  • Não sobrevive à recriação da Activity: os detentores de estado hospedados na lógica da IU geralmente dependem de fontes de dados da própria IU, e uma tentativa de manter essas informações entre as mudanças de configuração geralmente causa vazamento de memória. Se os detentores de estado precisarem de dados para serem mantidos após as mudanças de configuração, eles vão precisar delegar a outro componente mais adequado para sobreviverem à recriação da Activity. No Jetpack Compose, por exemplo, os estados de elementos de composição da IU criados com funções remembered geralmente delegam para rememberSaveable a fim de preservar o estado em uma recriação da Activity. Exemplos dessas funções incluem rememberScaffoldState() e rememberLazyListState().
  • Tem referências a fontes de dados com escopo na IU: as fontes de dados, como APIs e recursos do ciclo de vida, podem ser referenciadas e lidas com segurança, já que o detentor de estado da lógica da IU tem o mesmo ciclo de vida que ela.
  • É reutilizável em várias IUs: instâncias diferentes do mesmo detentor de estado da lógica da IU podem ser reutilizadas em diferentes partes do app. Por exemplo, um detentor de estado para gerenciar eventos de entrada do usuário em um grupo de ícones pode ser usado para ícones de filtro em uma página de pesquisa e também no campo "para" de destinatários de um e-mail.

O detentor de estado da lógica da IU normalmente é implementado com uma classe simples. Isso ocorre porque a própria IU é responsável pela criação do detentor de estado da lógica da IU, e ele tem o mesmo ciclo de vida que ela. No Jetpack Compose, por exemplo, o detentor de estado faz parte da composição e segue o ciclo de vida dela.

Você pode conferir esse comportamento no app de exemplo Now in Android (link em inglês):

O app Now in Android usa um detentor de estado de classe simples para gerenciar a lógica da IU
Figura 5: o app de exemplo Now in Android

O app de exemplo Now in Android mostra uma barra de apps na parte de baixo ou uma coluna de navegação, dependendo do tamanho da tela do dispositivo. Telas menores usam a barra de apps na parte de baixo e telas maiores usam a coluna de navegação.

Como a lógica para decidir o elemento da IU de navegação adequado usado na função de composição NiaApp não depende da lógica de negócios, ela pode ser gerenciada por um detentor de estado de classe simples conhecido como NiaAppState (link em inglês):

@Stable
class NiaAppState(
    val navController: NavHostController,
    val windowSizeClass: WindowSizeClass
) {

    // UI logic
    val shouldShowBottomBar: Boolean
        get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||
            windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact

    // UI logic
    val shouldShowNavRail: Boolean
        get() = !shouldShowBottomBar

   // UI State
    val currentDestination: NavDestination?
        @Composable get() = navController
            .currentBackStackEntryAsState().value?.destination

    // UI logic
    fun navigate(destination: NiaNavigationDestination, route: String? = null) { /* ... */ }

     /* ... */
}

No exemplo acima, estes detalhes sobre NiaAppState são importantes:

  • Não sobrevive à recriação da Activity: o NiaAppState é remembered (lembrado) na composição porque a criação dele é feita com uma função de composição rememberNiaAppState (link em inglês) de acordo com as convenções de nomenclatura do Compose. Depois que a Activity é recriada, a instância anterior é perdida, e uma nova instância é criada com todas as dependências transmitidas, de maneira adequada à nova configuração da Activity recriada. Essas dependências podem ser novas ou restauradas da configuração anterior. Por exemplo, rememberNavController() é usado no construtor NiaAppState e delega para rememberSaveable com o objetivo de preservar o estado em toda a recriação da Activity.
  • Tem referências a fontes de dados com escopo na IU: as referências ao navigationController, Resources e outros tipos de escopo de ciclo de vida parecidos podem ser armazenadas com segurança no NiaAppState porque elas compartilham o mesmo escopo de ciclo de vida.

Escolher entre um ViewModel e uma classe simples para um detentor de estado

Nas seções acima, escolher entre um ViewModel e um detentor de estado de classe simples se resume à lógica aplicada ao estado da IU e às fontes de dados em que a lógica opera.

Em resumo, o diagrama abaixo mostra a posição dos detentores de estado no pipeline de produção do estado da IU:

Fluxos de dados da camada de produção de dados para a camada da IU
Figura 6: detentores de estado no pipeline de produção do estado da IU.

Por fim, o estado da IU precisa ser colocado e produzido usando os detentores de estado mais próximos de onde ele é consumido. De forma menos formal, o estado precisa ser mantido o mais baixo possível na hierarquia, e precisa sempre ter um proprietário. Caso você precise de acesso à lógica de negócios e queira que o estado da IU sobreviva enquanto a tela puder ser acessada, mesmo após a recriação da Activity, um ViewModel é uma ótima opção para implementar um detentor de estado da lógica de negócios. Para a lógica da IU e estados de curta duração, uma classe simples com um ciclo de vida dependente apenas da IU provavelmente vai ser suficiente.