Working with SharedPreferences

At some point, our app needs to use the values that the user provided via the preference UI. We might also want to save other data in SharedPreferences that is not part of the preference UI.

Reading Preferences

Call getDefaultSharedPreferences() on android.preference.PreferenceManager to get the SharedPreferences that is used by the preference UI for its values.

SharedPreferences offers a series of getters to access named preferences, returning a suitably-typed result (e.g., getBoolean() to return a boolean preference). The getters also take a default value, which is returned if there is no preference set under the specified key.

You can read values at any point. If you want to find out when the preference UI (or other code in your app) changes the preferences, call registerOnSharedPreferenceChangeListener() on the SharedPreferences to register an OnSharedPreferenceChangeListener to be notified of when values change.

The sample app has a ConstraintLayout that shows the values of our three preferences:

<?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="state"
      type="androidx.lifecycle.LiveData&lt;com.commonsware.jetpack.simpleprefs.HomeViewState>" />
  </data>

  <androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".HomeFragment">

    <TextView
      android:id="@+id/checkLabel"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_margin="8dp"
      android:text="@string/labelCheck"
      android:textAppearance="@style/TextAppearance.AppCompat.Large"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />

    <TextView
      android:id="@+id/fieldLabel"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_margin="8dp"
      android:text="@string/fieldLabel"
      android:textAppearance="@style/TextAppearance.AppCompat.Large"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@+id/checkLabel" />

    <TextView
      android:id="@+id/listLabel"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_margin="8dp"
      android:text="@string/listLabel"
      android:textAppearance="@style/TextAppearance.AppCompat.Large"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@+id/fieldLabel" />

    <androidx.constraintlayout.widget.Barrier
      android:id="@+id/barrier"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      app:barrierDirection="end"
      app:constraint_referenced_ids="checkLabel,fieldLabel,listLabel" />

    <TextView
      android:id="@+id/checkValue"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_margin="8dp"
      android:text="@{state.isChecked ? @string/checked : @string/unchecked}"
      android:textAppearance="@style/TextAppearance.AppCompat.Large"
      app:layout_constraintBaseline_toBaselineOf="@id/checkLabel"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toEndOf="@+id/barrier"
      tools:text="checked" />

    <TextView
      android:id="@+id/fieldValue"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_margin="8dp"
      android:text="@{state.fieldValue}"
      android:textAppearance="@style/TextAppearance.AppCompat.Large"
      app:layout_constraintBaseline_toBaselineOf="@id/fieldLabel"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toEndOf="@+id/barrier"
      tools:text="Something" />

    <TextView
      android:id="@+id/listValue"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_margin="8dp"
      android:text="@{state.listValue}"
      android:textAppearance="@style/TextAppearance.AppCompat.Large"
      app:layout_constraintBaseline_toBaselineOf="@id/listLabel"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toEndOf="@+id/barrier"
      tools:text="ABE" />

    <Button
      android:id="@+id/edit"
      android:layout_width="0dp"
      android:layout_height="wrap_content"
      android:layout_marginEnd="8dp"
      android:layout_marginStart="8dp"
      android:layout_marginTop="8dp"
      android:text="@string/editPrefs"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@+id/listValue" />
  </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

We are using data binding to populate three of the TextView widgets with certain values, pulled from a HomeViewState that we get via LiveData. That comes from HomeMotor implementation of AndroidViewModel:

package com.commonsware.jetpack.simpleprefs;

import android.app.Application;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.preference.PreferenceManager;

public class HomeMotor extends AndroidViewModel {
  private final SharedPreferences prefs;
  private final MutableLiveData<HomeViewState> states = new MutableLiveData<>();

  public HomeMotor(@NonNull Application application) {
    super(application);

    prefs = PreferenceManager.getDefaultSharedPreferences(application);
    prefs.registerOnSharedPreferenceChangeListener(LISTENER);
    emitState();
  }

  @Override
  protected void onCleared() {
    prefs.unregisterOnSharedPreferenceChangeListener(LISTENER);
  }

  LiveData<HomeViewState> getStates() {
    return states;
  }

  private void emitState() {
    states.setValue(
      new HomeViewState(prefs.getBoolean("checkbox", false),
        prefs.getString("field", ""), prefs.getString("list", "")));
  }

  private SharedPreferences.OnSharedPreferenceChangeListener LISTENER =
    (prefs, key) -> emitState();
}
package com.commonsware.jetpack.simpleprefs

import android.app.Application
import android.content.SharedPreferences
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.preference.PreferenceManager

class HomeMotor(application: Application) : AndroidViewModel(application) {
  private val listener =
    SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> emitState() }
  private val prefs = PreferenceManager.getDefaultSharedPreferences(application)
  private val _states = MutableLiveData<HomeViewState>()
  val states: LiveData<HomeViewState> = _states

  init {
    prefs.registerOnSharedPreferenceChangeListener(listener)
    emitState()
  }

  override fun onCleared() {
    prefs.unregisterOnSharedPreferenceChangeListener(listener)
  }

  private fun emitState() {
    _states.value = HomeViewState(
      prefs.getBoolean("checkbox", false),
      prefs.getString("field", "") ?: "",
      prefs.getString("list", "") ?: ""
    )
  }
}

When the motor is created, we call PreferenceManager.getDefaultSharedPreferences to retrieve the SharedPreferences object. Then, we call registerOnSharedPreferenceChangeListener() to be notified of changes, then call emitState(). That reads the values of our three preferences (using getBoolean() and getString() methods) and puts them in the HomeViewState for the UI to use. Later on, when the HomeMotor is cleared, we call unregisterOnSharedPreferenceChangeListener(), so we no longer get updates (and so the motor can be garbage-collected).

The net result is that the HomeFragment will show the initial default values, then will reflect the results of any changes that you make:

HomeFragment, Showing Current Preference Values
HomeFragment, Showing Current Preference Values

Modifying Preferences

Call edit() on the SharedPreferences object to get an “editor” for the preferences. This object has a set of setters that mirror the getters on the parent SharedPreferences object. It also has:

  1. remove() to get rid of a single named preference
  2. clear() to get rid of all preferences
  3. apply() or commit() to persist your changes made via the editor

The last one is important — if you modify preferences via the editor and fail to save the changes, those changes will evaporate once the editor goes out of scope. commit() is a blocking call, while apply() works asynchronously. If you are on a background thread already, call commit(); otherwise, call apply().


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.