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 recomposições, conforme explicado em Estado e Jetpack Compose.

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

Escolha a vida útil correta

No Compose, há várias funções que podem ser usadas para manter o estado em composições e além: remember, retain, rememberSaveable e rememberSerializable. Essas funções diferem em termos de ciclo de vida e 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

Todos

Não pode referenciar 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 para a composição
  • Objetos de configuração para elementos que podem ser compostos
  • 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 o estado no Compose. Quando remember é chamado pela primeira vez, o cálculo fornecido é executado e lembrado, ou seja, é armazenado pelo Compose para reutilização futura pelo combinável. Quando um elemento combinável é recomposto, ele executa o código novamente, mas 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 o próprio conjunto de valores lembrados, chamado de memorização posicional. Quando os valores lembrados são memorizados para uso em recomposições, eles são vinculados à posição deles na hierarquia da composição. Se um elemento combinável for usado em locais diferentes, cada instância na hierarquia de composição terá o próprio conjunto de valores lembrados.

Quando um valor lembrado não é mais usado, ele é esquecido e o registro dele é descartado. Os valores memorizados são esquecidos quando são removidos da hierarquia de composição (incluindo quando um valor é removido e adicionado novamente para ser movido 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 a menor vida útil e esquece os valores antes das outras quatro funções de memorização descritas nesta página. Por isso, ele é mais adequado para:

  • Criar objetos de estado interno, como posição de rolagem ou estado de animação
  • Como 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 nas mudanças de configuração da atividade e no encerramento do processo iniciado pelo sistema.

rememberSaveable e rememberSerializable

rememberSaveable e rememberSerializable são criados com base em remember. Elas têm a vida útil mais longa das funções de memoização discutidas neste guia. Além de memorizar objetos de forma posicional 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 aplicativos em primeiro plano ou se o usuário revoga 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 que são serializáveis com a biblioteca kotlinx.serialization. Escolha rememberSerializable se o tipo estiver (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 memorizar qualquer estado que o 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 representáveis 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, implemente um Saver personalizado para serializar e desserializar o 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 a seguir um exemplo de Saver para uma classe Size. Ele é implementado ao empacotar 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 quanto tempo ela memoriza os valores. Ele tem um nome diferente porque os valores retidos também têm um ciclo de vida diferente dos valores lembrados.

Quando um valor é retido, ele é memorizado posicionalmente e salvo em uma estrutura de dados secundária que tem um ciclo de vida separado, vinculado ao ciclo de vida do app. Um valor retido pode sobreviver a mudanças de configuração sem ser serializado, mas não pode 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á descartado, o que é o equivalente de retain para ser esquecido.

Em troca desse ciclo de vida mais curto que o de rememberSaveable, o retain pode 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) para 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

Em essência, retain e ViewModel oferecem uma funcionalidade semelhante na capacidade mais comum de manter instâncias de objeto em mudanças de configuração. A escolha entre retain ou ViewModel depende do tipo de valor que você está mantendo, de como ele deve ser definido e se você precisa de mais funcionalidades.

ViewModels são objetos que normalmente encapsulam a comunicação entre as camadas de dados e de interface do app. Eles permitem que você mova a lógica para fora das funções combináveis, o que melhora a testabilidade. Os ViewModels são gerenciados como singletons em um ViewModelStore e têm uma vida útil diferente dos valores retidos. Enquanto um ViewModel permanece 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 será desativado se a hierarquia da interface for recriada e o valor retido não for 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 o Dagger e o Hilt, integração com o SavedState e suporte integrado a corrotinas para iniciar tarefas em segundo plano. Isso faz com que o ViewModel seja o lugar ideal para lançar tarefas em segundo plano e solicitações de rede, interagir com outras fontes de dados no projeto e, se quiser, capturar e persistir o estado da interface de missão crítica, que precisa ser retido em todas as mudanças de configuração no ViewModel e sobreviver ao encerramento do processo.

retain é mais adequado para objetos com escopo definido para instâncias de composição específicas e que não exigem reutilização ou compartilhamento entre composições semelhantes. 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 a conexão 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 projetam 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 a corrotinas e estado salvo não seja oferecido de forma predefinida, o retain pode servir como o elemento básico para o ciclo de vida de sósias do ViewModel com esses recursos integrados. As especificações de como projetar esse componente estão fora do escopo deste guia.

retain

ViewModel

Escopo

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

Os 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.

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

Pertence a

RetainedValuesStore

ViewModelStore

Casos de uso

  • Como manter valores específicos da IU locais para instâncias combináveis individuais
  • Rastreamento de impressões, possivelmente por meio de RetainedEffect
  • Elemento básico para definir um componente de arquitetura personalizado "semelhante ao ViewModel"
  • Extrair interações entre as camadas de interface e de dados em uma classe separada, tanto para organização de 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.
  • Compartilhar estados em grandes áreas da IU, como telas inteiras
  • Interoperabilidade com View

Combine retain e rememberSaveable ou rememberSerializable

Às vezes, um objeto precisa ter uma vida útil híbrida de retained e rememberSaveable ou rememberSerializable. Isso pode ser um indicador de que o 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 os dois ciclos de vida corretamente adiciona uma complexidade significativa. Recomendamos o uso desse padrão como parte de padrões de arquitetura mais avançados e personalizados, e somente quando todas as seguintes condições forem verdadeiras:

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

Quando todos esses casos se aplicam, recomendamos dividir a classe em três partes: os dados salvos, os dados retidos e um objeto "mediador" que não tem um 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 salvos não possam ser manipulados por dados retidos, já que isso evita um cenário em que uma atualização de dados salvos é 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.

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

Memoização posicional e layouts adaptáveis

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

Como os valores lembrados e retidos são memorizados por posição, eles só são reutilizados se aparecerem no mesmo ponto da hierarquia de composição. À medida que os 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 o estado vai persistir durante 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 seguindo um destes procedimentos:

  • Verifique se os elementos de composição 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 adequada. As instâncias de MovableContent podem mover valores lembrados e retidos dos locais antigos para os novos.

Lembrar das funções de fábrica

Embora as interfaces do Compose sejam compostas 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 lembrança pretendido, incluindo a vida útil e as entradas de chave. Isso permite que os consumidores do seu estado criem com confiança instâncias 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. Como alternativa, se a implementação da função depender do objeto ser retained e a API nunca for 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 correta de Saver.
  • Evite efeitos colaterais ou inicializar valores com base em CompositionLocals que podem não ser relevantes para o uso. Lembre-se de que 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) }
    )
}