20

I'm trying to add a mutable list of parcelable objects to my composable. I also want to be able to add objects to and remove objects from it.

Currently I'm using something like this:

val names = remember { mutableStateListOf<String>() }

names.add("Bill")
names.remove("Bill")

Now I want this list to survive configuration change, therefore it's perhaps a good idea to use rememberSaveable. Perhaps something like this:

val names = rememberSaveable { mutableStateListOf<String>() }

names.add("Bill")
names.remove("Bill")

But this does not work, it throws the following exception:

androidx.compose.runtime.snapshots.SnapshotStateList cannot be saved using the current SaveableStateRegistry. The default implementation only supports types which can be stored inside the Bundle. Please consider implementing a custom Saver for this class and pass it to rememberSaveable().

This means, that SnapshotStateList (the result of mutableStateListOf) is not saveable.

So far I can think of few ways how to work around this:

  1. Actually implementing a saver for SnapshotStateList.
  2. Using something like val namesState = rememberSaveable { mutableStateOf(listOf<String>()) }. This does indeed work flawlessly, however updating the list requires setting the value, which is both slow and inconvenient (e.g. namesState.value = namesState.value + "Joe" for just adding a single element).

Both these ways seem too complicated for a seemingly small task. I wonder what is the best way to do what I want. Thanks.

Halil Ozel
  • 2,482
  • 3
  • 17
  • 32
waleko
  • 302
  • 1
  • 2
  • 9

4 Answers4

25

There should be no data inside remember or rememberSaveable that you are afraid of losing: it's gonna be destroyed as soon as your Composable disappears (goes out of composition, to be precise). Consider using view models in this case.


If you are still interested in storing mutableStateListOf inside rememberSaveable, I suggest that you follow your error recommendation and create a Saver for SnapshotStateList:

@Composable
fun <T: Any> rememberMutableStateListOf(vararg elements: T): SnapshotStateList<T> {
    return rememberSaveable(
        saver = listSaver(
            save = { stateList ->
                if (stateList.isNotEmpty()) {
                    val first = stateList.first()
                    if (!canBeSaved(first)) {
                        throw IllegalStateException("${first::class} cannot be saved. By default only types which can be stored in the Bundle class can be saved.")
                    }
                }
                stateList.toList()
            },
            restore = { it.toMutableStateList() }
        )
    ) {
        elements.toList().toMutableStateList()
    }
}

Then, you can use it like this:

val names = rememberMutableStateListOf<String>()
LaunchedEffect(Unit) {
    names.add("Bill")
}
Text(names.joinToString { it })

The expected behaviour for this sample: each time you rotate your device - one more element gets added.

Don't use any state modifications inside the composable like you did when you added and removed an item. You should only do that inside side-effects, like I did here with LaunchedEffect, or callbacks, like onClick.

Note that saveableMutableStateListOf is still limited to Bundle-saveable types, like String, Int, etc. If you need to store a custom type inside, you will need to modify Saver to save/recreate it too.

Richard Onslow Roper
  • 5,477
  • 2
  • 11
  • 42
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • 1
    Thanks Philip, this works and does solve my problem. Perhaps this is the most elegant we can do at this point... I also added `Parcelable` class to `T`, so that items could always be saved. Is this really the way it's intended to be done? I think having a saveable mutable array is not something far-fetched and should be easily available out of the box. – waleko Aug 23 '21 at 07:15
  • 1
    Thank you. I tried it with view models, and they fit really well here. – waleko Aug 23 '21 at 08:09
  • You are brilliant, phil – ino Sep 03 '22 at 15:10
  • How can I use like `by rememberMutableStateListOf()`? I think I need to implement getValue. But I have no idea. – c-an Mar 08 '23 at 09:06
  • @c-an no, you can't do it. `SnapshotStateList` is not subclass of `State`, instead it implements `MutableList` interface, so you can modify it same as you'd do with any other mutable list, and you don't need `by` delegation here – Phil Dukhov Mar 09 '23 at 00:03
1

I came up with something like this

val selectedLists: MutableList<String?> = rememberSaveable(
    saver = listSaver<MutableList<String?>, String?>(
        save = {
            if (it.isNotEmpty()) {
                it.toList()
            } else {
                listOf<String?>(null)
            }
        },
        restore = {
            it.toMutableStateList()
        }
    )
) {
    mutableStateListOf(*viewState.selectedLists.map { it.name }.toTypedArray())
}
SMH
  • 33
  • 4
0

The kind of data you are storing must be saveable in a Bundle, or else, you must pass in a custom-saver object. Just look at the docs for state and maybe specifically for rememberSaveable

Richard Onslow Roper
  • 5,477
  • 2
  • 11
  • 42
-2

I found the following solution for myself:

  1. I create a saver function in which I convert a list of strings into a string separated by '\n' and vice versa:

     fun saverListOfString(): Saver <MutableList<String>, String> = Saver(
     save = {
         var saved = ""
         it.forEachIndexed { index, s ->
             if (index > 0) saved += "\n"
             saved += s
         }
         saved
     },
     restore = { restored ->
         val restoredList = restored.lines()
         val list = mutableStateListOf<String>()
         restoredList.forEach { list.add(it) }
         list
     }
    

    )

  2. I save the list of strings in Saveable like this:

val stringList = rememberSaveable(saver = saverListOfString()) { mutableStateListOf() }
moken
  • 3,227
  • 8
  • 13
  • 23
  • 1
    This is very un-kotlin code (is that a word?) and doesn't make much sense. There are easier ways to serialize a list, and using \n as a seperator makes no sense if the strings themselves contain that – Zun May 04 '23 at 08:56
  • 1
    Either use a ViewModel (your UI layer should only display UI, it shouldn't be responsible for saving state) or implement the saver as such: https://stackoverflow.com/a/68887484/3999808 – Zun May 04 '23 at 08:58