https://kotlinlang.org logo
#compose
Title
# compose
m

mattinger

01/05/2022, 4:13 PM
So, if i have a ViewModel with a MutableState object, how can i get that tied to a SavedStateHandle? I was consdering manually doing this with a snapshotFlow and having the viewmodel start a coroutine to monitor it and write it to the savedStateHandle. But this seems like a ton of boilerplace for something that LiveData gets out of the box. See thread for code.
Copy code
class PodOnboardingViewModel(savedStateHandle: SavedStateHandle): ViewModel() {
    private val _state: MutableState<PodOnboardingState> =
        mutableStateOf(PodOnboardingState())

    val state: State<PodOnboardingState>
        get() = _state

    init {
        val restored: PodOnboardingState? = savedStateHandle.get(KEY)
        restored?.let { _state.value = it }

        val stateFlow = snapshotFlow { state.value }

        viewModelScope.launch {
            stateFlow.collect { current ->
                savedStateHandle.set(KEY, current)
            }
        }
    }

    companion object {
        private val KEY = "PodOnboardingViewModel.state"
    }
}
I see SnapshotMutableState interface, but neither of the implementations are public.
I came up with this class, but i feel like this is something that’s either missing from compose, or i just am not finding something:
Copy code
class SavingMutableState<T>(
    initialValue: T,
    private val savedStateHandle: SavedStateHandle,
    private val key: String
): MutableState<T> {
    private val internalState: MutableState<T> =
        mutableStateOf(initialValue)

    init {
        val previousValue: T? = savedStateHandle.get(key)
        if (previousValue == null) {
            internalState.value = initialValue
            savedStateHandle.set(key, initialValue)
        } else {
            internalState.value = previousValue
        }
    }

    override var value: T
        get() = internalState.value
        set(value) {
            internalState.value = value
            savedStateHandle.set(key, value)
        }

    override fun component1(): T =
        value

    override fun component2(): (T) -> Unit = {
        value = it
    }
}
a

Alex Vanyo

01/05/2022, 4:53 PM
You are right, this is something that doesn’t have a nice library-provided way at the moment. There’s an issue here to follow it: https://issuetracker.google.com/issues/195689777
k

K Merle

01/05/2022, 4:56 PM
Is there a reason for only adding function if variable is already immutable?
Copy code
val state: State<PodOnboardingState>
        get() = _state
m

mattinger

01/05/2022, 5:08 PM
@K Merle so that we’re exposing it as a State object and not a MutableState object. I don’t want external things mutating the state. I have other mutator functions not shown for doing such things:
Copy code
var serialNumber: String?
        get()  { return _state.value.serialNumber }
        set(value) { _state.value = _state.value.copy(serialNumber = value) }
k

K Merle

01/05/2022, 5:12 PM
Isn't
val state: State<PodOnboardingState> = _state
the same?
m

mattinger

01/05/2022, 5:14 PM
pretty much, yes.
that said, it’s not the point of this post 😉
k

K Merle

01/05/2022, 5:15 PM
Yea, sorry. I'm just curious.
m

mattinger

01/05/2022, 5:15 PM
I was just looking for confirmation that what i did really needs to be done.
i

Ian Lake

01/05/2022, 6:32 PM
💯 1
m

mattinger

01/05/2022, 8:50 PM
@Ian Lake Where is that extension function? I’m not finding it through code completion, but using the above wrapper class, i did write my own extension function. which just instantiates an instance of the wrapper class.
i

Ian Lake

01/05/2022, 8:52 PM
'lets you write' means you have to write it, there's no library that provides that functionality at this moment
m

mattinger

01/05/2022, 8:53 PM
Yeah, i just noticed that as i was reading through that link. So yeah, i essentially have the same thing.
i

Ian Lake

01/05/2022, 8:54 PM
Not quite, since you assume your
T
can actually be put in a
SavedStateHandle
directly while the code I provided supports any type of
T
by using the
Saver
APIs
m

mattinger

01/05/2022, 8:55 PM
yeah, mine is not as flexible, but all my models i’m putting in there this way will be Parcelable, so i have that going for me.
I assume i can just use autoSaver() since my models are all Parcelable, but if they weren’t i could provide my own implementation of Saver i guess.
i

Ian Lake

01/05/2022, 8:59 PM
Correct, exactly like
rememberSaveable
🙂
m

mattinger

01/05/2022, 9:01 PM
I pushed my implementation to our utils repo. Perhaps i’ll replace it later with the more generic one. Thanks as always @Ian Lake for the explanations.
I’m basically our compose expert at the moment, so i have my hands dipped into all parts of it as we get going with using it.
@Ian Lake Any suggestions how to do tests on this? i’m trying to setup initial values for the viewmodel and see how it performs, but i can’t seem to get the savedStateHandle in the right state
a

Alex Vanyo

01/05/2022, 10:26 PM
A
SavedStateHandle
is just a plain class that you can instantiate directly for tests, so you should be able to construct one directly with whatever state you need via
SavedStateHandle(initialState: Map<String, Any>)
or modifying it before passing it to the `ViewModel`’s constructor.
m

mattinger

01/05/2022, 10:27 PM
I think i figured it out. It’s just odd because of what’s being put in the bundle:
Copy code
with(autoSaver<PodOnboardingState>()) {
            val saverScope = SaverScope { true }
            savedStateHandle.set(
                PodOnboardingViewModel.KEY,
                bundleOf("value" to saverScope.save(PodOnboardingState(serialNumber = "1")))
            )
        }
It’s basically backed by a map of value to a provider that can be used to save the value. it just makes the test setup a bit more difficult than simple set and get on the savedStateHandle
i

Ian Lake

01/05/2022, 10:51 PM
autoSaver()
isn't going to do anything to a Parcelable when you
save
it, it just returns that exact object back so none of that
saverScope
code should be necessary (see: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]onMain/kotlin/androidx/compose/runtime/saveable/Saver.kt;l=96)
m

mattinger

01/06/2022, 2:25 PM
I think the bigger issue I’m having @Ian Lake is retrieving the value from the existing SavedStateHandle. There seems to be no way to access the internal providers. The .get method only looks in the
mRegular
field, which doesn’t have an entry for the providers. It just seems odd because the keys() function does look in there:
Copy code
@MainThread
    @NonNull
    public Set<String> keys() {
        HashSet<String> allKeys = new HashSet<>(mRegular.keySet());
        allKeys.addAll(mSavedStateProviders.keySet());
        allKeys.addAll(mLiveDatas.keySet());
        return allKeys;
    }

    @MainThread
    @Nullable
    public <T> T get(@NonNull String key) {
        return (T) mRegular.get(key);
    }
The initial approach I took uses .get and set directy, so this is not much of an issue. It just really boils down to how can i test this thing.
i

Ian Lake

01/06/2022, 4:30 PM
With the code I posted, nothing is in the SavedStateHandle until it is saved - you'd access the
var
directly if you want the in process value - that's the source of truth
a

Alex Vanyo

01/06/2022, 9:30 PM
Hmm, it would be kind of nice to trigger a
save
manually for testing purposes, if you wanted to verify that some
Saver
did the right thing (for instance). It looks like that’s almost there with
SavedStateHandle.savedStateProvider()
, but that isn’t public.
I might just be spoiled now by
StateRestorationTester
and expect that everywhere 😄
9 Views