The new way of storing data in Android — Jetpack DataStore

Gábor Marosfalvi
Supercharge's Digital Product Guide
7 min readAug 11, 2021
Photo by CHUTTERSNAP on Unsplash

Key concept

In mobile applications some data has to be persisted to make the application startup faster, reduce network traffic or handle data completely offline. In this article, I would like to demonstrate the opportunities of how to store data in your Android application, especially with Jetpack DataStore. To introduce this I am going to give a walkthrough about the four mainly used approaches. The solutions that will be covered in the article are:

  • SharedPreferences
  • Preferences DataStore
  • Room
  • Proto DataStore

Besides the summary of current storage solutions, I will focus on the differences between SharedPrefrences, Room and DataStore. Regarding the DataStore, I added the implementation steps for both Preferences DataStore, and Proto DataStore.

SharedPreferences

If you have to store key-value pairs in your app, e.g.: user’s settings, IDs, the simplest way to implement this solution is the SharedPreferences API. A SharedPreferences object points to a file containing key-value pairs and provides simple methods to read and write them. Each SharedPreferences file is managed by the framework and can be private or shared across applications.

Implementation

Add required gradle dependency:

Saving user information to SharedPreferences:

Retrieving user information from SharedPreferences:

In the case of my application, which demonstrates a simple user data saving with the usage of Kotlin coroutines, this solution is not easy to use with Flow.

Unfortunately, SharedPreferences runs its read/write operations, except apply() function, on the main thread. Due to this disadvantage, and the lack of Kotlin Flow, and LiveData support, I would like to recommend a more sophisticated option, the Preferences DataStore.

Preferences DataStore

Preferences DataStore aims to replace SharedPreferences. The concept behind Preference DataStore is quite similar to SharedPreferences. It uses key-value pairs to store data and also does not provide type safeness. If you’re currently using SharedPreferences to store data, consider migrating to DataStore instead.

Why should you use Preferences DataStore instead of SharedPreferences?

  • DataStore is safe to call on the UI thread because it uses Dispatchers.IO under the hood
  • You do not have to use apply() or commit() functions to save the changes
  • Handles data updates transactionally
  • Exposes a Flow representing the current state of data

Implementation

1. Need to add this line into app-level build.gradle file’s dependencies

2. Create the DataStore instance

To create a Preferences DataStore, we can use the preferencesDataStore delegate. The delegate will ensure that we have a single instance of DataStore with that name in our application.

3. Create keys for the key part of the key-value pairs (preferencesKeys)

4. To save data into DataStore

The edit() function is a suspend function, so it needs to be called from CoroutineContext.

Inside the lambda we have access to MutablePreferences, so we can change the value under the specified key.

5. Get the Flow<User> from DataStore

Migration from SharedPreferences

With the usage of SharedPreferencesMigration we have the opportunity to migrate the “old fashioned” SharedPrefrences data to DataStore. Related/suggested article in this topic: Working with Preferences DataStore - Codelabs step by step guide - Step 7

SharedPreferences vs DataStore

  • It is built on Kotlin Coroutines and Flow. It exposes the preference values using Flow.
  • You don’t need to manually switch to a background thread
  • DataStore is safe from runtime exceptions and has error handling support. SharedPreferences throws parsing errors as runtime exceptions.
  • In both implementations, DataStore saves the preferences in a file and performs all data operations on Dispatchers.IO unless specified otherwise.

In my opinion, the following table is the best way to highlight the differences between the two key-value pair based storage approaches, and the Proto DataStore:

Room

Room is designed to store and handle non-trivial amounts of structured data locally. Under the hood Room is a persistence library, which provides an abstraction layer over SQLite. The three major components of the library are:

  • Entity
  • DAO
  • Database

The following diagram illustrates the connection between these major components:

Why use Room?

  • Part of Jetpack
  • Verifying SQL queries at compile time
  • It can be integrated with LiveData, RxJava, and Coroutine Flow easily
  • Reduces the amount of boilerplate code

Implementation

Add required Gradle dependencies:

Create the DAO with the Room SQL queries:

Room row insertion:

When should you use DataStore instead of Room?

If you use Room to save only one user’s data, you need to create a database with a table, and also need to implement the query and insert functions. It sounds really inconvenient, so in this case, the Proto DataStore can be the corresponding approach.

Proto Datastore

The aim of Proto DataStore is really similar to Preferences DataStore, but the previous one is able to store objects with custom data types. Unlike the Preferences DataStore it does not use key-value pairs, just returns the generated object in a Flow. The generated file’s type and structure depend on the schema of the .protoc file.

Key features:

  • Provides type safety out of the box
  • Able to define list inside protoc schema with repeated marked fields
  • Requires to learn a new serialization mechanism, which depends on Protobuf, but it is worth the effort
  • Protobuf is faster than XML and JSON serialization formats

Implementation

1. Add the following gradle dependencies to your app-level build.gradle file

If you need to use only Proto DataStore with typed objects you do not need to add the preferences version of the datastore dependency (androidx.datastore:datastore-preferences:1.0.0-beta01).

2. Add protobuf to plugins in build.gradle

3. Add protobuf configuration to build.gradle

4. Add your proto file to project

In my case, I need to define the schema, which contains the user’s first name, last name, and birthday. So I need to add two string fields, and one 64-bit integer field (int64). Birthday is stored in Long format, and that was the reason for using a 64-bit integer field.

You need to place your .proto file under app/src/main/proto folder. I gave user_preference.proto name to my file.

If you want to be familiar with the syntax of the protocol buffers, you should check this documentation:

5. Create the DataStore Serializer

This serializer class tells DataStore how to read and write your data type. Kotlin Serialization supports multiple formats including JSON and Protocol buffers.

As I mentioned before you have an option to use JSON instead of Protocol buffers. In my opinion, JSON is more readable and understandable, but the most common usage of DataStore depends on protocol buffers. Related, and recommended article, if you want to start a JSON based DataStore:

6. Create the DataStore

7. To save data into DataStore

8. Get the Flow<User> from DataStore

Default value

First of all, if you want to read an empty DataStore, which contains only standard field types (e.g.: string, int32, enum), it will return the protobuf’s generated object with pre-initialized default values. In this case, your integers will be zero, and the strings will be empty strings.

If you import google/protobuf/wrappers.proto into your protobuf file, which depends on proto3 syntax you will be able to add fields with nullable types, e.g.: google.protobuf.StringValue first_name = 1;

Room vs DataStore

If you have a need for partial updates, referential integrity, or large/complex datasets, you should consider using Room instead of DataStore. DataStore is ideal for small or simple datasets and does not support partial updates or referential integrity.

Suppose that your data structure is not large/complex, and need to store only one/few rows in a table, e.g: one user’s data. In this scenario, the usage of the Room can be a little bit of overkill. So if you have to store only a small bunch of data in your database, you have only one or few tables, and you don’t want to use the benefits of Room, you should choose DataStore.

The most crucial disadvantage of DataStore against Room is that, it does not support partial updates: if any field is modified, the whole object will be serialized and persisted to disk. If you want partial updates, consider the Room API (SQLite).

Wrapping up

In summary, DataStore is ideal to use with Kotlin, and especially with coroutines. Don’t worry if you already have an application, which contains only the standard SharedPreferences solution. The migration is provided by the DataStore API.

Because of the fact that DataStore handles data migration, guarantees data consistency, and handles data corruption, it is worth changing from SharedPreferences to DataStore.

The examples of the article were implemented originally in my POC application, which aims to demonstrate the implementation of these four ways of storage. If you are interested in the deeper technical/coding part of the topic, feel free to check the application on Github:

Resources/additional recommended articles

--

--