Using AndroidViewModel to Access Application Context and Persist Data with SharedPreferences
This example demonstrates a basic counter application that retains its state across process death by leveraging AndroidViewModel, SavedStateHandle, and SharedPreferences.
AndroidViewModel extends the base ViewModel class and provides access to the application context via getApplication(). This allows safe access to global resources such as strings, shared preferences, or system services without leaking activity contxets.
The implementation uses:
SavedStateHandleto retain UI-related data during configuration changes.LiveDatato observe changes and automatically update the UI.SharedPreferencesfor persistent storage—ensuring data survives app restarts or device reboots.
MyViewModel.java
package com.example.viewmodelshp;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.SavedStateHandle;
public class CounterViewModel extends AndroidViewModel {
private final SavedStateHandle savedState;
private final String dataKey;
private final String prefsName;
public CounterViewModel(@NonNull Application app, SavedStateHandle handle) {
super(app);
this.savedState = handle;
this.dataKey = app.getResources().getString(R.string.data_key);
this.prefsName = app.getResources().getString(R.string.shp_name);
if (!savedState.contains(dataKey)) {
restoreFromDisk();
}
}
public LiveData<Integer> getCurrentValue() {
return savedState.getLiveData(dataKey);
}
private void restoreFromDisk() {
SharedPreferences prefs = getApplication().getSharedPreferences(prefsName, Context.MODE_PRIVATE);
int savedValue = prefs.getInt(dataKey, 0);
savedState.set(dataKey, savedValue);
}
public void persistToDisk() {
Integer current = getCurrentValue().getValue();
if (current == null) current = 0;
SharedPreferences prefs = getApplication().getSharedPreferences(prefsName, Context.MODE_PRIVATE);
prefs.edit().putInt(dataKey, current).apply();
}
public void increment(int delta) {
Integer current = getCurrentValue().getValue();
savedState.set(dataKey, (current == null ? 0 : current) + delta);
}
}
MainActivity.java
package com.example.viewmodelshp;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.SavedStateViewModelFactory;
import androidx.lifecycle.ViewModelProvider;
import android.os.Bundle;
import com.example.viewmodelshp.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
private CounterViewModel viewModel;
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
viewModel = new ViewModelProvider(
this,
new SavedStateViewModelFactory(getApplication(), this)
).get(CounterViewModel.class);
binding.setViewModel(viewModel);
binding.setLifecycleOwner(this);
}
@Override
protected void onPause() {
super.onPause();
viewModel.persistToDisk();
}
}
String Resources (res/values/strings.xml)
<resources>
<string name="app_name">ViewModelSHP</string>
<string name="button_plus">+</string>
<string name="button_minus">-</string>
<string name="data_key">counter_value</string>
<string name="shp_name">app_preferences</string>
</resources>
Data Binding Layout (activity_main.xml)
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example.viewmodelshp.CounterViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{String.valueOf(viewModel.currentValue)}"
android:textSize="30sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.3"
tools:text="0" />
<Button
android:id="@+id/button_minus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.increment(-1)}"
android:text="@string/button_minus"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.76"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.48" />
<Button
android:id="@+id/button_plus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="@{() -> viewModel.increment(1)}"
android:text="@string/button_plus"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.48" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>