compose-multiplatform: `rememberSaveable` does not work - restore/save logic is never called
I currently have:
val appState = remember { AppState() }
I thought I would try switching to rememberSaveable
so that app settings would persist across app invocations.
So I changed it to:
val appState = rememberSaveable(saver = AppState.Saver) { AppState() }
AppState.Saver
is currently just:
val Saver = object : Saver<AppState, Any> {
override fun restore(value: Any): AppState? {
TODO("Not yet implemented")
}
override fun SaverScope.save(value: AppState): Any? {
TODO("Not yet implemented")
}
}
If I put a breakpoint on those TODO
lines, I can see that they are never called. I’d expect a call to save during application exit, and a call to restore some time during startup.
I assume there’s a way to call the right things programmatically to get this to happen as a workaround but haven’t figured it out yet.
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Comments: 23 (7 by maintainers)
Let me try to summarize this thread.
Android Jetpack Compose behavior
rememberSaveable
relies (via a few abstraction layers) on system’sSaved instance state
mechanism. From the documentation:So, it’s NOT stored on disk, it does NOT survive when the user closes the application.
Compose Multiplatform behavior
Currently save/restore aren’t invoked out of the box because
SaveableStateRegistry
is not provided by default (yet). We do not plan to provide saving it to disk out of the box since it’s not the initial purpose of this API. Since the lifecycle/scope of stored data is outside of Compose itself and relies on navigation, currently we cannot provide expected/correct implementation out of the box, but it’s on our radar as an integration point with the future navigation solution.For now you can provide a custom implementation of
SaveableStateRegistry
(see below).Providing custom savable logic
If you want to provide custom storing logic, you can do it via providing
LocalSaveableStateRegistry
:Example of simple
SaveableStateRegistry
that stores values as-is inside process memory (might be useful if you recreate Compose during the run and want to restore the state):Based on this you can implement saving the state on the disk if you want to.
I understand, what happens is sometimes
rememberSaveable
is misused / overused, saving state that perhaps don’t belong there. But definitely the data or ui-state that goes into the database shouldn’t know about process death at all. Thanks for the DisposableEffect clarification.No, it’s regular compose local and might be applied/replaced only to part of your application. I didn’t test it personally, but I don’t see any reason why it wouldn’t work
I’m currently working on porting lifecycle, things a bit more complicated there. On Android lifecycle library is independent from Compose, but vice versa - Compose has it in the dependencies to find “lifecycle owner” from View tree and provide as composition local. However, there is no such “lifecycle owner” like activity in multiplatform, and Compose does window management related things itself. So, as the first step Compose will be “lifecycle owner”. With ViewModels and “state registry owner” the situation is similar - there is no API at the moment to hold it outside of Compose. I wrote about it above:
But it’s another topic.
Usually current visual state is not stored in database. I mean for example collapsed spoiler or selected text range.
rememberSaveable
and saving state on config changes in general is about such cases.What happen? The last example is about custom registry with custom logic. It’s up to you when you’re going to save the current state. Maybe you want to save it every minute? The saving just before disposing just allows you to save the state for next run - it’s what @hakanai wanted. My example shows how to achieve that. Anyway, “event” is out of scope of the original issue. Please follow https://github.com/JetBrains/compose-multiplatform/issues/2915 for lifecycle events. But I doubt that it’s directly related.
I thought about a general case with
ComposePanel
when you have direct control of disposing it/Compose.It’s true, the order is opposite and consistent. But you need to save it BEFORE disposing the content that you will initialize later
You just need to make sure you call
DisposableEffect
in the right place:Previously, since I had full control over Compose lifecycle, I thought it would be just easier not to rely on that order. But if it done correctly, it will work with
DisposableEffect
tooBuuuuut, how do you know when you should call it then?
DisposableEffect
is the only obvious API I was able to think of. Is there some other way to find out that the current context is about to be disposed? I certainly have no idea from my own code.When I wrote that code, I think I was assuming that disposals are called in the opposite order from the order they are initialised. Which is usually a fairly sane assumption to make, but I don’t know how Compose has actually implemented it. (Maybe what I should have done was had the
DisposableEffect
also create the thing which actually does the saving? Then I’d have it in scope and would know that it hasn’t been disposed by anyone else.)What I wish Compose had was something like a
rememberDisposable
which would allow me to remember a thing but also have a hook that would be called to dispose it.I did some investigation and looks like
rememberSaveable
actually works, but you need to implement additional thing to make it work, it’s just that android’s navigation library already has it implemented underneath. Also on desktop serialization is not really is necessary since everything is stored in memoryTo make it work you need to create instance of
SaveableStateHolder
withrememberSaveableStateHolder()
and wrap you navigation destinations withstateHolder.SaveableStateProvider(stringKey) { content }
, it will assosiate your content with key and save data for specific destinations, then when removing destination you just need to invokeSaveableStateHolder.removeState(key)
Here is snippet of what I did in my project:
I partially agree, except:
It isn’t clear that Compose is just a “UI framework”. The docs really make it seem like an “app framework” instead - code examples show the
main
function directly calling theirsingleWindowApplication
function, i.e. the very first line of code is Compose code.This means I have to reinvent the wheel even though surely I am not the only person trying to use this framework to make full applications.
So yeah, Compose may be a UI framework and not an app framework, but if that’s the case, things like
rememberSaveable
are confusing at best, and it means we have to wait for someone to make an actual app framework which is built on Compose until we can really develop apps with ease, because “just putting state in some object and serialising it” is not trivial.Initially, from what I gathered reading docs, it seemed like
rememberSaveable
was the way to remember state across runs of an application.But the impression I get now is that perhaps
rememberSaveable
is just a hack to work around Android’s terrible behaviour where it throws your app state away when you rotate the device, and on desktop I appear to be able to resize the window totally fine without this sort of hack, so perhapsrememberSaveable
was never meant to work on desktop?And worse - the impression I get from talking to people in a few places now is that both
remember
andrememberSaveable
should be avoided as much as possible, and we should be initialising all our app state before even calling the first composable function.So instead of doing this:
We should actually be doing this:
Which is fine, but now I’m back to having no convenient way to store my app state at all, and I’m back to reinventing the wheel.