Search code examples
androidkotlinpersistenceviewmodelviewmodel-savedstate

savedStateHandle not saving State


Following this question I made some simple changes in my app, but it's no working as I expect.

I have a Timer that sends a notification when the timer is done. Clicking this notification restarts the activity, deleting all the timer information, which is stored mainly in the viewModel. For this reason, I decided to use saved State for viewModel.

Here's my viewModel:

class TimerViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {

private val _secondsRemaining = savedStateHandle.getLiveData<Long>(SECONDS_REMAINING)
val secondsRemaining : LiveData<Long>
    get() = _secondsRemaining

Here is how I use the viewModel in my Fragment:

private val timerViewModel by viewModels<TimerViewModel>()

When I start the timer, I save the value of the seconds remaining in the LiveData, on every Tick of the clock. When the timer finishes, the app sends the notification and the timer starts again, counting a new cycle:

timer = object : CountDownTimer(timerLengthSeconds * 1000, 1000){
        override fun onFinish(){
            (....)
        }

        override fun onTick(millisUntilFinished: Long) {
            var secondsRemainingInCountdown = millisUntilFinished / 1000

            (...)

            _secondsRemaining.value = secondsRemainingInCountdown
         
        }
    }.start()
}

So, when the timer finishes, the app sends the notification but the timer has restarted, and the seconds remaining are getting updated (I've checked this via Logs). When the user clicks the notification the activity gets killed and restarts, and the expactation would be to see the timer with the seconds remaining saved in the LiveData. But when the activity restarts, LiveData value is null.

I have also tried setting a value of 10, in case LiveData is null when first created

private val _secondsRemaining = savedStateHandle.getLiveData<Long>(SECONDS_REMAINING, 10)

but when the activity restarts, I get 10 as the value of LiveData

I can't figure out the reason.

My second problem is that I want to save the state of a LiveData that stores a custom class, that saves the state of the clock

private val _timerState = MutableLiveData<TimerState>()
val timerState : LiveData<TimerState>
    get() = _timerState

Being this class:

    sealed class TimerState {

    object OnFocusRunning : TimerState()
    object OnRestRunning : TimerState()
    object OnFocusPaused : TimerState()
    object OnRestPaused : TimerState()
    object Completed : TimerState()
    object RestCompleted : TimerState()
    object NotStarted : TimerState()
}

But I haven't succeed in this, since TimerState is a custom class and not a primitive type.


Solution

  • When you're using a SavedStateHandle you need to set your values on it to store them. If you use getLiveData for a particular key, then that LiveData will update whenever you set a new value for that key. If you're setting it directly on the LiveData, you're bypassing the saved state:

    class TimerViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    
        // create a LiveData that monitors this key in the saved state
        private val _secondsRemaining = savedStateHandle.getLiveData<Long>(SECONDS_REMAINING)
        val secondsRemaining : LiveData<Long> get() = _secondsRemaining
    
        // expose a setter that updates the state - this will propagate to the LiveData
        fun setSecondsRemaining(remaining: Long) {
            savedStateHandle[SECONDS_REMAINING] = remaining
        }
    
    }
    

    As for your other problem, yeah you're limited in what you can store, and for custom classes you either need to serialise them into a form you can store, or make them Serializable or Parcelable.

    In your case though, since that sealed class isn't doing anything special except being an instance of a type, I'd just make it an enum class instead - those are Serializable so you can throw the value straight in there!

    enum class TimerState {
        OnFocusRunning, OnRestRunning // etc
    }