Hello, I've a problem with saved state module. Whe...
# ballast
r
Hello, I've a problem with saved state module. When my app is starting an input is send to the view model. But apparently this is happening before the state is restored. As the result there are two problems: 1.
IllegalStateException: Nothing can be processed until the state has been restored
is thrown 2. The saved state module is not working anymore (doesn't save anything)
I'm using Kotlin/JS so there is a simple way to workaround the problem - I can put my initialization code in
setTimeout({}, 0)
block.
It's enough to run my code after ballast has loaded state.
However, I would prefer the initial inputs be queued/buffered by the view model as the state is restored and then processed.
I've tried to use
onRestoreComplete
callback but I don't have access to the viewmodel from there.
c
When the
IllegalStateException
is thrown, the interceptor gets killed and can’t recover, which is why it’s not saving the state after that point. But setting a delay is not an idea solution for sending the initial input. It’s not necessarily guaranteed to always work, which is why the
onRestoreComplete
callback is the intended way to initialize the VM strictly after the state has been restored. It’s also not meant to be combined with
BootstrapInterceptor
or have an initial Input sent any other way. Why do you need access to the ViewModel in
onRestoreComplete
? Why is it not enough to just send the initialization input from there? You might need to tweak the initialization logic a bit to fit into the SavedState module’s way of working, but I may have missed something that needs to be fixed
And just to add some context to this, on why the
Nothing can be processed until the state has been restored
error is thrown: it’s likely the SavedState restoration will overwrite anything that would have been processed by another Initialize-type Input. So to keep things working as expected and avoid race-conditions (because
adapter.restore()
is suspending and running in parallel to the main VM Input queue), it forcibly checks that nothing is done until the state has been restored
r
The initialization input is set by routing (external in this case, not the one from ballast). The data sent is based on the routing url.
So the state is restored from the storage and then the app must show the right view.
c
So i’m assuming that initial input, coming from the router, contains the data sent through the routing params? And the main thing that’s needed is to save/restore the VM’s state using the SavedState module, but also have the state contain the routing params?
r
Yes, exactly.
c
Got it, thanks. I think the best solution is to pass those routing params into the Adapter, so that they can be provided to the VM through
restore()
or
onRestoreComplete()
. This would require those routing params to be available to the VM’s constructor
Another (maybe slightly hacky) way is to set some flag in the State, or emit an Event from the Input sent by
onRestoreComplete
. And you can use that as a signal for the UI to then send the follow-up Input containing the routing params. This would work without needing to have the params available in the constructor
However, I would prefer the initial inputs be queued/buffered by the view model as the state is restored and then processed.
The
FifoInputStrategy
does allow buffering those Inputs, but the problem with state restoration is that it would require re-ordering Inputs to process everything as expected (where the routing params are sent to the queue but not accepted until the state is restored)
r
It would help if I could somehow get the reference to the vm from the onRestoreComplete callback.
c
Just curious, how would that help?
r
I could just send the event.
In my case I could just initialize my routing from there (the routing just sends inputs so the only thing needed is the vm)
You have implementd the
onRestoreComplete
so it can return an Input. But it would be helpful to be able to return an Event as well.
c
Events can’t be sent from anywhere other than an Input (or a SideJob, which also comes from an Input), even if you had a reference to the VM. So you’d need to send an Input anyway.
r
onRestoreComplete
returning an Input is kind of duplication because the input is just for making a new state and the current state was just created by the restore method.
It would be more logical to return an Event, because the restoration is an event.
c
Right, so in that case, if you can, just set the routing params in
restore()
. And the main reason for sending a follow-up Input isn’t really to set more state, but to execute some logic that needs to be done after the state is set, like starting to observe flows in a sideJob. And I do get the idea behind sending an Event from the
onRestoreComplete
(or Interceptors in general), but the current implementation does not allow for this. And now that you bring it up, and considering that sideJobs can send events directly, it probably would make sense to allow Interceptors to directly send Events. However, the library would need some breaking changes to enable this, which I’m trying to avoid right now until Kotlin 1.8.0 gets released. So I’m mostly just trying to see if there’s a way to accomplish what you need in a way that isn’t terrible with the Ballast APIs that are currently available
So, at this point, even though it’s not the most ideal, I think the cleanest solution is to send an Input from
onRestoreComplete
, that itself send the Event you need. Here’s another solution that may be a bit better and require less boilerplate
Copy code
public object MyScreen {
    public data class State(
        val restoredState: String = "",
        val isRestored: Boolean = false,
    )
}

public class Adapter : SavedStateAdapter<MyScreen.Inputs, MyScreen.Events, MyScreen.State> {
    override suspend fun RestoreStateScope<MyScreen.Inputs, MyScreen.Events, MyScreen.State>.restore(): MyScreen.State {
        return MyScreen.State(
            restoredState = getRestoredState(),
            isRestored = true,
        )
    }
}

public fun CoroutineScope.initialRoutingParams(vm: BallastViewModel<MyScreen.Inputs, MyScreen.Events, MyScreen.State>) {
    launch {
        val restoredState = vm.observeStates().first { it.isRestored }
        vm.send(MyScreen.Inputs.RoutingParams)
    }
}
r
Unfortunately this solution is too slow. There is a noticeable delay between restored state and the state after processing routing.
I'll play a bit more tomorrow integrating Koin - perhaps I'll find some other way to deal with this.
c
You might try changing the dispatchers in the ViewModelConfiguration. If nothing’s suspending in the initialization code, I’m wondering if the delay you’re seeing is because everything runs on
Dispatchers.Default
unless changed (since it’s the only one available in common code). I was hoping to avoid actual/expect in the core module, but I’ve been thinking that it’s probably worth introducing it for picking the most appropriate dispatchers for each platform by default