Saving Simple Data in Android - SharedPreferences and DataStore APIs
Tags: android, tutorial, learning, androiddevelopment
Published: 2023-11-24 16:33:43

---
title: Saving Simple Data in Android - SharedPreferences and DataStore APIs
published: true
date: 2023-11-24 16:22:38 UTC
tags: android, tutorial, learning, androiddevelopment
canonical_url: https://blog.nicholasfragiskatos.dev/saving-simple-data-in-android
---

When it comes to data persistence and storing structured data on an Android device, the first solution that comes to mind might involve using a database, like SQLite, paired with an ORM like Room. However, if the goal is to store some configuration flags, user preferences, settings, or some other relatively small handful of simple data, then reaching for a database might be unnecessary.

Fortunately, the Android framework provides a few different storage solution APIs just for this use case: _SharedPreferences_ , _Preferences DataStore_, and _Proto DataStore_. In this article, we will look at what they are, how they differ, and how to work with them.

I created an example app to demonstrate the different solutions. I will provide small code snippets where needed, but the full sample project can be found [here on GitHub](https://github.com/nfragiskatos/SimpleDataStorageInAndroid).

## Project Setup

The project was created using Android Studio's "_Empty Views Activity_" template. Then I created three new Activities, one for each storage implementation:

- `SharedPreferencesActivity`

- `PrefsDataStoreActivity`

- `ProtoDataStoreActivity`

Each activity contains the same form inputs and a button that when clicked will persist three string values and an integer value. The difference between each activity is which API is used.

![View of the three screens in the sample app, each dealing with one of the API storage solutions](https://cdn.hashnode.com/res/hashnode/image/upload/v1700797043199/ac001a5c-cd14-4365-a9fc-b36197c27cab.png)

## Saved Data File Locations

Each API creates its own unique file in app-specific storage to save all the data. Being in app-specific storage means the data will persist until the user either clears the app data, or uninstalls the app.

Data stored using the SharedPreferences API is located in

```bash
/data/data/<APP_NAME>/shared_prefs/<SHARED_PREF_FILE>
```

Data stored using the DataStore API (both Preference and Proto) is located in

```bash
/data/data/<APP_NAME>/files/datastore/<DATASTORE_FILE>
```

To view these files outside of the app, we can use the _Device Explorer_ tool in Android Studio.

> View -> Tool Windows -> Device Explorer

## SharedPreferences API

The SharedPreferences API is the original solution, and also the simplest. It stores a collection of key-value pairs where the values are simple data types (String, Float, Int, Long, Boolean) and are stored in an XML file. For example:

```bash
/data/data/com.nicholasfragiskatos.preferancestorageexample/shared_prefs/my_prefs.xml
```

The shared preference file is created using a file name, so any number of preference files can exist at a time for the app as long as a unique name is used. We can have one file for the whole application, one for an Activity, or some combination depending on the business logic and data organization strategies.

### Creating and Retrieving a Shared Preference File

Creating or retrieving a shared preference file requires using `Context.getSharedPreferences(String name, int mode)`. Typically this will be through an Activity since an Activity is a Context.

```kotlin
class SharedPreferencesActivity : AppCompatActivity() {

    // ....

    // References My_Shared_Prefs.xml
    val mySharedPrefs = getSharedPreferences("My_Shared_Prefs", Context.MODE_PRIVATE)

    // ...
}
```

> 💡
> There are four different modes: MODE\_PRIVATE, MODE\_WORLD\_READABLE, MODE\_WORLD\_WRITEABLE, and MODE\_MULTI\_PROCESS. However, since this is an old API, the _only mode that is not deprecated_ is MODE\_PRIVATE.

The `Activity` class has a wrapper function named `getPreferences(int mode)`. Although, all that does is invoke `getSharedPreferences(...)` using the class name of the current Activity as the name of the file. For example:

```kotlin
class SharedPreferencesActivity : AppCompatActivity() {

    // ....

    // References SharedPreferencesActivity.xml
    val mySharedPrefs = getPreferences(Context.MODE_PRIVATE)

    // ...
}
```

> 💡
> If the preference file does not already exist, then it will only be created when a value is written. Just calling `getSharedPreferences(...)` will not create the file.

### Writing Data

Writing to the file requires invoking `SharedPreferences.edit()` to obtain an `Editor` that helps facilitate the file IO. Then, similar to how we work with a `Bundle`, we use `putInt(...)`, `putString(...)`, etc., functions provided by the `Editor` to modify the preferences. Each function requires a key and value parameters, where the key is a `String`. Lastly, to actually write the changes to disk, we either invoke `commit()`, or `apply()`.

```kotlin
val demographicsPrefs = getSharedPreferences(DEMOGRAPHICS_FILE_KEY, Context.MODE_PRIVATE)

demographicsPrefs.edit().apply {  
    putString(FIRST_NAME_PREF_KEY, "Leslie")  
    putString(LAST_NAME_PREF_KEY, "Knope")  
    apply()  
}
```

Example XML file:

```xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>  
<map>  
    <string name="com.nicholasfragiskatos.preferancestorageexample.FIRST_NAME_PREF_KEY">Leslie</string>  
    <string name="com.nicholasfragiskatos.preferancestorageexample.LAST_NAME_PREF_KEY">Knope</string>  
</map>
```

### `apply()` vs `commit()`

`commit()` writes the data synchronously, and it returns a `boolean` on whether or not the new values were successfully written.

`apply()` commits its changes first to the in-memory `SharedPreferences` immediately, and also starts an _asynchronous_ commit to the disk. However, it does not return any value to signal success or failure.

Unless you need the return value, it's advised to just use `apply()`.

### Reading Data

`SharedPreferences` provides `getInt(...)`, `getString(...)`, etc., functions to read data from the preference file. These functions require the same key parameter used to write the value and a default value parameter in case the value doesn't exist.

```kotlin
val demographicsPrefs = getSharedPreferences(DEMOGRAPHICS_FILE_KEY, Context.MODE_PRIVATE)

val firstName = demographicsPrefs.getString(FIRST_NAME_PREF_KEY, "")  
val lastName = demographicsPrefs.getString(LAST_NAME_PREF_KEY, "")
```

## DataStore

DataStore is the new API that is meant to replace SharedPreferences. Again, the values are simple data types (String, Float, Int, Long, Boolean). However, unlike SharedPreferences, DataStore uses Kotlin coroutines and Flows to store data asynchronously and guarantee data consistency across multi-process access.

There are two flavors of DataStore provided to us, _Preferences DataStore_ and _Proto DataStore_. While both are an improvement over regular SharedPreferences, each requires a bit more complexity, with Proto DataStore being the most complex by including Protocol Buffers, as we'll see later.

> 💡
> Creating more than one `DataStore` object that references the same file _in the same process_ will break functionality and result in an `IllegalStateException` error when reading/writing data.

### Viewing DataStore Files

The DataStore implementation used will determine what type of file is created on the device. If the preferences approach is used, then a `.preferences_pb` file will be created. If the proto approach is used then a `.proto` file will be created. For example:

```bash
/data/data/com.nicholasfragiskatos.preferancestorageexample/files/datastore/MyPrefsDataStore.preferences_pb

/data/data/com.nicholasfragiskatos.preferancestorageexample/files/datastore/MyProtoSchema.proto
```

Unfortunately, both file types need to be decoded first before they can be viewed outside of the app. Google provides a command line utility called [protoc](https://google.github.io/proto-lens/installing-protoc.html) for Mac and Linux. `protoc` can be used as follows.

```bash
protoc --decode_raw < MyProtoSchema.proto
```

### Preferences DataStore

To get started with Preference DataStore implementation, we first need to add a dependency to the Gradle file.

```bash
implementation "androidx.datastore:datastore-preferences:1.0.0"
```

Similar to SharedPreferences, we need to create a handle to the file we want to read/write from. This time though it's a `DataStore` object that will manage the transactions. To obtain it we use the `preferencesDataStore(...)` property delegate on an extension property of `Context`.

```kotlin
// make sure to use androidx.datastore.preferences.core.Preferences
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "MyPrefsDataStore")
```

#### Defining Keys for Values

Like SharedPreferences, we access DataStore preferences with a key, but instead of a simple `String` key, we need to create a `Preferences.Key<T>` for each value. This allows the key-value pair to be typed. These keys are created using `stringPreferencesKey(...)`, `intPreferencesKey(...)`, etc.

```kotlin
val FIRST_NAME_PREF_KEY = stringPreferencesKey("${BuildConfig.APPLICATION_ID}.FIRST_NAME_PREF_KEY")  
val LAST_NAME_PREF_KEY = stringPreferencesKey("${BuildConfig.APPLICATION_ID}.LAST_NAME_PREF_KEY")  
val FAVORITE_COLOR_PREF_KEY = stringPreferencesKey("${BuildConfig.APPLICATION_ID}.FAVORITE_COLOR_PREF_KEY")  
val FAVORITE_ICE_CREAM_PREF_KEY = intPreferencesKey("${BuildConfig.APPLICATION_ID}.FAVORITE_ICE_CREAM_PREF_KEY")
```

#### Writing Data

Writing to the file requires invoking the `DataStore.edit(...)` extension function that takes a suspending lambda function parameter, `suspend (MutablePreferences) -> Unit`.

The `MutablePreferences` object implements the index get/set operator functions, so we can read/write values with our defined preference keys using the same syntax as we would for a `Map`.

```kotlin
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "MyPrefsDataStore")

class PrefsDataStoreActivity : AppCompatActivity() {

    // ...

    private suspend fun savePreferences() {
        val firstName = "Ron"
        val lastName = "Swanson"
        val favoriteColor = "Blue"  
        val iceCreamId = 2131231085 // some id for radio button

        applicationContext.dataStore.edit { settings: MutablePreferences ->  
            settings[FIRST_NAME_PREF_KEY] = firstName  
            settings[LAST_NAME_PREF_KEY] = lastName  
            settings[FAVORITE_COLOR_PREF_KEY] = favoriteColor  
            settings[FAVORITE_ICE_CREAM_PREF_KEY] = iceCreamId  
        }
    }

    // ...
}
```

_MyPrefsDataStore.preferences\_pb_ content:

```bash
protoc --decode_raw < MyPrefsDataStore.preferences_pb

1 {
  1: "com.nicholasfragiskatos.preferancestorageexample.FIRST_NAME_PREF_KEY"
  2 {
    5: "Ron"
  }
}
1 {
  1: "com.nicholasfragiskatos.preferancestorageexample.LAST_NAME_PREF_KEY"
  2 {
    5: "Swanson"
  }
}
1 {
  1: "com.nicholasfragiskatos.preferancestorageexample.FAVORITE_COLOR_PREF_KEY"
  2 {
    5: "Brown"
  }
}
1 {
  1: "com.nicholasfragiskatos.preferancestorageexample.FAVORITE_ICE_CREAM_PREF_KEY"
  2 {
    3: 2131231085
  }
}
```

#### Reading Data

Reading from the file is done by collecting on the `DataStore.data` property, which is a `Flow`.

```kotlin
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "MyPrefsDataStore")

class PrefsDataStoreActivity : AppCompatActivity() {
    private suspend fun initFromPreferences() {  
        applicationContext.dataStore.data.collect { settings: Preferences ->  

            val firstName = settings[FIRST_NAME_PREF_KEY] ?: ""  
            val lastName = settings[LAST_NAME_PREF_KEY] ?: ""  
            val favoriteColor = settings[FAVORITE_COLOR_PREF_KEY] ?: ""  
            val favoriteIceCreamId = settings[FAVORITE_ICE_CREAM_PREF_KEY] ?: R.id.rbChocolate  
        }  
    }
}
```

### Proto DataStore

Using the Proto DataStore implementation requires significantly more setup than the previous two methods. However, despite the overhead, this approach does ensure type safety, and read and writes are defined by updating a custom, generated class object with named properties instead of relying on keys.

To begin, we need the following changes in the Gradle file:

- Add Protobuf plugin

- Add Proto DataStore, and ProtoBuf dependencies

- Create a custom Protobuf configuration

```bash
plugins {  
    // ...
    id "com.google.protobuf" version "0.9.1"  
}

// ...

dependencies {  
    // ...

    implementation "androidx.datastore:datastore:1.0.0"  
    implementation "com.google.protobuf:protobuf-javalite:3.25.0"  

}

protobuf {  
    protoc {  
        artifact = "com.google.protobuf:protoc:3.21.7"  
    }  

    generateProtoTasks {  
        all().each { task ->  
            task.builtins {  
                java {  
                    option 'lite'  
                }  
            }  
        }  
    }  
}
```

Next, we need to create a schema in the `app/src/main/proto` directory of the project. It's a simple text file with the extension `.proto`. Here's a basic example for the sample application, but the [Protocol Buffer Documentation](https://protobuf.dev/overview/) provides more details.

```javascript
syntax = "proto3";  

option java_package = "com.nicholasfragiskatos.preferancestorageexample";  
option java_multiple_files = true;  

message MySettings {  
    string first_name = 1;  
    string last_name = 2;  
    string favorite_color = 3;  
    int32 favorite_ice_cream_flavor = 4;  
}
```

With the schema defined, a `MySettings` class will be generated for us. The generated class implementation is extensive, but one convenient thing to point out is that it has a corresponding property for each property defined in the schema. For example, the schema defines a `first_name` property, so `MySettings` will have a `firstName` property.

The next part of the setup is to create a custom `Serializer` that tells `DataStore` how to read and write our custom data type (`MySettings`) as defined by the schema. This is largely boilerplate code.

```kotlin
object MySettingsSerializer : Serializer<MySettings> {  

    override val defaultValue: MySettings = MySettings.getDefaultInstance()  
    override suspend fun readFrom(input: InputStream): MySettings {  
        try {  
            return MySettings.parseFrom(input)  
        } catch (exception: InvalidProtocolBufferException) {  
            throw CorruptionException("cannot read proto.", exception)  
        }  
    }  

    override suspend fun writeTo(t: MySettings, output: OutputStream) {  
        t.writeTo(output)  
    }  
}
```

Now, when we read from the file `DataStore` provides a `MySettings` object, and when we write to the file we give `DataStore` a `MySettings` object.

Lastly, similar to preferences DataStore, we need to create a `DataStore` object that will manage the transactions. This time we use the `dataStore(...)` property delegate on an extension property of `Context`.

```kotlin
val Context.myProtoDataStore: DataStore<MySettings> by dataStore("MyProtoSchema.proto", serializer = MySettingsSerializer)
```

#### Writing Data

Writing to the file requires invoking the `DataStore.updateData(...)` function that takes a suspending lambda function parameter, `suspend (MySettings) -> MySettings`. We use the builder pattern to create and return an updated `MySettings` object to save.

```kotlin
val Context.myProtoDataStore: DataStore<MySettings> by dataStore("MyProtoSchema.proto", serializer = MySettingsSerializer)  

class ProtoDataStoreActivity : AppCompatActivity() {  

private lateinit var binding: ActivityProtoDataStoreBinding  

    // ...

    private suspend fun savePreferences() {  
        val firstName = "Ben"
        val lastName = "Wyatt"
        val favoriteColor = "Green"
        val iceCreamId = 2131231084 // some id for radio button

        applicationContext.myProtoDataStore.updateData { settings ->  
            settings.toBuilder()  
            .setFirstName(firstName)  
            .setLastName(lastName)  
            .setFavoriteColor(favoriteColor)  
            .setFavoriteIceCreamFlavor(iceCreamId)  
            .build()  
        }  
    }  
}
```

_MyProtoSchema.proto_ content:

```bash
protoc --decode_raw < MyProtoSchema.proto
1: "Ben"
2: "Wyatt"
3: "Green"
4: 2131231084
```

#### Reading Data

Like Preferences DataStore, reading from the file is done by collecting on the `DataStore.data` property, which is a `Flow`.

```kotlin
val Context.myProtoDataStore: DataStore<MySettings> by dataStore("MyProtoSchema.proto", serializer = MySettingsSerializer)  

class ProtoDataStoreActivity : AppCompatActivity() {  

private lateinit var binding: ActivityProtoDataStoreBinding  

    // ...

    private suspend fun initFromPreferences() {  
        applicationContext.myProtoDataStore.data.collect { settings ->  
            binding.etFirstName.setText(settings.firstName)  
            binding.etLastName.setText(settings.lastName)  
            binding.etFavoriteColor.setText(settings.favoriteColor)  
            binding.rgIceCreamFlavor.check(settings.favoriteIceCreamFlavor)  
        }  
    }  

    // ...
}
```

## Conclusion

The Android framework provides the developer with _SharedPreferences_ , _Preferences DataStore_, and _Proto DataStore_ APIs to persist sets of structured data without having to rely on a database. All three APIs write and read simple values (String, Float, Int, Long, Boolean) to a file in app-specific storage.

SharedPreferences is the original solution and is built into the `Context` interface with the `getSharedPreferences(...)` function. We write and read values to an XML file using `String` keys. While simple and convenient, it does rely on managing unique keys for all values. Also, it could perform synchronous file I/O on the main UI thread if using `commit()`, and even if `apply()` is used instead of `commit()` then there is no mechanism for signaling success or failure.

The Preferences DataStore API improves upon SharedPreferences by taking advantage of Kotlin coroutines and Flows to store data asynchronously and guarantee data consistency across multi-processor access. While this solution also relies on keys to write data, this time they are not `String` values but instead `Preferences.Key<T>` objects that allow the key-value pair to be typed.

Lastly, we learned about Proto DataStore. Like Preferences DataStore, it provides the benefits of asynchronous writes using coroutines and Flows. Another major benefit is that instead of managing key-value pairs, we get a custom, schema-backed, generated class with regular named properties. Proto DataStore provides this custom object when reading from storage, and we provide the same object when writing back to storage. However, these benefits come at the cost of much greater complexity in setup and configuration.

* * *

Thank you for taking the time to read my article. I hope it was helpful.

If you noticed anything in the article that is incorrect or isn't clear, please let me know. I always appreciate the feedback.

* * *