Migrating SharedPreferences to DataStore

Migrating SharedPreferences to DataStore

DataStore, SharedPreferences, Android

DataStore is a new and improved data storage solution that is aimed at replacing SharedPreferences. It provides two implementations: Preference DataStore which stores key-value pairs, and Proto DataStore which stores typed objects using protocol buffers, hence providing type safety. Data is stored asynchronously, consistently, and transactionally, overcoming some of the drawbacks of SharedPreferences.

Android Docs: DataStore

Android provides SharedPreferencesMigrations class for migrating from SharedPreferences to DataStore.

Migrating to Preferences DataStore.

This is the implementation of SharedPreferences.

val SETTINGS_NAME = "settings"
val prefs = getSharedPreferences(SETTINGS_NAME, MODE_PRIVATE)

For you to use Preference DataStore, add Preferences Datastore dependencies in build.gradle(app) file.

implementation("androidx.datastore:datastore-preferences:1.0.0")

To migrate to Preference Datastore, pass in SharedPreferencesMigration to the list of migrations.
SharedPreferencesMigration takes context and the name of SharedPreferences to migrate as parameters. DataStore will automatically migrate, from SharedPreferences to DataStore, for us.

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = SETTINGS_NAME,
    produceMigrations = { context ->
        listOf(SharedPreferencesMigration(context, SETTINGS_NAME))
    })

Migrations are run before any data access can occur in DataStore. This means that your migration must have succeeded before DataStore.data emits any values and before DataStore.edit() can update data.

Migrating to Proto DataStore.

Let's take user profile, e.i. their name, nickname, email etc. stored in SharedPreferences. The implementation would look like this.

val prefs = getSharedPreferences(SETTINGS_NAME, MODE_PRIVATE)
with(prefs.edit()) {
            putString(NAME_KEY, "Kinya")
            putString(EMAIL_KEY, "kinya@example.com")
            putString(NICKNAME_KEY, "B__Kinya")
            apply()
        }

We could unify this under UserProfile object. Proto Datastore allows storage of typed objects using protocol buffers.

To work with Proto DataStore and get Protobuf to generate code for our schema, let's make the following changes to the build.gradle(app) file.

Add the Protobuf plugin
plugins {
   ...
   id "com.google.protobuf" version "0.8.17"
}
Add the Protobuf and Proto DataStore dependencies
// DataStore
implementation  "androidx.datastore:datastore-core:1.0.0"
// Protobuf
Implementation "com.google.protobuf:protobuf-javalite:3.18.0"
Configure protobuf. Add this code in build.gradle(app) file.
protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.14.0"
    }
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {
                    option 'lite'
                }
            }
        }
    }
}
Create the protobuf schema.

In protobufs, each structure is defined using a message keyword.
Each member of the structure is defined inside the message, based on type and name and it gets assigned a 1-based order.

Check Proto Language guide for more info on syntax

Create user_profile.proto file in /main/proto folder. Add the following code to the file.

syntax = "proto3";

message UserProfile{
    string name = 1;
    string email = 2;
    string nickname = 3;
}

Build the project.

Define a UserProfileSerializer class that implements Serializer<T>, where T is the type defined in the proto file. This serializer class tells DataStore how to read and write your data type.

object UserProfileSerializer : Serializer<UserProfile>{
    override val defaultValue: UserProfile
        get() = UserProfile.getDefaultInstance()

    override suspend fun readFrom(input: InputStream): UserProfile {
        try {
            return UserProfile.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }

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

To migrate to Proto DataStore,

  • Pass in SharedPreferencesMigration to the list of migrations parameter of datastore builder. The instance of SharedPreferencesMigration takes context and the name of the SharedPreferences to migrate as parameters.
  • Implement a mapping function that defines how to migrate from the key-value pairs used by SharedPreferences to the DataStore schema you defined.
val Context.protoDataStore: DataStore<UserProfile> by dataStore(
    fileName = "settings.pb",
    serializer = UserProfileSerializer,
    produceMigrations = { context ->
        listOf(
            SharedPreferencesMigration(
                context,
                SETTINGS_NAME
            ) { sharedPrefs: SharedPreferencesView, currentData: UserProfile ->
                currentData.toBuilder()
                    .setEmail(sharedPrefs.getString(EMAIL_KEY))
                    .setName(sharedPrefs.getString(NAME_KEY))
                    .setNickname(sharedPrefs.getString(NICKNAME_KEY))
                    .build()
            }
        )
    }
)

You have successfully migrated from SharedPreferences to DataStore. Now you read data using DataStore.data or update data using DataStore.update()

Thank you for reading 🤠