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
@Injectin 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.