Skip to main content

Working with DataStore

1. Setting Up DataStore Dependencies

DataStore is a modern Android data storage solution that provides a more robust and flexible alternative to SharedPreferences. It uses Kotlin Coroutines and Flow for asynchronous operations.

Dependency Version Description
androidx.datastore:datastore-core 1.1.0 Core DataStore functionality
androidx.datastore:datastore-preferences 1.1.0 Preferences DataStore implementation
androidx.datastore:datastore-runtime 1.1.0 Runtime support for DataStore

Example: Adding Dependencies to build.gradle.kts

dependencies {
    implementation("androidx.datastore:datastore-core:1.1.0")
    implementation("androidx.datastore:datastore-preferences:1.1.0")
}
Note: DataStore requires Android API level 21 or higher.

2. Creating Preferences DataStore

Preferences DataStore is a modern, robust, and type-safe way to store key-value pairs or simple data types in Android applications. It is built on top of Kotlin Coroutines and Flow, providing a reactive approach to data persistence.

Property Description Example
DataStore Interface for storing and retrieving data asynchronously val dataStore: DataStore<Preferences>
Preferences Represents the data structure used to store key-value pairs val preferences: Preferences
DataStoreFactory Factory for creating DataStore instances DataStoreFactory.create(...)

Example: Creating Preferences DataStore

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.first

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

suspend fun saveSetting(context: Context, key: String, value: String) {
    context.dataStore.edit { preferences ->
        preferences[Preferences.Key<String>(key)] = value
    }
}

suspend fun getSetting(context: Context, key: String): String? {
    return context.dataStore.data.first()[Preferences.Key<String>(key)]
}
Note: Preferences DataStore uses Preferences as its data structure, which is a key-value store. It supports primitive types like String, Int, Boolean, etc.
Warning: DataStore operations are asynchronous and must be performed on a background thread. Avoid calling data.first() on the main thread.

Key Points

  • DataStore is a modern alternative to SharedPreferences.
  • It uses Kotlin Coroutines and Flow for reactive data handling.
  • Preferences DataStore is ideal for simple key-value storage.

3. Reading Preferences

Property Type Description Default Value
datastore DataStore<Preferences> Instance of the DataStore used to read preferences N/A
key PreferenceKey<T> Key used to identify the preference value N/A

Example: Reading a String Preference

val stringKey = stringPreferencesKey("user_name")
val userName: String? = datastore.data
    .map { preferences ->
        preferences[stringKey]
    }
    .firstOrNull()

Example: Reading an Integer Preference

val intKey = intPreferencesKey("user_age")
val userAge: Int? = datastore.data
    .map { preferences ->
        preferences[intKey]
    }
    .firstOrNull()

Example: Reading a Boolean Preference

val boolKey = booleanPreferencesKey("is_dark_mode")
val isDarkMode: Boolean? = datastore.data
    .map { preferences ->
        preferences[boolKey]
    }
    .firstOrNull()
Note: The data property of the DataStore returns a Flow<Preferences>, which allows you to observe changes in preferences. The firstOrNull() function is used to get the first value emitted by the flow.

4. Writing Preferences

Property Type Description Example
datastore DataStore<Preferences> Instance of DataStore to write preferences val datastore: DataStore<Preferences> = context.createDataStore(name = "preferences")
key StringPreferencesKey Key to identify the preference value val themeKey = stringPreferencesKey("theme")
value String Value to store in preferences "dark_mode"

Example: Writing a Preference Value

val themeKey = stringPreferencesKey("theme")
val datastore: DataStore<Preferences> = context.createDataStore(name = "preferences")

// Writing a preference value
val writeJob = CoroutineScope(Dispatchers.IO).launch {
    datastore.edit { preferences ->
        preferences[themeKey] = "dark_mode"
    }
}

Example: Writing Multiple Preferences

val themeKey = stringPreferencesKey("theme")
val fontSizeKey = intPreferencesKey("font_size")
val datastore: DataStore<Preferences> = context.createDataStore(name = "preferences")

// Writing multiple preferences
val writeJob = CoroutineScope(Dispatchers.IO).launch {
    datastore.edit { preferences ->
        preferences[themeKey] = "light_mode"
        preferences[fontSizeKey] = 16
    }
}
Note: DataStore operations are asynchronous and must be handled within a coroutine scope.

5. Defining Preference Keys

Key Type Description Usage
int Integer preference key val ageKey = Preferences.Key<Int>(int::class.java)
String String preference key val nameKey = Preferences.Key<String>(String::class.java)
Boolean Boolean preference key val isActiveKey = Preferences.Key<Boolean>(Boolean::class.java)
Float Float preference key val scoreKey = Preferences.Key<Float>(Float::class.java)
Long Long preference key val timestampKey = Preferences.Key<Long>(Long::class.java)
Set<String> Set of strings preference key val tagsKey = Preferences.Key<Set<String>>(Set::class.java)

Example: Defining and Using Preference Keys

import androidx.datastore.preferences.core.*

val nameKey = Preferences.Key<String>(String::class.java)
val ageKey = Preferences.Key<Int>(Int::class.java)

val preferencesDataStore = DataStoreFactory.create(
    produceFile = { File(context.filesDir, "user_prefs.preferences_pb") }
)

// Reading a preference
val name: String? = preferencesDataStore.data.map { it[nameKey] }

// Writing a preference
val update = MutablePreferences()
update[nameKey] = "John Doe"
update[ageKey] = 30
preferencesDataStore.updateData { update }

6. Using Proto DataStore

Proto DataStore is a type-safe, asynchronous data storage solution that uses Protocol Buffers (protobuf) to serialize data. It is ideal for storing structured data with strong typing and efficient serialization.

Property Description Notes
Proto DataStore Uses Protocol Buffers for serialization Strongly typed, efficient storage
Async Operations All operations are asynchronous Requires CoroutineScope
Type Safety Compile-time type checking Reduces runtime errors

Example: Define a Proto Message

// user.proto
syntax = "proto3";

message User {
  string name = 1;
  int32 age = 2;
  bool is_active = 3;
}

Example: Create DataStore Instance

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.protobuf.GeneratedMessageV3

val dataStore: DataStore<User> = DataStore(
    serializer = UserSerializer
)

Example: Read Data

val user = dataStore.data.firstOrNull()
user?.let { userData ->
    println("Name: ${userData.name}")
}

Example: Write Data

val newUser = User.newBuilder()
    .setName("John Doe")
    .setAge(30)
    .setIsActive(true)
    .build()

dataStore.updateData { userData ->
    newUser
}

7. Handling Exceptions

Example: Catching DataStore Exceptions

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import java.io.IOException

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

suspend fun readPreferences() {
    try {
        val preferences = dataStore.data.first()
        // Process preferences
    } catch (e: IOException) {
        // Handle IO exceptions from DataStore
        Log.e("DataStore", "Failed to read preferences", e)
    } catch (e: Exception) {
        // Handle other exceptions
        Log.e("DataStore", "Unexpected error", e)
    }
}
Exception Type Description Handling Recommendation
IOException Thrown when DataStore encounters file system or I/O issues. Log the error and consider retry logic or fallback behavior.
DataStoreException Base exception for DataStore-related errors. Use to catch all DataStore-specific exceptions.
IllegalStateException Occurs when DataStore is accessed incorrectly (e.g., after being closed). Ensure proper lifecycle management of DataStore access.
Note: DataStore operations are asynchronous and may throw exceptions during read/write operations. Always wrap these in try-catch blocks to prevent crashes.

8. Implementing Migration from SharedPreferences

Example: Migrating SharedPreferences to DataStore

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import kotlinx.coroutines.flow.first

// Step 1: Read from SharedPreferences
val prefs = context.getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
val oldValue = prefs.getString("username", "default")

// Step 2: Write to DataStore
val dataStore = context.createDataStore(name = "user_datastore")
val editor = dataStore.edit { preferences -> preferences.toMutablePreferences() }
editor.set(stringPreferencesKey("username"), oldValue ?: "default")

// Step 3: Remove old SharedPreferences
prefs.edit().remove("username").apply()
Step Description Action
1. Read Old Data Retrieve data from SharedPreferences getSharedPreferences() and getString()
2. Write to DataStore Save data into new DataStore format dataStore.edit() and set()
3. Cleanup Remove old SharedPreferences keys edit().remove()
Note: Migration should be done once during app upgrade. Ensure all users have migrated before removing SharedPreferences.
Warning: Do not read from SharedPreferences after migration completes, as it may cause inconsistency.

Migration Best Practices

  • Perform migration once per major version update
  • Use Flow<Preferences> to observe changes in DataStore
  • Backward compatibility: Keep SharedPreferences until all users migrate

9. Using DataStore with Hilt

Example: Injecting DataStore in a ViewModel

class MyViewModel @Inject constructor(
    private val dataStore: DataStore<Preferences>
) : ViewModel() {
    fun saveValue(key: String, value: String) {
        viewModelScope.launch {
            dataStore.edit { preferences ->
                preferences[key] = value
            }
        }
    }
}
Component Description Notes
DataStore DataStore<Preferences> Used for reading/writing preferences
Hilt Module @Module with @Provides Defines how to create DataStore instance
ViewModel Injection @Inject constructor() Allows Hilt to inject DataStore
Note: Ensure DataStore is properly scoped and initialized in Hilt modules.
Warning: Avoid injecting DataStore into Activities or Fragments directly; prefer ViewModels.

Key Points

  • DataStore is injected using Hilt's dependency injection system.
  • Use @Inject in ViewModel constructors to receive DataStore.
  • Define Hilt modules for creating and configuring DataStore instances.

10. Implementing Type-Safe Preferences

Property Type Description Default
datastore DataStore<Preferences> Instance of DataStore for preferences None
key PreferenceKey<T> Key for accessing preference value None
value T Value to be stored or retrieved None

Example: Saving Preferences

val nameKey = stringPreferencesKey("name")
val ageKey = intPreferencesKey("age")

val preferences = datastore.edit { preferences -> 
    preferences[nameKey] = "John Doe"
    preferences[ageKey] = 30
}

Example: Reading Preferences

val nameKey = stringPreferencesKey("name")
val ageKey = intPreferencesKey("age")

val preferencesFlow = datastore.data.map { preferences ->
    val name = preferences[nameKey] ?: "Unknown"
    val age = preferences[ageKey] ?: 0
    "$name is $age years old"
}
Note: Ensure that all keys are defined as PreferenceKey instances to maintain type safety.

Was this article helpful?