3

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.

2 Answers2

3

SavedStateHandle is not very useful. It only works in one specific scenario - when system kills my app in the background.

But what if user kills my app, or clears all apps stack, or restarts their device? Those are equally if not more common scenarios, and usually happen on a daily basis, and in all of such scenarios, SavedStateHandle won't help.

I think official docs doesn't do a good job explaining this, from the comments in this thread and all over stackoverflow it seems most people misunderstood SavedStateHandle. Me too. It should not be used to persist the app data, in fact it just (temporarily) persists session data.

Since most real-world apps usually must cover all aforementioned scenarios by for example persisting data in SharedPreferences or DataStore or Room, I really don't see many reasons to even bother with SavedStateHandle in most cases...Just skip it and use SharedPreferences and you'll save yourself time & keep your code simple.

qkx
  • 2,383
  • 5
  • 28
  • 50
1

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
}
cactustictacs
  • 17,935
  • 2
  • 14
  • 25
  • thanks a lot for your answer. I've tried this code, but it's still not working. When the `activity` restarts, `LiveData` is still null. Probably you could have a look? https://github.com/arieldipietro/PomodoroTechnique – Ariel Di Pietro Apr 25 '22 at 02:28
  • @ArielDiPietro where's the ``TimerViewModel``? – cactustictacs Apr 25 '22 at 17:42
  • sorry I might have made a mistake when pushing the project. Should be working now – Ariel Di Pietro Apr 25 '22 at 20:33
  • 1
    @ArielDiPietro if you mean the two `LiveData`s you're logging in the `init` block of `TimerViewModel`, those look ok to me and there's no reason they should be null (especially the one you're giving a default of `0`). But you know this state is lost when the app is intentionally closed, right? `SavedStateHandle`s are only there to keep your data when the system kills your app in the background, so you can restore that state (like `savedInstanceState` but for view models). If you're killing the activity yourself, you lose that state - if you want to persist it, use `SharedPreferences` or a DB – cactustictacs Apr 25 '22 at 22:14
  • Sorry for my delay. I guess I misundersood the whole concept from the beginning. I moved all the information easily to a DB and that solved everythin! Thanks a lot! – Ariel Di Pietro May 01 '22 at 20:46
  • @ArielDiPietro I did the same thing when I first started using it, but yeah it's a temporary "app running state" kinda thing. Glad you got it working! – cactustictacs May 01 '22 at 21:41
  • `SavedStateHandle` is almost completely pointless. It only works in one specific scenario - when system kills my app in the background. But there are other equally (if not more) common scenarios - for example when a user terminates app manually, or clears all apps stack, or restart the device. And in all of such scenarios, `SavedStateHandle` does not work! Imho it is better to just persist data you wanna keep on your own, just use `SharedPreferences`, `DataStore` or `Room`. And it is guaranteed to work in every scenario. I don't see a reason to bother with `SavedStateHandle` – qkx Feb 15 '23 at 05:45
  • @qkx it's the difference between user-initiated dismissal (i.e. the user explicitly closing the app) vs system-initiated dismissal where the system destroys the app process in the background, but it's still "open" as far as the user's concerned, and they expect to see the same state when they navigate back to it. To maintain the current session's state, you need to use things like `SavedStateHandle` and `onSaveInstanceState` to store info about that state, just in case the app process gets destroyed (and UI widgets handle this themselves) – cactustictacs Feb 15 '23 at 18:56
  • User-initiated dismissal is meant to be treated as a "fresh start" without that old UI state, which is why the *saved state* mechanisms we're talking about (as well as widget state) are cleared when that dismissal happens. It means you don't need to worry about what's been happening to the app in your setup logic - if saved state exists, you use it during setup! If it doesn't, you just start fresh as normal. There are a few scenarios where you'd expect the state to persist - you can read about them here: https://developer.android.com/topic/libraries/architecture/saving-states#expectations – cactustictacs Feb 15 '23 at 18:59
  • How do you make a class `Parcelable`? – IgorGanapolsky May 09 '23 at 14:31
  • 1
    @IgorGanapolsky you need to implement the `Parcelable` interface, the docs tell you what methods you need to write (https://developer.android.com/reference/android/os/Parcelable) and there's lots of guides on the internet walking you through the process. You can try the `kotlin-parcelize` library (https://developer.android.com/kotlin/parcelize) which can automatically generate it, depending on the types of your class members. Anything non-standard and you'll still have to write a custom function to store/restore your state, but the library is a little nicer – cactustictacs May 09 '23 at 15:46