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:
|
|
|
|
|---|---|---|---|
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 |
Todos |
Não pode referenciar 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 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),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, 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.
|
|
|
|---|---|---|
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 |
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. |
|
Pertence a |
|
|
Casos de uso |
|
|
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
MovableContentpara realocar elementos combináveis com estado de maneira adequada. As instâncias deMovableContentpodem 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
rememberao nome da função. Como alternativa, se a implementação da função depender do objeto serretainede a API nunca for 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ção correta deSaver. - 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) } ) }