Módulo de estado guardado para ViewModel Parte de Android Jetpack.

Según se mencionó en Cómo guardar estados de IU, los objetos ViewModel pueden manejar cambios de configuración, por lo que no necesitas preocuparte por el estado en rotaciones y otros casos. Sin embargo, si necesitas controlar el cierre del proceso que inició el sistema, te recomendamos que uses la API de SavedStateHandle como respaldo.

Por lo general, se almacena o se hace referencia al estado de la IU en objetos ViewModel y no en actividades, por lo que usar onSaveInstanceState() o rememberSaveable requiere código estándar que el módulo de estado guardado pueda manejar por ti.

Cuando se usa este módulo, los objetos ViewModel reciben un objeto SavedStateHandle mediante su constructor. Este objeto es un mapa de clave-valor que te permite escribir y recuperar objetos en el estado guardado y desde este. Estos valores se conservan después de que el sistema finaliza el proceso y quedan disponibles a través del mismo objeto.

El estado guardado está vinculado a la pila de tareas. Si la pila de tareas desaparece, también desaparece el estado guardado. Esto puede ocurrir cuando fuerzas la detención de una app, la quitas del menú Recientes o reinicias el dispositivo. En esos casos, la pila de tareas desaparece y no puedes restablecer la información en estado guardado. En las situaciones de descarte del estado de la IU iniciada por el usuario, no se restablece el estado guardado. En situaciones iniciadas por el sistema, sí se restablece.

Configuración

A partir de Fragment 1.2.0 o su dependencia transitiva Activity 1.1.0, puedes aceptar un SavedStateHandle como argumento de constructor de tu ViewModel.

Kotlin

class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }

Java

public class SavedStateViewModel extends ViewModel {
    private SavedStateHandle state;

    public SavedStateViewModel(SavedStateHandle savedStateHandle) {
        state = savedStateHandle;
    }

    ...
}

Luego, podrás recuperar una instancia de ViewModel sin ninguna configuración adicional. La fábrica predeterminada de ViewModel proporciona el SavedStateHandle apropiado para tu 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);

        ...


    }

    ...
}

Cuando proporcionas una instancia de ViewModelProvider.Factory personalizada, puedes habilitar el uso de SavedStateHandle extendiendo AbstractSavedStateViewModelFactory.

Cómo trabajar con SavedStateHandle

La clase SavedStateHandle es un mapa de claves-valor que te permite escribir y recuperar datos hacia el estado guardado y desde este mediante los métodos set() y get().

Mediante SavedStateHandle, se retiene el valor de la búsqueda luego del cierre del proceso, lo que asegura que el usuario vea el mismo conjunto de datos filtrados antes y después de la recreación sin necesidad de que la actividad o el fragmento guarden, restablezcan o reenvíen el valor a ViewModel de forma manual.

.

SavedStateHandle también tiene otros métodos que quizás esperas cuando interactúas con un mapa de par clave-valor:

Además, puedes recuperar valores de SavedStateHandle con un contenedor de datos observables. Estos son los tipos compatibles:

LiveData

Recupera valores de SavedStateHandle que se unen en un observable de LiveData por medio de getLiveData(). Cuando se actualiza el valor de la clave, LiveData recibe el valor nuevo. Con frecuencia, el valor se establece debido a las interacciones del usuario, como ingresar una búsqueda a fin de filtrar una lista de datos. Este valor actualizado se puede usar 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

Recupera valores de SavedStateHandle que se unen en un observable de StateFlow por medio de getStateFlow(). Cuando actualizas el valor de la clave, StateFlow recibe el valor nuevo. Con frecuencia, puedes establecer el valor debido a las interacciones del usuario, como ingresar una búsqueda para filtrar una lista de datos. Luego, puedes transformar este valor actualizado mediante otros operadores de flujo.

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
    }
}

Compatibilidad con estados experimentales de Compose

El artefacto lifecycle-viewmodel-compose proporciona las APIs experimentales de saveable que permiten la interoperabilidad entre SavedStateHandle y Saver de Compose, de modo que cualquier State que puedas guardar mediante rememberSaveable con un Saver personalizado también se pueda guardar con 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 admitidos

Los datos conservados en un SavedStateHandle se guardan y se restablecen como un Bundle, junto con el resto de las savedInstanceState correspondientes a la actividad o al fragmento.

Tipos admitidos de forma directa

De forma predeterminada, puedes llamar a set() y get() en un SavedStateHandle para los mismos tipos de datos que un Bundle, como se muestra a continuación:

Compatibilidad con tipo/clase Compatibilidad con arreglos
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+)

Si la clase no extiende una de las de la lista anterior, considera hacerla parcelable. Para ello, agrega la anotación @Parcelize de Kotlin o implementa Parcelable directamente.

Cómo guardar clases no parcelables

Si una clase no implementa Parcelable o Serializable y no se puede modificar para que implemente una de esas interfaces, entonces no es posible guardar directamente una instancia de esa clase en un SavedStateHandle.

Desde Lifecycle 2.3.0-alpha03, SavedStateHandle te permite guardar cualquier objeto, ya que proporciona tu propia lógica para guardar y restablecer el objeto como un Bundle mediante el método setSavedStateProvider(). SavedStateRegistry.SavedStateProvider es una interfaz que define un único método saveState() que muestra un Bundle que contiene el estado que quieres guardar. Cuando SavedStateHandle está listo para guardar su estado, llama a saveState() a fin de recuperar el Bundle de SavedStateProvider y guarda el Bundle para la clave asociada.

Considera un ejemplo de una app que solicita una imagen de la app de cámara mediante el intent ACTION_IMAGE_CAPTURE pasando un archivo temporal para el lugar donde la cámara debe almacenar la imagen. El TempFileViewModel encapsula la lógica para crear ese archivo temporal.

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;
    }
}

A fin de garantizar que el archivo temporal no se pierda si el proceso de la actividad finaliza y luego se restablece, TempFileViewModel puede usar SavedStateHandle para conservar sus datos. A fin de permitir que TempFileViewModel guarde sus datos, implementa SavedStateProvider y establécelo como un proveedor en el SavedStateHandle de 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 restablecer los datos de File cuando el usuario vuelva, recupera el Bundle del temp_file de SavedStateHandle. Este es el mismo Bundle proporcionado por saveTempFile() que contiene la ruta de acceso absoluta. Luego, se puede usar esta ruta para crear una instancia de un File nuevo.

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;
        }
    }
}

SavedStateHandle en las pruebas

Para probar un ViewModel que tome un SavedStateHandle como dependencia, crea una instancia nueva de SavedStateHandle con los valores de prueba que requiere y pásala a la instancia de ViewModel que estás probando.

Kotlin

class MyViewModelTest {

    private lateinit var viewModel: MyViewModel

    @Before
    fun setup() {
        val savedState = SavedStateHandle(mapOf("someIdArg" to testId))
        viewModel = MyViewModel(savedState = savedState)
    }
}

Recursos adicionales

Para obtener más información sobre el módulo de estado guardado para ViewModel, consulta los siguientes recursos.

Codelabs