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

Como 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 iniciado por el sistema, te recomendamos que uses onSaveInstanceState() 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() 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.

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 del par clave-valor que te permite escribir y recuperar datos hacia el estado guardado y desde este mediante los métodos set() y get(). Además, puedes recuperar valores de SavedStateHandle que se unen en un LiveData observable 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 UserViewModelJava(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);
    }
}

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 clave-valor:

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

Recursos adicionales

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

Codelabs