Módulo Saved State para ViewModel Parte do Android Jetpack.
Conforme mencionado em
Como salvar estados de IU,
os objetos ViewModel podem processar
mudanças de configuração, então você não precisa se preocupar com o estado em rotações
ou outros casos. No entanto, se você precisa lidar com encerramentos de processo
iniciados pelo sistema, é recomendável usar a API SavedStateHandle como backup.
Em geral, o estado da interface é armazenado ou referenciado em objetos ViewModel, não
em atividades. O uso de onSaveInstanceState() ou rememberSaveable exige um código boilerplate que
o módulo Saved State pode processar
para você.
Ao usar esse módulo, os objetos ViewModel recebem um objeto
SavedStateHandle
pelo construtor. Esse objeto é um mapa de chave-valor que permite
gravar e acessar objetos de e para o estado salvo. Esses valores
persistem depois que o processo é encerrado pelo sistema e permanecem disponíveis
pelo mesmo objeto.
O estado salvo fica vinculado à pilha de tarefas. Portanto, se ela desaparece, o estado também desaparece. Isso pode ocorrer ao forçar o fechamento ou remover o app do menu "Recentes" ou ao reiniciar o dispositivo. Nesses casos, a pilha de tarefas desaparece e não é possível restaurar as informações do estado salvo. Em cenários em que o estado da interface iniciado pelo usuário é dispensado, o estado salvo não é restaurado. Já em cenários iniciados pelo sistema, o estado é salvo.
Configurar
A partir do Fragment 1.2.0
ou da respectiva dependência transitiva
Activity 1.1.0, é possível aceitar
um SavedStateHandle como argumento de construtor para seu ViewModel.
Kotlin
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
Java
public class SavedStateViewModel extends ViewModel { private SavedStateHandle state; public SavedStateViewModel(SavedStateHandle savedStateHandle) { state = savedStateHandle; } ... }
Assim, você pode extrair uma instância do ViewModel sem qualquer outra
configuração. A fábrica ViewModel padrão fornece o
SavedStateHandle apropriado para seu ViewModel.
Kotlin
class MainFragment : Fragment() { val vm: SavedStateViewModel by viewModels() ... }
Java
class MainFragment extends Fragment { private SavedStateViewModel vm; public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { vm = new ViewModelProvider(this).get(SavedStateViewModel.class); ... } ... }
Ao fornecer uma instância
ViewModelProvider.Factory,
personalizada, é possível ativar o uso de SavedStateHandle estendendo
AbstractSavedStateViewModelFactory.
Como trabalhar com SavedStateHandle
A classe SavedStateHandle é um mapa de chave-valor que permite gravar e
acessar dados de e para o estado salvo, usando os métodos
set()
e get().
Usando SavedStateHandle, o valor da consulta é retido após o encerramento do processo,
garantindo que o usuário veja o mesmo conjunto de dados filtrados antes e depois
da recriação, sem que a atividade ou o fragmento precise salvar, restaurar
e encaminhar manualmente esse valor de volta para ViewModel.
SavedStateHandle também tem outros métodos que podem ser esperados ao interagir
com um mapa de chave-valor:
contains(String key): verifica se há um valor para a chave fornecida.remove(String key): remove o valor da chave especificada.keys(): retorna todas as chaves contidas emSavedStateHandle.
Além disso, é possível extrair valores de SavedStateHandle usando um
detentor de dados observáveis. Veja a lista de tipos com suporte:
LiveData
Extraia valores de SavedStateHandle que são encapsulados em um
LiveData observável usando
getLiveData().
Quando o valor da chave é atualizado, o LiveData recebe o novo valor. Na maioria
das vezes, o valor é definido devido a interações do usuário, como a inserção de uma consulta para
filtrar uma lista de dados. Esse valor atualizado pode ser usado para
transformar LiveData.
Kotlin
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { val filteredData: LiveData<List<String>> = savedStateHandle.getLiveData<String>("query").switchMap { query -> repository.getFilteredData(query) } fun setQuery(query: String) { savedStateHandle["query"] = query } }
Java
public class SavedStateViewModel extends ViewModel { private SavedStateHandle savedStateHandle; public LiveData<List<String>> filteredData; public SavedStateViewModel(SavedStateHandle savedStateHandle) { this.savedStateHandle = savedStateHandle; LiveData<String> queryLiveData = savedStateHandle.getLiveData("query"); filteredData = Transformations.switchMap(queryLiveData, query -> { return repository.getFilteredData(query); }); } public void setQuery(String query) { savedStateHandle.set("query", query); } }
StateFlow
Extraia valores de SavedStateHandle que são encapsulados em um
StateFlow observável usando
getStateFlow().
Quando você atualiza o valor da chave, o StateFlow recebe o novo valor. Na maioria
das vezes, é possível definir o valor devido a interações do usuário, por exemplo, a inserção de uma
consulta para filtrar uma lista de dados. Em seguida, você pode transformar esse valor atualizado
usando outros operadores do Flow.
Kotlin
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { val filteredData: StateFlow<List<String>> = savedStateHandle.getStateFlow<String>("query") .flatMapLatest { query -> repository.getFilteredData(query) } fun setQuery(query: String) { savedStateHandle["query"] = query } }
Suporte para o estado experimental do Compose
O artefato lifecycle-viewmodel-compose fornece as APIs experimentais
saveable
que permitem a interoperabilidade entre o SavedStateHandle e o
Saver do Compose. Todo State que você pode salvar usando rememberSaveable
com um Saver personalizado também pode ser salvo com SavedStateHandle.
Kotlin
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { var filteredData: List<String> by savedStateHandle.saveable { mutableStateOf(emptyList()) } fun setQuery(query: String) { withMutableSnapshot { filteredData += query } } }
Tipos com suporte
Dados mantidos em um SavedStateHandle são salvos e restaurados como um
Bundle, com o restante do
savedInstanceState
para a atividade ou o fragmento.
Tipos com suporte direto
Por padrão, você pode chamar set() e get() em um SavedStateHandle para os
mesmos tipos de dados que um Bundle, como mostrado abaixo.
| Suporte para tipo/classe | Suporte para matriz |
double |
double[] |
int |
int[] |
long |
long[] |
String |
String[] |
byte |
byte[] |
char |
char[] |
CharSequence |
CharSequence[] |
float |
float[] |
Parcelable |
Parcelable[] |
Serializable |
Serializable[] |
short |
short[] |
SparseArray |
|
Binder |
|
Bundle |
|
ArrayList |
|
Size (only in API 21+) |
|
SizeF (only in API 21+) |
Se a classe não estender um desses itens na lista acima, parcele-a
adicionando a anotação @Parcelize
do Kotlin ou implementando
Parcelable diretamente.
Salvar classes não comparáveis
Se uma classe não implementar Parcelable ou Serializable e não puder ser modificada
para implementar uma dessas interfaces, não será
possível salvar diretamente uma instância dessa classe em um SavedStateHandle.
No
Lifecycle 2.3.0-alpha03 e versões mais recentes,
SavedStateHandle permite que você salve qualquer objeto fornecendo sua
lógica para salvar e restaurar o objeto como um
Bundle usando o
método
setSavedStateProvider(). SavedStateRegistry.SavedStateProvider
é uma interface que define um único método
saveState()
que retorna um Bundle contendo o estado que você quer salvar. Quando
SavedStateHandle está pronto para salvar o estado, ele chama saveState()
para extrair o Bundle do SavedStateProvider e salvar o
Bundle para a chave associada.
Imagine um exemplo de app que solicita uma imagem do app de câmera pela
intent ACTION_IMAGE_CAPTURE,
transmitindo um arquivo temporário para onde a câmera vai armazenar a
imagem. O TempFileViewModel encapsula a lógica para criar esse
arquivo temporário.
Kotlin
class TempFileViewModel : ViewModel() { private var tempFile: File? = null fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
Java
class TempFileViewModel extends ViewModel { private File tempFile = null; public TempFileViewModel() { } @NonNull public File createOrGetTempFile() { if (tempFile == null) { tempFile = File.createTempFile("temp", null); } return tempFile; } }
Para garantir que o arquivo temporário não seja perdido se o processo da atividade for encerrado
e depois restaurado, TempFileViewModel pode usar SavedStateHandle
para manter os dados. Para permitir que TempFileViewModel salve os dados, implemente
SavedStateProvider e defina-o como um provedor no SavedStateHandle
do ViewModel:
Kotlin
private fun File.saveTempFile() = bundleOf("path", absolutePath) class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private var tempFile: File? = null init { savedStateHandle.setSavedStateProvider("temp_file") { // saveState() if (tempFile != null) { tempFile.saveTempFile() } else { Bundle() } } } fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
Java
class TempFileViewModel extends ViewModel { private File tempFile = null; public TempFileViewModel(SavedStateHandle savedStateHandle) { savedStateHandle.setSavedStateProvider("temp_file", new TempFileSavedStateProvider()); } @NonNull public File createOrGetTempFile() { if (tempFile == null) { tempFile = File.createTempFile("temp", null); } return tempFile; } private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider { @NonNull @Override public Bundle saveState() { Bundle bundle = new Bundle(); if (tempFile != null) { bundle.putString("path", tempFile.getAbsolutePath()); } return bundle; } } }
Para restaurar os dados de File quando o usuário retornar, acesse o Bundle
temp_file do SavedStateHandle. Esse é o mesmo Bundle fornecido pelo
saveTempFile() que contém o caminho absoluto. O caminho absoluto pode
ser usado para instanciar um novo File.
Kotlin
private fun File.saveTempFile() = bundleOf("path", absolutePath) private fun Bundle.restoreTempFile() = if (containsKey("path")) { File(getString("path")) } else { null } class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private var tempFile: File? = null init { val tempFileBundle = savedStateHandle.get<Bundle>("temp_file") if (tempFileBundle != null) { tempFile = tempFileBundle.restoreTempFile() } savedStateHandle.setSavedStateProvider("temp_file") { // saveState() if (tempFile != null) { tempFile.saveTempFile() } else { Bundle() } } } fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
Java
class TempFileViewModel extends ViewModel { private File tempFile = null; public TempFileViewModel(SavedStateHandle savedStateHandle) { Bundle tempFileBundle = savedStateHandle.get("temp_file"); if (tempFileBundle != null) { tempFile = TempFileSavedStateProvider.restoreTempFile(tempFileBundle); } savedStateHandle.setSavedStateProvider("temp_file", new TempFileSavedStateProvider()); } @NonNull public File createOrGetTempFile() { if (tempFile == null) { tempFile = File.createTempFile("temp", null); } return tempFile; } private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider { @NonNull @Override public Bundle saveState() { Bundle bundle = new Bundle(); if (tempFile != null) { bundle.putString("path", tempFile.getAbsolutePath()); } return bundle; } @Nullable private static File restoreTempFile(Bundle bundle) { if (bundle.containsKey("path") { return File(bundle.getString("path")); } return null; } } }
Usar o SavedStateHandle em testes
Para testar um ViewModel que usa um SavedStateHandle como dependência, crie
uma nova instância de SavedStateHandle com os valores de teste necessários e o transmita
para a instância do ViewModel que você está testando.
Kotlin
class MyViewModelTest { private lateinit var viewModel: MyViewModel @Before fun setup() { val savedState = SavedStateHandle(mapOf("someIdArg" to testId)) viewModel = MyViewModel(savedState = savedState) } }
Outros recursos
Para mais informações sobre o módulo Saved State para ViewModel, consulte os
recursos a seguir.
Codelabs
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Salvar estados da interface
- Trabalhar com objetos de dados observáveis
- Criar ViewModels com dependências