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:
|
|
|
|
|---|---|---|---|
Os valores sobrevivem às recomposições? |
✅ |
✅ |
✅ |
Os valores sobrevivem às recriações de atividades? |
❌ |
✅ A mesma instância ( |
✅ Um objeto equivalente ( |
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 |
Casos de uso |
|
|
|
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),Stringou 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.
|
|
|
|---|---|---|
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. |
|
Destruição |
Ao sair permanentemente da hierarquia de composição |
Quando o |
Funcionalidade adicional |
Pode receber callbacks quando o objeto está ou não na hierarquia de composição |
O |
Propriedade de |
|
|
Casos de uso |
|
|
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
MovableContentpara realocar elementos combináveis com estado de maneira otimizada. As instâncias deMovableContentpodem 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
rememberao nome da função. Opcionalmente, se a implementação da função depender de o objeto serretainede a API nunca evoluir para depender de uma variação diferente deremember, use o prefixoretain. - Use
rememberSaveableourememberSerializablese a persistência de estado for escolhida e for possível escrever uma implementaçãoSavercorreta. - 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) } ) }