Jetpack DataStore: Preferences vs Proto

Jetpack DataStore is a data storage solution from the Android Jetpack libraries designed to replace SharedPreferences. DataStore offers several advantages over SharedPreferences, including asynchronous APIs, transactionality, and strong data consistency. Within DataStore, there are two main implementations: Preferences DataStore and Proto DataStore. Choosing between these options depends on your specific data storage needs. This blog post will explore the differences, benefits, and use cases of each implementation.

What is Jetpack DataStore?

Jetpack DataStore is a data persistence solution that allows you to store key-value pairs or typed objects with protocol buffers. It provides a safer and more efficient way to store data compared to SharedPreferences. Key benefits include:

  • Asynchronous Operations: Performs operations asynchronously, preventing UI freezes.
  • Data Consistency: Ensures data consistency through transactional updates.
  • Type Safety: Proto DataStore offers type safety via protocol buffers.
  • Migration Support: Easier data migration compared to SharedPreferences.

Preferences DataStore

Preferences DataStore stores data as key-value pairs, similar to SharedPreferences. It does not require defining a schema upfront and is straightforward to implement.

When to Use Preferences DataStore?

  • Simple Data: When storing simple data types like booleans, integers, strings, etc.
  • No Schema Required: When you don’t want to define a schema for your data.
  • Quick Implementation: For quick and easy implementation without complex configurations.

Example of Using Preferences DataStore

Step 1: Add Dependency

Add the Preferences DataStore dependency to your build.gradle file:

dependencies {
    implementation "androidx.datastore:datastore-preferences:1.0.0"
    implementation "androidx.datastore:datastore-preferences-core:1.0.0"
}

Step 2: Create the DataStore

Create an instance of Preferences DataStore:

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore

private val Context.dataStore: DataStore by preferencesDataStore(name = "my_preferences")

Step 3: Write Data

Write data to Preferences DataStore:

import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking

object PreferenceManager {
    private val EXAMPLE_STRING_KEY = stringPreferencesKey("example_string")

    suspend fun saveStringToPreferences(context: Context, value: String) {
        context.dataStore.edit { preferences ->
            preferences[EXAMPLE_STRING_KEY] = value
        }
    }

    fun getStringFromPreferences(context: Context): Flow {
        return context.dataStore.data.map { preferences ->
            preferences[EXAMPLE_STRING_KEY] ?: ""
        }
    }
}

// Usage:
fun main() {
    val context: Context = //Your application context
    runBlocking {
        PreferenceManager.saveStringToPreferences(context, "Hello DataStore!")
        PreferenceManager.getStringFromPreferences(context).collect { value ->
            println("Value from DataStore: $value")
        }
    }
}

Step 4: Read Data

Read data from Preferences DataStore:


//Reading the data
// In a coroutine scope, for example:
lifecycleScope.launch {
    dataStore.data
        .map { preferences ->
            preferences[EXAMPLE_STRING_KEY] ?: "default_value"
        }
        .collect { value ->
            // Use the value
            textView.text = value
        }
}

Proto DataStore

Proto DataStore stores data as typed objects using protocol buffers. This requires defining a schema using a .proto file and generating corresponding Kotlin classes.

When to Use Proto DataStore?

  • Complex Data: When storing complex data structures with specific fields and types.
  • Schema Definition: When you want to define a schema to ensure data consistency and type safety.
  • Versioned Data: For applications requiring versioned data structures and schema evolution.

Example of Using Proto DataStore

Step 1: Add Dependencies

Add the Proto DataStore and Protocol Buffers dependencies to your build.gradle file:

dependencies {
    implementation "androidx.datastore:datastore:1.0.0"
    implementation "com.google.protobuf:protobuf-javalite:3.18.0"
    implementation "androidx.protobuf:protobuf-kotlin-lite:3.18.0"
}

Configure Protocol Buffers in your build.gradle file:

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

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.18.0"
    }
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                java {
                    option "lite"
                }
            }
        }
    }
}

Step 2: Define the Proto Schema

Create a user_prefs.proto file in the src/main/proto directory:

syntax = "proto3";

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

message UserPreferences {
    string user_name = 1;
    int32 age = 2;
    bool is_active = 3;
}

Step 3: Generate Kotlin Classes

Build your project to generate the Kotlin classes from the .proto file. The generated class will be named UserPreferences.

Step 4: Create the DataStore

Create an instance of Proto DataStore:

import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.Serializer
import androidx.datastore.dataStore
import com.example.UserPreferences
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.io.OutputStream

object UserPreferencesSerializer : Serializer {
    override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()

    override suspend fun readFrom(input: InputStream): UserPreferences {
        return try {
            UserPreferences.parseFrom(input)
        } catch (e: Exception) {
            defaultValue
        }
    }

    override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
        withContext(Dispatchers.IO) {
            t.writeTo(output)
        }
    }
}

private val Context.userDataStore: DataStore by dataStore(
    fileName = "user_prefs.pb",
    serializer = UserPreferencesSerializer
)

Step 5: Write Data

Write data to Proto DataStore:

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import com.example.PreferenceManager //Assuming you created this previously for PreferenceDataStore
import com.example.UserPreferences //From the generated proto

object ProtoManager {
    suspend fun saveUserPreferences(context: Context, userName: String, age: Int, isActive: Boolean) {
        context.userDataStore.updateData { prefs ->
            prefs.toBuilder()
                .setUserName(userName)
                .setAge(age)
                .setIsActive(isActive)
                .build()
        }
    }

    fun getUserPreferences(context: Context): Flow {
        return context.userDataStore.data
    }
}

fun main() {
    val context: Context = //Your application context
    runBlocking {
        ProtoManager.saveUserPreferences(context, "Alice", 30, true)

        ProtoManager.getUserPreferences(context).collect { userPrefs ->
            println("User Name: ${userPrefs.userName}")
            println("Age: ${userPrefs.age}")
            println("Is Active: ${userPrefs.isActive}")
        }
    }
}

Step 6: Read Data

Read data from Proto DataStore:


lifecycleScope.launch {
    userDataStore.data.collect { userPreferences ->
        userNameTextView.text = userPreferences.userName
        ageTextView.text = userPreferences.age.toString()
        isActiveCheckBox.isChecked = userPreferences.isActive
    }
}

Differences Between Preferences and Proto DataStore

Feature Preferences DataStore Proto DataStore
Data Format Key-Value Pairs Typed Objects (Protocol Buffers)
Schema Definition No Schema Required Schema Required (.proto file)
Type Safety No Type Safety Strong Type Safety
Data Complexity Simple Data Types Complex Data Structures
Implementation Complexity Simpler More Complex
Data Migration Less Flexible More Flexible (schema evolution)
Dependencies androidx.datastore:datastore-preferences androidx.datastore:datastore, com.google.protobuf:protobuf-javalite

Best Practices

  • Choose the Right Implementation: Consider the complexity and type safety requirements when choosing between Preferences and Proto DataStore.
  • Handle Exceptions: Always handle exceptions when reading or writing data to prevent unexpected crashes.
  • Use Asynchronous Operations: Ensure all DataStore operations are performed asynchronously to avoid blocking the main thread.
  • Test Data Migration: When using Proto DataStore, thoroughly test data migrations when evolving the schema.
  • Context Handling: Properly manage the context when creating and accessing DataStore to avoid memory leaks.

Conclusion

Jetpack DataStore provides a robust and modern solution for data persistence in Android applications, offering improvements over SharedPreferences with asynchronous APIs and data consistency. Choosing between Preferences and Proto DataStore depends on your specific data storage requirements. Preferences DataStore is suitable for simple key-value storage with quick implementation, while Proto DataStore is ideal for complex data structures with type safety and schema evolution. By understanding the differences and benefits of each implementation, you can make an informed decision to enhance your app’s data management strategy.