6

I have a screen that loads a bunch of requests and collects some data from the user on the same screen and an external WebView. Therefore, I have a ViewModel that contains these complex request objects (+ user input data). I need to persist this data through system-initiated process death, which SavedStateHandle is designed for. But I don't want to persist this data in a database because it is only relevant to the current user experience.

I have integrated my ViewModels with Hilt and received SaveStateHandle. Because I have some complex objects that are accessed/modified in several places in code I can't save them "on the go". I made them implement Parcelable and just wanted to save them at once. Unfortunately, ViewModels don't have a lifecycle method like onSaveInstanceState().

Now, I have tried using onCleared() which sounded like a ok place to write to the handle. But it turns out that all .set() operations I perform there get lost (I'm testing this with developer options "Don't keep activities". When I use .set() elsewhere, it does work). Because the ViewModel is not tied to the lifecycle of a single fragment/activity but rather to a NavGraph I can't call in from their onSaveInstanceState().

How/where can I properly persist my state in SaveStateHandle?

A1m
  • 2,897
  • 2
  • 24
  • 39

2 Answers2

5

This is precisely the use case that the Lifecycle 2.3.0-alpha03 release enables:

SavedStateHandle now supports lazy serialization by allowing you to call setSavedStateProvider() for a given key, providing a SavedStateProvider that will get a callback to saveState() when the SavedStateHandle is asked to save its state. (b/155106862)

This allows you to handle any complex object and get a callback exactly when it needs to be saved.

var complexObject: ComplexObject? = null

init {
    // When using setSavedStateProvider, the underlying data is
    // stored as a Bundle, so to extract any previously saved value,
    // we get it out of the Bundle, if one exists
    val initialState: Bundle = savedStateHandle.get<Bundle?>("complexObject")
    if (initialState != null) {
        // Convert the previously saved Bundle to your ComplexObject
        // Here, it is a single Parcelable, so we'll just get it out of
        // the bundle
        complexObject = initialState.getParcelable("parcelable")
    }

    // Now to register our callback for when to save our object,
    // we use setSavedStateProvider()
    savedStateHandle.setSavedStateProvider("complexObject") {
        // This callback requires that you return a Bundle.
        // You can either add your Parcelable directly or
        // skip being Parcelable and add the fields to the Bundle directly
        // The key is that the logic here needs to match your
        // initialState logic above.
        Bundle().apply {
            putParcelable("parcelable", complexObject)
        }
    }
}
ianhanniballake
  • 191,609
  • 30
  • 470
  • 443
  • I seem to run into a bug here. When I'm opening a chrome custom tab via intent the savedStateProvider is properly called and everything works fine. But when the user uses the system "Overview button" to navigate to a different app the call is not made (though the `onSaveInstanceState` of my `MainActivity` is called). Any idea? – A1m Jul 21 '20 at 03:29
  • @A1m - you can [file an issue against Lifecycle](https://issuetracker.google.com/issues/new?component=413132) with a sample project that reproduces your issue if you think there's a bug. – ianhanniballake Jul 21 '20 at 04:04
  • Personally I think this solution is ridiculous, why not just have an `override fun onSaveInstanceState()` method in the ViewModel? Or just support saving state in onCleared()? This is a huge oversight IMHO. – Wess Oct 22 '20 at 06:05
  • @Wess - You can certainly have your custom subclass of `ViewModel` have an `onSaveInstanceState()` method that hides all this logic, but `SavedStateHandle`, the class that actually controls all of the state saving, is purposefully usable from any `ViewModel` class (be it `ViewModel`, `AndroidViewModel`, or whatever other hierarchy of custom types you have). Note that `onCleared()` would never, ever be the appropriate place to save state - it is only called when your ViewModel is being permanently destroyed and never coming back. – ianhanniballake Oct 22 '20 at 15:12
  • 3
    @ianhanniballake that makes sense. But my suggestion is to rather provide an override method which signals to the `ViewModel` that it is going into a saved state soon and it should in that method save what it wants into the `SavedStateHandle`. I.o.w. similar to `override fun onSaveInstanceState(outState: Bundle)` in `Fragments`, except it doesn't have parameter `Bundle` - its purpose is only to notify the `ViewModel` that it should save any state now. In my view something like this would be an easier and more familiar solution to developers to save state lazily, without creating subclasses. – Wess Nov 13 '20 at 07:31
  • @Wess see my answer to the question. reduces some of the code – ono Dec 07 '22 at 23:32
0

Adding to @ianhanniballake, you don't need to add any data to Bundle. You can still access Parcelable (or another data type) directly. The callback still works when it needs to save it.

init {
    savedStateHandle.setSavedStateProvider("") {
        savedStateHandle["complexState"] = state
        Bundle()
    }
}

var state by mutableStateOf(
    savedStateHandle["complexState"] ?: ComplexState()
)
ono
  • 2,984
  • 9
  • 43
  • 85