Chapters

Hide chapters

Android Fundamentals by Tutorials

First Edition · Android 14 · Kotlin 1.9 · Android Studio Hedgehog (2023.1.1)

9. Data Store
Written by Kevin D Moore

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Picture this: You’re browsing recipes and find one you like. You’re in a hurry and want to bookmark it to check it later. Can you build an app that does that? You sure can! Read on to find out how.

In this chapter, your goal is to learn how to use the Data Store library to save important pieces of information to your device.

Saving Data

There are three primary ways to save data to your device:

  1. Write formatted data, like JSON, to a file.
  2. Use a library to write simple data to a shared location.
  3. Use a SQLite database.

Writing data to a file is simple, but it requires you to handle reading and writing data in the correct format and order. For more complex data, you can save the information to a local database.

Why Save Small Bits of Data?

There are many reasons to save small bits of data. For example, you could save the user ID when the user has logged in — or if the user has logged in at all. You could also save the onboarding state or data that the user has bookmarked to consult later.

SharedPreferences

Android has a built-in interface named SharedPreferences that is available in every version of Android. You can retrieve any preference from any Context. You can also retrieve the full interface from an Activity.

How Does It Work?

SharedPreferences uses the system to store data into a file. These are small bits of information like integers, strings or booleans. It has several sets of function calls:

Editor

To make changes, you need to call the edit() method to get an editor. You then have access to the following methods:

// 1
val sharedPreferences = getSharedPreferences("MyPreferences", Context.MODE_PRIVATE)
// 2
if (sharedPreferences.contains("MyKey")) {
  // 3
  val myValue = sharedPreferences.getString("MyKey", "MyDefault")
  Timber.d("My Value is $myValue")
}

DataStore Library

Google has been transitioning to using external libraries instead of just updating the system. This strategy provides them with the flexibility to frequently roll out modifications to the libraries that are immediately accessible to users. Instead of updating the SharedPreferences code in the system, they’ve created a new library called DataStore. This is a modern library for storing key/value pairs. The DataStore library uses the DataStore interface and is designed a bit differently. If you take a look at the definition, it is pretty simple:

interface DataStore<T> {
  val data: Flow<T>
  suspend fun updateData(transform: suspend (t: T) -> T): T
}
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
val prefKey = stringPreferencesKey("MyKey")
context.dataStore.data.first()[prefKey]

Add DataStore Library

If you’re following along with your app from the previous chapters, open it and keep using it with this chapter. If not, just locate the projects folder for this chapter and open starter in Android Studio. Open up gradle/libs.versions.toml. At the end of the versions section add:

prefsVersion = "1.0.0"
# Preferences
prefs = {module = "androidx.datastore:datastore-preferences", version.ref = "prefsVersion" }
implementation(libs.prefs)

Saving UI States

You’ll use DataStore to save a list of saved searches in this section. Later, you’ll also save the tab that the user has selected so the app always opens to last selected tab.

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.first

class Prefs(val context: Context) {
  // TODO: Add dataStore
  // TODO: Add saveString
  // TODO: Add getString
  // TODO: Add saveInt
  // TODO: Add getInit
  // TODO: Add hasKey
}
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "recipes")
// 1
suspend fun saveString(key: String, value: String) {
  // 2
  val prefKey = stringPreferencesKey(key)
  // 3
  context.dataStore.edit { prefs ->
      // 4
      prefs[prefKey] = value
  }
}
suspend fun getString(key: String): String? {
  val prefKey = stringPreferencesKey(key)
  return context.dataStore.data.first()[prefKey]
}
suspend fun saveInt(key: String, value: Int) {
  val prefKey = intPreferencesKey(key)
  context.dataStore.edit { prefs ->
      prefs[prefKey] = value
  }
}
suspend fun getInt(key: String): Int? {
  val prefKey = intPreferencesKey(key)
  return context.dataStore.data.first()[prefKey]
}
suspend fun hasKey(key: String): Boolean {
  val prefKey = stringPreferencesKey(key)
  return context.dataStore.data.first().contains(prefKey)
}
lateinit var prefs: Prefs
prefs = Prefs(this)
val LocalPrefsProvider =
    compositionLocalOf<Prefs> { error("No prefs provided") }
val prefs = remember { Prefs(context) }
LocalPrefsProvider provides (application as RecipeApp).prefs,

Updating ViewModels

Now that you have your Prefs class written, you want to provide it to your view models to be able to save data. Open up RecipeViewModel.kt and at // TODO: Add Prefs, change the constructor to include the prefs. The constructor should look like:

class RecipeViewModel(private val prefs: Prefs) : ViewModel() {
viewModelScope.launch {
  val searchString = _uiState.value.previousSearches.joinToString(",")
  prefs.saveString(PREVIOUS_SEARCH_KEY, searchString)
}
// 1
if (!_uiState.value.previousSearches.contains(searchString)) {
  val updatedSearches = mutableListOf<String>()
  // 2
  updatedSearches.addAll(uiState.value.previousSearches)
  updatedSearches.add(searchString)
  // 3
  _uiState.value = _uiState.value.copy(previousSearches = updatedSearches)
  // 4
  savePreviousSearches()
}
val prefs = LocalPrefsProvider.current
RecipeViewModel(prefs)

Exercise

Convert the remaining methods, just like you just did with RecipeDetails.

viewModelScope.launch {
  val previousSearchString = prefs.getString(PREVIOUS_SEARCH_KEY)
  if (!previousSearchString.isNullOrEmpty()) {
      val storedList = previousSearchString.split(",")
      _uiState.value = _uiState.value.copy(previousSearches = storedList.toMutableList())
  }
}

Saving the Selected Tab

In this section, you’ll use shared preferences to save the current UI tab that the user has navigated to. Open ui/MainScreen.kt. Start by getting an instance of your prefs class. Replace // TODO: Add Prefs with:

val prefs = LocalPrefsProvider.current
val currentIndex = prefs.getInt(CURRENT_INDEX_KEY)
if (currentIndex != null) {
  selectedIndex.intValue = currentIndex
}
scope.launch {
  prefs.saveInt(CURRENT_INDEX_KEY, 0)
}
scope.launch {
  prefs.saveInt(CURRENT_INDEX_KEY, 1)
}

Key Points

Where to Go From Here?

In this chapter, you learned how to persist simple data.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now