androidkotlin

Saving a mutable list in SavedStateHandle and editing its values by index


I'm trying to save a mutable list in SavedStateHandle then be able to edit its values by index

class QuizViewModel(private val savedStateHandle: SavedStateHandle): ViewModel(){
    private var currentIndex
        get() = savedStateHandle.get(CURRENT_INDEX_KEY) ?: 0
        set(value) = savedStateHandle.set(CURRENT_INDEX_KEY,value)

    private var userAnswers : MutableList<Int>
        get(currentIndex){
            savedStateHandle.get<MutableList<Int>>(USER_ANSWERS_INDEX_KEY)?.get(currentIndex) ?: mutableListOf<Int>(0,0,0,0,0)
        }
        set(value) {
            field = value
            savedStateHandle[USER_ANSWERS_INDEX_KEY]
        }

I want to be able to get and set the current user score based on an index. How does one navigate this?


Solution

  • The core problem is that you use a mutable list. That makes it incredibly hard to correctly track changes and update the UI accordingly. Instead, use immutable types like List and place them in a Kotlin Flow. Whenever you want to change anything, create a new list and emit it as a new value in the Flow.

    Fortunately for you, the SavedStateHandle already supports Flows out of the box. You can use getMutableStateFlow() to get a MutableStateFlow where you can use the value property or the update function to change the current value.

    I'm not entirely sure how you want to actually use the list, but this is how the view model could look like for some example use cases:

    class QuizViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
        private val currentIndex: MutableStateFlow<Int> = savedStateHandle
            .getMutableStateFlow(
                key = CURRENT_INDEX_KEY,
                initialValue = 0,
            )
        private val _userAnswers = savedStateHandle
            .getMutableStateFlow(
                key = USER_ANSWERS_INDEX_KEY,
                initialValue = List(5) { 0 },
            )
    
        val userAnswers: StateFlow<List<Int>> = _userAnswers.asStateFlow()
        val currentAnswer: StateFlow<Int> = userAnswers
            .combine(currentIndex) { answers, index ->
                answers[index]
            }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5.seconds),
                initialValue = 0,
            )
    
        fun currentIndex(index: Int) {
            currentIndex.value = index
        }
    
        fun answer(index: Int, answer: Int) {
            _userAnswers.update {
                it.mapIndexed { i, item ->
                    if (i == index) answer else item
                }
            }
        }
    }
    

    As usual, you should only expose StateFlow in the view model. That's why asStateFlow() needs to be applied to the MutableStateFlow, and why stateIn() is needed on the returned Flow from combine().

    Your UI can then observe the view model's flows for changes by collecting them. If you use Compose for your UI you would simply use collectAsStateWithLifecycle() from the Gradle dependency androidx.lifecycle:lifecycle-runtime-compose.

    Use the view model's functions currentIndex and answer to change the SavedStateHandle's values. The Flows - and in consequence also the UI - get automatically updated as well.

    You might want to check the initial values I provided for the (Mutable)StateFlows, I'm not sure that is what you really want.


    As a closing note, I'm not sure that SavedStateHandle is actually what you want to use in the first place. It only protects the data from being lost if the Android system decides to kill the app (due to inactivity or to free memory and so on). If you want the app data to be present after the user closed and reopened the app, then you need to use a persistence framework, like DataStore or a fully fledged database like Room.

    From SavedStateHandle's documentation:

    Saved state is tied to your task stack. If your task stack goes away, your saved state also goes away. This can occur when force stopping an app, removing the app from the recents menu, or rebooting the device. In such cases, the task stack disappears and you can't restore the information in saved state. In User-initiated UI state dismissal scenarios, saved state isn't restored. In system-initiated scenarios, it is.