A State-Aware ViewModel
Version 2.2.0
of the Jetpack lifecycle artifacts added first-class support for tying ViewModel
and your saved instance state together. The InstanceState
sample module in the Sampler
and SamplerJ
projects use an EventViewModel
that still holds our events and start time. However, in this case, we also use the new ViewModel
capabilities to store that data in the saved instance state Bundle
and use that data when creating a new EventViewModel
instance in a new process.
The SavedStateHandle
The first step for making this work is to add a SavedStateHandle
constructor parameter to our ViewModel
. SavedStateHandle
is a wrapper around the saved instance state Bundle
that the Jetpack developers introduced.
So, EventViewModel
has that constructor parameter:
package com.commonsware.jetpack.samplerj.state;
import android.os.SystemClock;
import java.util.ArrayList;
import java.util.Random;
import androidx.lifecycle.SavedStateHandle;
import androidx.lifecycle.ViewModel;
public class EventViewModel extends ViewModel {
private static final String STATE_EVENTS = "events";
private static final String STATE_START_TIME = "startTime";
final ArrayList<Event> events;
final Long startTime;
private final int id = new Random().nextInt();
private final SavedStateHandle state;
public EventViewModel(SavedStateHandle state) {
this.state = state;
ArrayList<Event> events = state.get(STATE_EVENTS);
if (events == null) {
this.events = new ArrayList<>();
}
else {
this.events = events;
}
Long startTime = state.get(STATE_START_TIME);
if (startTime == null) {
this.startTime = SystemClock.elapsedRealtime();
state.set(STATE_START_TIME, this.startTime);
}
else {
this.startTime = startTime;
}
}
void addEvent(String message, int activityHash) {
events.add(new Event(message, activityHash, id));
state.set(STATE_EVENTS, events);
}
@Override
protected void onCleared() {
events.clear();
}
}
package com.commonsware.jetpack.sampler.state
import android.os.SystemClock
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import java.util.*
private const val STATE_EVENTS = "events"
private const val STATE_START_TIME = "startTime"
class EventViewModel(private val state: SavedStateHandle) : ViewModel() {
val events: ArrayList<Event> = state.get(STATE_EVENTS) ?: arrayListOf()
val startTime: Long = state.get(STATE_START_TIME)
?: SystemClock.elapsedRealtime().also { state.set(STATE_START_TIME, it) }
private val id = Random().nextInt()
fun addEvent(message: String, activityId: Int) {
events.add(Event(message, activityId, id))
state.set(STATE_EVENTS, events)
}
override fun onCleared() {
events.clear()
}
}
SavedStateHandle
uses basic get()
and set()
methods for manipulated the saved instance state. Both operate using strings as keys, as with a Bundle
. So, in EventViewModel
, we initialize our events
and startTime
properties to be based on the supplied SavedStateHandle
. But, if our handle does not contain that data, we initialize it to be an empty list of events and the current time, respectively.
Whenever we change these properties, we also update the SavedStateHandle
. Since the value of startTime
is only defined once, if we need to use the current time for its value, we also use set()
to save that in the handle. And, in the addEvent()
function, we not only update the contents of the events
ArrayList
, but we make sure that the SavedStateHandle
has the latest copy of those events.
Note that id
is not part of the saved instance state. That value will get regenerated for each new EventViewModel
.
The Results
We do not need to do anything special when retrieving our viewmodels — the Jetpack viewmodel system already knows how to handle SavedStateHandle
scenarios by default. As a result, our activities look and work much the same as the earlier LifecycleList
counterparts:
package com.commonsware.jetpack.samplerj.state;
import android.os.Bundle;
import com.commonsware.jetpack.samplerj.state.databinding.ActivityMainBinding;
import java.util.ArrayList;
import java.util.Random;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
public class MainActivity extends AppCompatActivity {
private EventAdapter adapter;
private EventViewModel vm;
private final int id = new Random().nextInt();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final ActivityMainBinding binding =
ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
vm = new ViewModelProvider(this).get(EventViewModel.class);
adapter = new EventAdapter(getLayoutInflater(), vm.startTime);
addEvent("onCreate()");
binding.items.setLayoutManager(new LinearLayoutManager(this));
binding.items.addItemDecoration(
new DividerItemDecoration(this, DividerItemDecoration.VERTICAL));
binding.items.setAdapter(adapter);
}
@Override
protected void onStart() {
super.onStart();
addEvent("onStart()");
}
@Override
protected void onResume() {
super.onResume();
addEvent("onResume()");
}
@Override
protected void onPause() {
addEvent("onPause()");
super.onPause();
}
@Override
protected void onStop() {
addEvent("onStop()");
super.onStop();
}
@Override
protected void onDestroy() {
addEvent("onDestroy()");
super.onDestroy();
}
private void addEvent(String message) {
vm.addEvent(message, id);
adapter.submitList(new ArrayList<>(vm.events));
}
}
package com.commonsware.jetpack.sampler.state
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.commonsware.jetpack.sampler.state.databinding.ActivityMainBinding
import java.util.*
import kotlin.collections.ArrayList
class MainActivity : AppCompatActivity() {
private val vm: EventViewModel by viewModels()
private lateinit var adapter: EventAdapter
private val id = Random().nextInt()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
adapter = EventAdapter(layoutInflater, vm.startTime)
addEvent("onCreate()")
binding.items.layoutManager = LinearLayoutManager(this)
binding.items.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
binding.items.adapter = adapter
}
override fun onStart() {
super.onStart()
addEvent("onStart()")
}
override fun onResume() {
super.onResume()
addEvent("onResume()")
}
override fun onPause() {
addEvent("onPause()")
super.onPause()
}
override fun onStop() {
addEvent("onStop()")
super.onStop()
}
override fun onDestroy() {
addEvent("onDestroy()")
super.onDestroy()
}
private fun addEvent(message: String) {
vm.addEvent(message, id)
adapter.submitList(ArrayList(vm.events))
}
}
Seeing the effect of our saved instance state support is tricky. We need to have our process be terminated while leaving the task alone. That means we cannot swipe our task off of the overview screen, as that terminates the task. Similarly, the stop-process buttons in Android Studio toolbars (with a red square icon) seem to terminate the task as well.
One way to see this work is to use the command line.
The Android SDK ships with an adb
command-line utility. We can use this to communicate with a running emulator or debuggable device. You will find adb
in the platform-tools/
directory of your Android SDK installation.
In particular:
-
adb shell
says “give me access to a Linux-style shell inside of Android” -
adb shell am
says “run theam
command in that shell”, wheream
is the “activity manager” tool -
adb shell am kill ...
says “kill the process identified by the supplied application ID” (shown here as...
)
To experiment with instance states:
- Run the sample app
- Undergo configuration changes, if desired
- Press HOME, to move the app to the background and trigger an
onSaveInstanceState()
call - Run
adb shell am kill com.commonsware.jetpack.sampler.state
(for the Kotlin edition) oradb shell am kill com.commonsware.jetpack.samplerj.state
(for the Java edition) to terminate the background process - Bring up the overview screen and tap on the entry for this sample app
In the original LifecycleList
sample, configuration changes would show new instance ID codes for the activity, but the EventViewModel
instance ID code would be the same, as we reuse the existing viewmodel on a configuration change. Now, though, we are terminating the process, so we should see a new viewmodel ID code for the newer events:
However, we still have the original events and the original start time — we did not start over with an empty EventViewModel
, because we populated it from the saved instance state Bundle
.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.