Exploring Data Store — A New Way of Storing Data in Android.

DataStore: Simplify async, consistent, and transactional data storage in Android with Kotlin coroutines and Flow.
Aug 29 2022 · 5 min read

Introduction 

Jetpack DataStore is a data storage solution that allows you to store key-value pairs or typed objects with protocol buffers.
DataStore uses Kotlin coroutines and Flow to store data asynchronously, consistently, and transactionally.

We are hoping that you are already having sufficient knowledge regarding when, why, and how to store data in Android applications using SharedPreferencesand Room Database.

In this blog post, we will coverDataStore, and its two types (Preferences DataStore, Proto DataStore).

At the end of this blog, you will learn how to use both these DataStores with the example given below:

Preference DataStore

 

Proto DataStore

We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!

Let’s know basics of DataStore types!

Preferences DataStore: Stores and accesses data using Keys. This implementation does not require a predefinedschema and it does not provide type safety. Preferences DataStore uses key-value pairs to store and retrieve data.

The concept behind Preferences DataStore is similar to SharedPreferences. If you are currently using SharedPreferences in the project then at the end of this article you will be able to replace it with Preferences DataStore.

Proto DataStore: Stores data as instances of a custom data type. This implementation requires you to define schema using protocol buffers, but it provides type safety. Proto DataStore returns the generated object in a Flow.

The concept behind Proto DataStore is to store objects with custom data types.

1. Preferences DataStore

Implementation steps:

1. Add the below dependency into the app-level build.gradle file:


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

2. Create the instance of DataStore:

 val Context.dataStore: DataStore<Preferences> by preferencesDataStore("user_prefs")

Here, we have usedpreferencesDataStore, a property delegate that manages DataStore as a singleton. The mandatory parameter name(user_prefs) is the name of the Preferences DataStore.

3. Create keys:


   val USER_AGE_KEY = intPreferencesKey("USER_AGE")
   val USER_NAME_KEY = stringPreferencesKey("USER_NAME")

As the preferences DataStore does not use any kind ofschema, we need to use the key type function to define a key for each value that we need to store in the Database.

Here, we want to store user age and user name in the preferences DataStore and the corresponding key types will be intPreferencesKey() and stringPreferencesKey().

4. Write to a Preferences DataStore


  suspend fun storeUserInfo(age: Int, name: String) {
        context.dataStore.edit { preferences ->
            preferences[USER_AGE_KEY] = age
            preferences[USER_NAME_KEY] = name
        }
    }

Here, we have the suspended functionedit() which needs to be called from CoroutineContext and inside the lambda, we have MutablePreferences hence we can easily store/change the required values using keys.

Here, we should observe that we can easily get rid of using apply() or commit()functions to save changes (that we need to use in SharedPreferences)

5. Read from a Preferences DataStore


 // get the user's age
    val userAgeFlow: Flow<Int> = context.dataStore.data.map { preferences ->
        preferences[USER_AGE_KEY] ?: 0
    }

    // get the user's name
    val userNameFlow: Flow<String> = context.dataStore.data.map { preferences ->
        preferences[USER_NAME_KEY] ?: ""
    }

As the preferences DataStore exposes a Flow representing the current state of data, we can easily get the Flow<Int>and Flow<String> for the user’s age and name respectively.

The complete class details for creating the database instance and reading/writing data are available on Github.

2. Proto DataStore

Implementation steps:

1. Add the below dependency into the app-level build.gradle file:


    // Proto DataStore
    implementation "androidx.datastore:datastore:1.0.0"

    // protobuf
    implementation "com.google.protobuf:protobuf-javalite:3.19.4"

Here, note that we have only taken dependency for Proto DataStore, if you want to use both Preferences DataStore and Proto DataStore in the same project kindly add the dependency for a Preferences DataStore also.

2. Add the protobuf to plugins in the build.gradle file:


plugins {
    ...
    id "com.google.protobuf" version "0.8.17"
}

3. Add the protobuf configuration in the build.gradle file:


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

    // Generates the java Protobuf-lite code for the Protobufs in this project. See
    // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
    // for more information.
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {
                    option 'lite'
                }
            }
        }
    }
}

4. Add the proto file (Define a schema):


syntax = "proto3";

option java_package = "com.example.protodatastore";
option java_multiple_files = true;

message EmployeePreference{
  string emp_name = 1;
  string emp_designation = 2;
}

Here, we have defined a schema called EmployeePreference which contains twofieldsof string types called emp_name and emp_designation.

We need to place our .proto file under app/source/main/proto folder. In our case file name is employee_pref.proto.

As you can see, each field in the message definition has a unique number. These field numbers are used to identify your fields in the message binary format, and should not be changed once your message type is in use.

As the class of the stored object is generated at compile time make sure to rebuild the project.

5. Create the serializer for DataStore:


object EmployeePreferencesSerializer : Serializer<EmployeePreference> {
    override val defaultValue: EmployeePreference = EmployeePreference.getDefaultInstance()

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

    @Suppress("BlockingMethodInNonBlockingContext")
    override suspend fun writeTo(t: EmployeePreference, output: OutputStream) = t.writeTo(output)
}

The class EmployeePreferencesSerializer implements Serializer<EmployeePreference>where EmployeePreference is the type defined in the proto file. This serializer class tells DataStore how to read and write your data type.

6. Create the DataStore:


private val Context.employeeProtoDataStore: DataStore<EmployeePreference> by dataStore(
        fileName = DATA_STORE_FILE_NAME,
        serializer = EmployeePreferencesSerializer
    )

Use the property delegate created by dataStore to create an instance of DataStore, where the type is the one defined in the proto file. Call this once at the top level of your kotlin file and access it through this property delegate throughout the rest of your app. The filename parameter tells DataStore which file to use to store the data, and the serializer parameter tells DataStore the name of the serializer class defined in step 5.

7. Write to a Proto DataStore:


  suspend fun saveEmployeeInfo(eName: String, eDesignation: String) {
        context.employeeProtoDataStore.updateData { employeeData ->
            employeeData.toBuilder()
                .setEmpName(eName)
                .setEmpDesignation(eDesignation)
                .build()

        }
    }

Proto DataStore provides updateData()function that transactionally updates a stored object.updateData() gives us the current state of the data as an instance of our data type and updates the data transactionally in an atomic read-write-modify operation.

8. Read from a Proto DataStore:


  //Reading employee object from a proto data store
    val employeeInfo: Flow<EmployeePreference> = context.employeeProtoDataStore.data
        .map {
            it
        }

    //Reading employee object property empName from a proto data store
    val empName: Flow<String> = context.employeeProtoDataStore.data
        .map {
            it.empName
        }

    //Reading employee object property empDesignation from a proto data store
    val empDesignation: Flow<String> = context.employeeProtoDataStore.data
        .map {
            it.empDesignation
        }

Here, we have used employeeProtoDataStore.data to read theFlow of either the employee object or its properties like empName or empDesignation.

The complete class details for creating the proto-type database instance and reading/writing data are available on Github.

Conclusion

That’s it for today, hope you learned something!

Don’t worry if you didn’t get all the DataStore stuff.

You can find the complete source code of the preferences DataStore on Github.

You can find the complete source code of the proto DataStore on Github.


Code, Build, Repeat.
Stay updated with the latest trends and tutorials in Android, iOS, and web development.
jimmy image
Jimmy Sanghani
Jimmy Sanghani is a tech nomad and cofounder at Canopas helping businesses use new age leverage - Code and Media - to grow their revenue exponentially. With over a decade of experience in both web and mobile app development, he has helped 100+ clients transform their visions into impactful digital solutions. With his team, he's helping clients navigate the digital landscape and achieve their objectives, one successful project at a time.
jimmy image
Jimmy Sanghani
Jimmy Sanghani is a tech nomad and cofounder at Canopas helping businesses use new age leverage - Code and Media - to grow their revenue exponentially. With over a decade of experience in both web and mobile app development, he has helped 100+ clients transform their visions into impactful digital solutions. With his team, he's helping clients navigate the digital landscape and achieve their objectives, one successful project at a time.
canopas-logo
We build products that customers can't help but love!
Get in touch

Whether you need...

  • *
    High-performing mobile apps
  • *
    Bulletproof cloud solutions
  • *
    Custom solutions for your business.
Bring us your toughest challenge and we'll show you the path to a sleek solution.
Talk To Our Experts
footer
Follow us on
2024 Canopas Software LLP. All rights reserved.