Ciclos de vida de estado no Compose

No Jetpack Compose, as funções combináveis geralmente mantêm o estado usando a função remember. Os valores lembrados podem ser reutilizados em recombinações, conforme explicado em Estado e Jetpack Compose.

Embora remember sirva como uma ferramenta para manter valores em recomposições, o estado geralmente precisa durar mais do que o ciclo de vida de uma composição. Esta página explica a diferença entre as APIs remember, retain, rememberSaveable e rememberSerializable, quando escolher cada uma delas e quais são as práticas recomendadas para gerenciar valores lembrados e mantidos no Compose.

Escolher o ciclo de vida correto

No Compose, há várias funções que podem ser usadas para manter o estado em composições e muito mais: remember, retain, rememberSaveable e rememberSerializable. Essas funções diferem no ciclo de vida e na semântica, e cada uma é adequada para armazenar tipos específicos de estado. As diferenças estão descritas na tabela a seguir:

remember

retain

rememberSaveable, rememberSerializable

Os valores sobrevivem às recomposições?

Os valores sobrevivem às recriações de atividades?

A mesma instância (===) sempre será retornada.

Um objeto equivalente (==) será retornado, possivelmente uma cópia desserializada.

Os valores sobrevivem ao encerramento do processo?

Tipos de dados com suporte

Tudo

Não pode fazer referência a objetos que seriam vazados se a atividade fosse destruída.

Precisa ser serializável
(com um Saver personalizado ou com kotlinx.serialization)

Casos de uso

  • Objetos com escopo na composição
  • Objetos de configuração para elementos combináveis
  • Estado que pode ser recriado sem perder a fidelidade da interface
  • Caches
  • Objetos de longa duração ou "gerenciadores"
  • Entrada do usuário
  • Estado que não pode ser recriado pelo app, incluindo entrada de campo de texto, estado de rolagem, alternâncias etc.

remember

remember é a maneira mais comum de armazenar estado no Compose. Quando remember é chamado pela primeira vez, o cálculo especificado é executado e lembrado, ou seja, armazenado pelo Compose para reutilização futura pelo elemento combinável. Quando um elemento combinável é recomposto, ele executa o código novamente, mas todas as chamadas para remember retornam os valores da composição anterior em vez de executar o cálculo novamente.

Cada instância de uma função combinável tem seu próprio conjunto de valores lembrados, o que chamamos de memoização posicional. Quando os valores memorizados são armazenados em cache para uso em várias recomposições, eles são vinculados à posição na hierarquia de composição. Se um elemento combinável for usado em locais diferentes, cada instância na hierarquia de composição terá seu próprio conjunto de valores lembrados.

Quando um valor lembrado não é mais usado, ele é esquecido e o registro dele é descartado. Os valores lembrados são esquecidos quando são removidos da hierarquia de composição (inclusive quando um valor é removido e adicionado novamente para ir para um local diferente sem o uso do elemento combinável key ou MovableContent) ou chamados com parâmetros key diferentes.

Das opções disponíveis, remember tem o menor ciclo de vida e esquece os valores mais cedo do que as quatro funções de memoização descritas nesta página. Isso o torna mais adequado para:

  • Criar objetos de estado interno, como posição de rolagem ou estado de animação
  • Evitar a recriação de objetos caros em cada recomposição

No entanto, evite:

  • Armazenar qualquer entrada do usuário com remember, porque os objetos lembrados são esquecidos em mudanças de configuração de atividades e encerramento de processos iniciado pelo sistema.

rememberSaveable e rememberSerializable

rememberSaveable e rememberSerializable são criados com base em remember. Elas têm o maior ciclo de vida das funções de memoização discutidas neste guia. Além de fazer a memoização posicional de objetos em recomposições, ele também pode salvar valores para que possam ser restaurados em recriações de atividades, incluindo mudanças de configuração e encerramento do processo (quando o sistema encerra o processo do app enquanto ele está em segundo plano, geralmente para liberar memória para apps em primeiro plano ou se o usuário revogar as permissões do app enquanto ele está em execução).

rememberSerializable funciona da mesma forma que rememberSaveable, mas oferece suporte automático à persistência de tipos complexos serializáveis com a biblioteca kotlinx.serialization. Escolha rememberSerializable se o tipo for (ou puder ser) marcado com @Serializable e rememberSaveable em todos os outros casos.

Isso torna rememberSaveable e rememberSerializable candidatos perfeitos para armazenar o estado associado à entrada do usuário, incluindo entrada de campo de texto, posição de rolagem, estados de alternância etc. Salve esse estado para garantir que o usuário nunca perca o lugar. Em geral, use rememberSaveable ou rememberSerializable para armazenar em cache qualquer estado que seu app não consiga recuperar de outra fonte de dados persistente, como um banco de dados.

Observe que rememberSaveable e rememberSerializable salvam os valores memorizados serializando-os em um Bundle. Isso tem duas consequências:

  • Os valores que você memoriza precisam ser representados por um ou mais dos seguintes tipos de dados: primitivos (incluindo Int, Long, Float, Double), String ou matrizes de qualquer um desses tipos.
  • Quando um valor salvo é restaurado, ele é uma nova instância igual a (==), mas não a mesma referência (===) que a composição usou antes.

Para armazenar tipos de dados mais complicados sem usar kotlinx.serialization, é possível implementar um Saver personalizado para serializar e desserializar seu objeto em tipos de dados compatíveis. O Compose entende tipos de dados comuns, como State, List, Map, Set etc., e os converte automaticamente em tipos compatíveis. Confira abaixo um exemplo de Saver para uma classe Size. Ele é implementado agrupando todas as propriedades de Size em uma lista usando listSaver.

data class Size(val x: Int, val y: Int) {
    object Saver : androidx.compose.runtime.saveable.Saver<Size, Any> by listSaver(
        save = { listOf(it.x, it.y) },
        restore = { Size(it[0], it[1]) }
    )
}

@Composable
fun rememberSize(x: Int, y: Int) {
    rememberSaveable(x, y, saver = Size.Saver) {
        Size(x, y)
    }
}

retain

A API retain existe entre remember e rememberSaveable/rememberSerializable em termos de tempo de memorização dos valores. Ele tem um nome diferente porque os valores retidos também têm um ciclo de vida diferente dos equivalentes lembrados.

Quando um valor é mantido, ele é memoizado posicionalmente e salvo em uma estrutura de dados secundária com um ciclo de vida separado, vinculado ao ciclo de vida do app. Um valor retido consegue sobreviver a mudanças de configuração sem ser serializado, mas não consegue sobreviver ao encerramento do processo. Se um valor não for usado depois que a hierarquia de composição for recriada, o valor retido será desativado (que é o equivalente de retain a ser esquecido).

Em troca desse ciclo de vida mais curto que rememberSaveable, o retain consegue persistir valores que não podem ser serializados, como expressões lambda, fluxos e objetos grandes, como bitmaps. Por exemplo, é possível usar retain para gerenciar um player de mídia (como o ExoPlayer) e evitar interrupções na reprodução de mídia durante uma mudança de configuração.

@Composable
fun MediaPlayer() {
    // Use the application context to avoid a memory leak
    val applicationContext = LocalContext.current.applicationContext
    val exoPlayer = retain { ExoPlayer.Builder(applicationContext).apply { /* ... */ }.build() }
    // ...
}

retain x ViewModel

Basicamente, retain e ViewModel oferecem funcionalidades semelhantes na capacidade mais usada de manter instâncias de objetos em mudanças de configuração. A escolha de retain ou ViewModel depende do tipo de valor que você está persistindo, de como ele deve ser escopo e se você precisa de funcionalidades adicionais.

Os ViewModels são objetos que geralmente encapsulam a comunicação entre as camadas de dados e de interface do seu app. Eles permitem mover a lógica para fora das funções combináveis, o que melhora a capacidade de teste. Os ViewModels são gerenciados como singletons em um ViewModelStore e têm um ciclo de vida diferente dos valores retidos. Embora um ViewModel permaneça ativo até que o ViewModelStore seja destruído, os valores retidos são desativados quando o conteúdo é removido permanentemente da composição. Por exemplo, em uma mudança de configuração, isso significa que um valor retido é desativado se a hierarquia da interface for recriada e o valor retido não tiver sido consumido após a recriação da composição.

O ViewModel também inclui integrações prontas para uso para injeção de dependência com Dagger e Hilt, integração com SavedState e suporte integrado a corrotinas para iniciar tarefas em segundo plano. Isso faz com que ViewModel seja um lugar ideal para iniciar tarefas em segundo plano e solicitações de rede, interagir com outras fontes de dados no seu projeto e, opcionalmente, capturar e manter o estado da interface essencial que deve ser retido em todas as mudanças de configuração no ViewModel e sobreviver à morte do processo.

retain é mais adequado para objetos que têm escopo em instâncias combináveis específicas e não exigem reutilização ou compartilhamento entre elementos combináveis irmãos. Enquanto ViewModel é um bom lugar para armazenar o estado da interface e realizar tarefas em segundo plano, retain é um bom candidato para armazenar objetos para encanamento da interface, como caches, rastreamento de impressões e análises, dependências de AndroidViews e outros objetos que interagem com o SO Android ou gerenciam bibliotecas de terceiros, como processadores de pagamento ou publicidade.

Para usuários avançados que criam padrões de arquitetura de apps personalizados fora das recomendações de arquitetura de apps Android modernos, o retain também pode ser usado para criar uma API interna semelhante ao ViewModel. Embora o suporte para corrotinas e estado salvo não seja oferecido de imediato, o retain pode servir como bloco de construção para o ciclo de vida de ViewModel semelhantes com esses recursos criados por cima. As especificidades de como projetar um componente desse tipo estão fora do escopo deste guia.

retain

ViewModel

Escopo

Não há valores compartilhados. Cada valor é mantido e associado a um ponto específico na hierarquia de composição. Manter o mesmo tipo em um local diferente sempre age em uma nova instância.

ViewModels são singletons em um ViewModelStore

Destruição

Ao sair permanentemente da hierarquia de composição

Quando o ViewModelStore é limpo ou destruído

Funcionalidade adicional

Pode receber callbacks quando o objeto está ou não na hierarquia de composição

O coroutineScope integrado, com suporte para SavedStateHandle, pode ser injetado usando o Hilt.

Propriedade de

RetainedValuesStore

ViewModelStore

Casos de uso

  • Persistir valores específicos da interface do usuário locais para instâncias componíveis individuais
  • Rastreamento de impressões, possivelmente por RetainedEffect
  • Elemento básico para definir um componente de arquitetura personalizado semelhante a ViewModel.
  • Extrair interações entre as camadas de interface e de dados para uma classe separada, tanto para organização do código quanto para testes.
  • Transformar Flows em objetos State e chamar funções de suspensão que não devem ser interrompidas por mudanças de configuração
  • Compartilhamento de estados em grandes áreas da interface, como telas inteiras
  • Interoperabilidade com View

Combine retain e rememberSaveable ou rememberSerializable

Às vezes, um objeto precisa ter um ciclo de vida híbrido de retained e rememberSaveable ou rememberSerializable. Isso pode ser um indicador de que seu objeto precisa ser um ViewModel, que pode oferecer suporte ao estado salvo conforme descrito no guia do módulo Saved State para ViewModel.

é possível usar retain e rememberSaveable ou rememberSerializable simultaneamente. Combinar corretamente os dois ciclos de vida adiciona uma complexidade significativa. Recomendamos usar esse padrão como parte de padrões de arquitetura mais avançados e personalizados, e somente quando todas as condições a seguir forem verdadeiras:

  • Você está definindo um objeto composto por uma mistura de valores que precisam ser mantidos ou salvos (por exemplo, um objeto que rastreia uma entrada do usuário e um cache na memória que não pode ser gravado em disco).
  • Seu estado está no escopo de um elemento combinável e não é adequado para o escopo ou ciclo de vida singleton de ViewModel.

Quando todos esses casos são verdadeiros, recomendamos dividir a classe em três partes: os dados salvos, os dados retidos e um objeto "mediador" que não tem estado próprio e delega aos objetos retidos e salvos para atualizar o estado de acordo. Esse padrão tem o seguinte formato:

@Composable
fun rememberAndRetain(): CombinedRememberRetained {
    val saveData = rememberSerializable(serializer = serializer<ExtractedSaveData>()) {
        ExtractedSaveData()
    }
    val retainData = retain { ExtractedRetainData() }
    return remember(saveData, retainData) {
        CombinedRememberRetained(saveData, retainData)
    }
}

@Serializable
data class ExtractedSaveData(
    // All values that should persist process death should be managed by this class.
    var savedData: AnotherSerializableType = defaultValue()
)

class ExtractedRetainData {
    // All values that should be retained should appear in this class.
    // It's possible to manage a CoroutineScope using RetainObserver.
    // See the full sample for details.
    var retainedData = Any()
}

class CombinedRememberRetained(
    private val saveData: ExtractedSaveData,
    private val retainData: ExtractedRetainData,
) {
    fun doAction() {
        // Manipulate the retained and saved state as needed.
    }
}

Ao separar o estado por ciclo de vida, a separação de responsabilidades e armazenamento se torna muito explícita. É intencional que os dados de salvamento não possam ser manipulados por dados de retenção, já que isso evita um cenário em que uma atualização de dados de salvamento é tentada quando o pacote savedInstanceState já foi capturado e não pode ser atualizado. Ele também permite testar cenários de recriação testando seus construtores sem chamar o Compose ou simular uma recriação de atividade.

Consulte o exemplo completo (RetainAndSaveSample.kt) para ver um exemplo completo de como esse padrão pode ser implementado.

Memoização posicional e layouts adaptáveis

Os aplicativos Android podem oferecer suporte a vários formatos, incluindo smartphones, dobráveis, tablets e computadores. Os aplicativos precisam fazer a transição entre esses formatos usando layouts adaptáveis. Por exemplo, um app executado em um tablet pode mostrar uma visualização de lista e detalhes em duas colunas, mas pode navegar entre uma lista e uma página de detalhes quando apresentado em uma tela de smartphone menor.

Como os valores memorizados e retidos são memoizados posicionalmente, eles só são reutilizados se aparecerem no mesmo ponto na hierarquia de composição. À medida que seus layouts se adaptam a diferentes formatos, eles podem alterar a estrutura da hierarquia de composição e levar a valores esquecidos.

Para componentes prontos para uso, como ListDetailPaneScaffold e NavDisplay (do Jetpack Navigation 3), isso não é um problema, e seu estado vai persistir em todas as mudanças de layout. Para componentes personalizados que se adaptam a formatos, garanta que o estado não seja afetado por mudanças de layout fazendo uma das seguintes ações:

  • Verifique se os elementos combináveis com estado são sempre chamados no mesmo lugar na hierarquia de composição. Implemente layouts adaptáveis alterando a lógica de layout em vez de realocar objetos na hierarquia de composição.
  • Use MovableContent para realocar elementos combináveis com estado de maneira otimizada. As instâncias de MovableContent podem mover valores lembrados e retidos dos locais antigos para os novos.

Lembre-se das funções de fábrica

Embora as interfaces do Compose sejam formadas por funções combináveis, muitos objetos entram na criação e organização de uma composição. O exemplo mais comum disso são objetos combináveis complexos que definem o próprio estado, como LazyList, que aceita um LazyListState.

Ao definir objetos focados no Compose, recomendamos criar uma função remember para definir o comportamento de memorização pretendido, incluindo ciclo de vida e entradas de chave. Isso permite que os consumidores do seu estado criem instâncias com confiança na hierarquia de composição que vão sobreviver e ser invalidadas conforme o esperado. Ao definir uma função de fábrica combinável, siga estas diretrizes:

  • Adicione o prefixo remember ao nome da função. Opcionalmente, se a implementação da função depender de o objeto ser retained e a API nunca evoluir para depender de uma variação diferente de remember, use o prefixo retain.
  • Use rememberSaveable ou rememberSerializable se a persistência de estado for escolhida e for possível escrever uma implementação Saver correta.
  • Evite efeitos colaterais ou inicializar valores com base em CompositionLocals que podem não ser relevantes para o uso. O local em que o estado é criado pode não ser o mesmo em que ele é consumido.

@Composable
fun rememberImageState(
    imageUri: String,
    initialZoom: Float = 1f,
    initialPanX: Int = 0,
    initialPanY: Int = 0
): ImageState {
    return rememberSaveable(imageUri, saver = ImageState.Saver) {
        ImageState(
            imageUri, initialZoom, initialPanX, initialPanY
        )
    }
}

data class ImageState(
    val imageUri: String,
    val zoom: Float,
    val panX: Int,
    val panY: Int
) {
    object Saver : androidx.compose.runtime.saveable.Saver<ImageState, Any> by listSaver(
        save = { listOf(it.imageUri, it.zoom, it.panX, it.panY) },
        restore = { ImageState(it[0] as String, it[1] as Float, it[2] as Int, it[3] as Int) }
    )
}