No Jetpack Compose, as funções combináveis geralmente mantêm o estado usando a função remember. Os valores memorizados podem ser reutilizados em recomposições, conforme
explicado em Estado e Jetpack Compose.
Embora remember sirva como uma ferramenta para manter valores em recomposições, o estado geralmente precisa viver além da vida útil 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 memorizados e mantidos no Compose.
Escolher o tempo de vida correto
No Compose, há várias funções que podem ser usadas para manter o estado em
composições e além delas: remember, retain, rememberSaveable e
rememberSerializable. Essas funções diferem no tempo 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 permanecem vigentes nas 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 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 o estado no Compose. Quando remember é
chamado pela primeira vez, o cálculo fornecido é executado e é
memorizado, o que significa que ele é 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 memorizados, conhecido como memoização posicional. Quando os valores memorizados são memoizados para uso em 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 memorizados.
Quando um valor memorizado não é mais usado, ele é esquecido e o registro é descartado. Os valores memorizados são esquecidos quando são removidos da hierarquia de composição (inclusive quando um valor é removido e adicionado novamente para passar 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 tempo de vida e esquece os valores mais cedo das 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 dispendiosos em cada recomposição
No entanto, evite:
- Armazenar qualquer entrada do usuário com
remember, porque os objetos memorizados são esquecidos em mudanças de configuração de atividades e encerramento de processos iniciados pelo sistema.
rememberSaveable e rememberSerializable
rememberSaveable e rememberSerializable são criados com base em remember. Eles têm o maior tempo de vida das funções de memoização discutidas neste guia.
Além de memoizar objetos posicionalmente em recomposições, eles também podem salvar valores para que possam ser restaurados em recriações de atividades, incluindo mudanças de configuração e encerramento de processos (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 estiver em execução).
rememberSerializable funciona da mesma maneira que rememberSaveable, mas oferece suporte automático à persistência de tipos complexos que podem ser serializados 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 memoizar 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 memoizados serializando-os em um Bundle. Isso tem duas consequências:
- Os valores memoizados 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 será 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 com suporte. O Compose entende tipos de dados comuns, como State, List, Map, Set etc., e os converte automaticamente em tipos com suporte. Confira a seguir um exemplo de Saver para uma classe Size. Ele é implementado empacotando 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 memoização dos
valores. Ela tem um nome diferente porque os valores mantidos também têm um ciclo de vida diferente dos equivalentes memorizados.
Quando um valor é mantido, ele é memoizado posicionalmente e salvo em uma
estrutura de dados secundária que tem um tempo de vida separado vinculado ao tempo de vida do app's
tempo de vida. Um valor mantido 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 após a recriação da hierarquia de composição, o valor mantido será desativado (que é o equivalente de retain a ser esquecido).
Em troca desse ciclo de vida mais curto que rememberSaveable, retain pode manter 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
Basicamente, retain e ViewModel oferecem funcionalidades semelhantes na capacidade mais usados com frequência 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á mantendo, de como ele deve ser definido e se você precisa de funcionalidades adicionais.
Os ViewModels são objetos que normalmente encapsulam a comunicação entre a interface e as camadas de dados do app. Eles permitem mover a lógica das
funções combináveis, o que melhora a capacidade de teste. ViewModels são gerenciados como
singletons em um ViewModelStore e têm um tempo de vida diferente dos valores
mantidos. Um ViewModel permanece ativo até que o ViewModelStore seja destruído, enquanto os valores mantidos são desativados quando o conteúdo é removido permanentemente da composição. Por exemplo, para uma mudança de configuração, isso significa que um valor mantido é desativado se a hierarquia da interface for recriada e o valor mantido não for consumido após a recriação da composiçã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 a corrotinas integrado para iniciar tarefas em segundo plano. Isso torna ViewModel um lugar ideal para iniciar tarefas em segundo plano e solicitações de rede, interagir com outras fontes de dados no projeto e, opcionalmente, capturar e manter o estado da interface essencial que deve ser mantido em mudanças de configuração no ViewModel e sobreviver ao encerramento do processo.
retain é mais adequado para objetos com escopo em instâncias combináveis específicas e que 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 executar 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 projetam padrões de arquitetura de apps personalizados fora das recomendações da arquitetura moderna de apps Android, retain também pode ser usado para criar uma API interna semelhante a ViewModel. Embora o suporte a corrotinas e estado salvo não seja oferecido pronto para uso, retain pode servir como o bloco de construção para o ciclo de vida de componentes semelhantes a ViewModel com esses recursos criados. Os detalhes de como projetar um componente desse tipo estão fora do escopo deste guia.
|
|
|
|---|---|---|
Escopo |
Nenhum valor compartilhado. Cada valor é mantido 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. |
|
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 |
Integrado |
Pertence a |
|
|
Casos de uso |
|
|
Combinar retain e rememberSaveable ou rememberSerializable
Às vezes, um objeto precisa ter um tempo de vida híbrido 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 em
o guia do módulo Saved State para ViewModel.
É possível usar retain e rememberSaveable ou rememberSerializable simultaneamente. A combinação correta dos dois ciclos de vida adiciona uma complexidade significativa.
Recomendamos empregar 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 combinação 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 no disco)
- O estado tem escopo em um elemento combinável e não é adequado para o escopo singleton ou o tempo de vida do
ViewModel
Quando todas essas condições forem verdadeiras, recomendamos dividir a classe em três partes: os dados salvos, os dados mantidos e um objeto "mediador" que não tem estado próprio e delega aos objetos mantidos 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 tempo 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 mantidos, já que isso impede 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 os construtores sem chamar o Compose ou simular uma recriação de atividade.
Consulte o exemplo completo (RetainAndSaveSample.kt) para conferir 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 muitos 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 executado em um tablet pode mostrar uma visualização de detalhes e listas de 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 mantidos são memoizados posicionalmente, 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), esse não é um problema, e o 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:
- Garanta que os elementos combináveis com estado sejam 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 normalmente. As instâncias deMovableContentpodem mover valores memorizados e mantidos de locais antigos para novos.
Memorizar 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 memorização pretendido, incluindo o tempo de vida e as entradas de chave. Isso permite que os consumidores do estado criem instâncias na hierarquia de composição que vão permanecer vigente 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 do objeto sendoretainede a API nunca evoluir para depender de uma variação diferente deremember, use o prefixoretainem vez disso. - Use
rememberSaveableourememberSerializablese a persistência de estado for escolhida e for possível gravar uma implementaçãoSavercorreta. - Evite efeitos colaterais ou valores de inicialização 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 onde 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) } ) }