DataStore is a component from the Jetpack library designed for storing small amounts of data in Android applications. Unlike SharedPreferences, it works asynchronously and guarantees data consistency under concurrent access. According to Google, 2024, DataStore uses Kotlin Coroutines and Flow, making it safe for the main thread and suitable for reactive architectures.
Key Takeaways
DataStore is a solution from Google for local data storage in Android, introduced in 2020 as an alternative to SharedPreferences. It supports two modes: Preferences DataStore (simple key-value pairs) and Proto DataStore (a typed schema based on Protocol Buffers).
The main advantage is full asynchronicity: all read operations return a Flow from Kotlin Coroutines, and writes are performed in a coroutine context. This eliminates main thread blocking, which was a typical problem with SharedPreferences when handling large amounts of data.
DataStore guarantees operation atomicity: concurrent writes do not lead to data loss thanks to its transactional model. If two components modify the same value simultaneously, DataStore correctly handles the conflict through a compare-and-swap mechanism.
According to Google I/O 2023, DataStore is used in 40% of new Android projects, and Google recommends migrating from SharedPreferences in all applications where settings storage stability is required.
At the core of DataStore lies SingleProcessDataStore — an implementation that operates within a single process. It uses file-based storage with file-level locking: when writing data, the file is locked, preventing corruption under concurrent access.
DataStore automatically handles deserialization errors: if the file is corrupted, it returns a default value and overwrites the file. This behavior is configurable via corruptionHandler, which can be set when creating the DataStore.
SharedPreferences suffers from three fundamental problems: synchronous disk reading on the main thread, lack of atomicity guarantees for concurrent writes, and inability to reactively track changes. DataStore solves all three: Flow for observation, file locking for atomicity, and an asynchronous API for thread safety.
DataStore stores data in files on the device's internal storage. Preferences DataStore uses a file format similar to SharedPreferences but with additional metadata for integrity checking. Proto DataStore uses the binary Protocol Buffers format, which reduces file size and speeds up serialization.
When reading data, DataStore loads the entire file into memory once, after which subscribers receive the current state via Flow. Changes are broadcast to all active subscribers automatically — no manual listener registration is required, unlike in SharedPreferences.
Preferences DataStore uses a built-in serialization mechanism based on a Map. Each entry is a pair of a string and a primitive type (Int, Boolean, Float, Long, String, Set). Data is stored in an XML file, similar to SharedPreferences, but with atomic writing through file locking.
Example of creating Preferences DataStore: the preferencesDataStore extension on Context creates a singleton with the file name. On repeated calls, the same instance is returned — this eliminates file duplication and confusion between different storage instances.
Proto DataStore requires defining a data schema through a .proto file and compiling it with the protobuf plugin. The generated Java class is used as the single entry point for all fields — this eliminates typos in keys, which are common with SharedPreferences.
The Proto DataStore schema is defined once and supports adding new fields without losing old data. If a new version of the app adds a field with a default value, the old file will be correctly deserialized — backward compatibility is built into the protocol.
The choice between Preferences DataStore and Proto DataStore depends on data complexity and typing requirements. Both options are asynchronous and transactional, but differ in type safety and serialization performance.
| Characteristic | Preferences DataStore | Proto DataStore |
|---|---|---|
| Typing | Weak (key-value) | Strong (generated class) |
| Serialization | XML (built-in) | Protocol Buffers (protobuf) |
| File Size | Large (readable XML) | Small (binary) |
| Complexity | Low (no .proto) | Medium (.proto required) |
| Schema Migration | No schema | Automatic (proto) |
| Compatibility | SharedPreferences (via migration) | Proto DataStore only |
Preferences DataStore is suitable for simple settings: feature flags, authorization token string, app launch count. If the data is small (up to 10–15 keys) and does not require a strict schema, Preferences DataStore provides a minimal entry threshold without connecting the protobuf plugin.
Proto DataStore is justified when the data structure is complex or may change between app versions. For example, user profile settings or A/B test configuration with 20+ fields. Protobuf provides strong typing and automatic migrations, eliminating runtime errors due to key mismatches.
Google provides a built-in migration mechanism through the SharedPreferencesMigration class. Migration is performed once on first launch after the app update: DataStore reads data from SharedPreferences, writes it into its own format, and marks the migration as complete.
Migration supports custom transformations: if keys in SharedPreferences do not match the desired DataStore keys, you can specify a transformation function via SharedPreferencesMigration. This allows renaming keys and changing data types during migration.
First, add DataStore to build.gradle and create a DataStore instance with migration: SharedPreferencesMigration accepts the SharedPreferences file name and a set of keys to transfer. Second, remove all code working through SharedPreferences and replace it with DataStore calls. Third, test the migration: on first launch, data should appear in DataStore, and the old SharedPreferences file should no longer be used.
val Context.dataStore by preferencesDataStore(
name = "settings",
produceMigrations = { context ->
listOf(
SharedPreferencesMigration(context, "old_prefs")
)
}
)
DataStore integrates easily into an existing project. Below are practical examples for Preferences DataStore and Proto DataStore — both demonstrate reading, writing, and reactive observation of data.
In this example, Preferences DataStore stores three settings: dark theme, username, and launch count. Reading is done via the .data extension, which returns a Flow. Writing is done via the .edit suspend function, guaranteeing atomicity of changes.
val Context.settingsDataStore by preferencesDataStore(name = "settings")
val isDarkMode: Flow<Boolean> = settingsDataStore.data
.map { preferences ->
preferences[booleanPreferencesKey("dark_mode")] ?: false
}
suspend fun toggleDarkMode() {
settingsDataStore.edit { prefs ->
val current = prefs[booleanPreferencesKey("dark_mode")] ?: false
prefs[booleanPreferencesKey("dark_mode")] = !current
}
}
Proto DataStore requires defining a .proto file. After compilation, a UserSettings class is created, which is used for reading and writing. Version migrations of the schema are described in the same .proto file and applied automatically.
// user_preferences.proto
syntax = "proto3";
message UserPreferences {
string display_name = 1;
int32 notification_count = 2;
bool notifications_enabled = 3;
}
// Reading from DataStore
val userPreferencesFlow: Flow<UserPreferences> =
protoDataStore.data
// Writing new values
suspend fun updateDisplayName(name: String) {
protoDataStore.updateData { prefs ->
prefs.toBuilder()
.setDisplayName(name)
.build()
}
}
DataStore integrates with MVVM architecture through ViewModel. Flow from DataStore is collected via .stateIn and used in the UI. With each data change, the UI updates automatically — no manual updates or LiveData needed.
class SettingsViewModel(
private val dataStore: DataStore<Preferences>
) : ViewModel() {
val uiState: StateFlow<SettingsUiState> =
dataStore.data
.map { prefs ->
SettingsUiState(
isDarkMode = prefs[booleanPreferencesKey("dark_mode")] ?: false,
counter = prefs[intPreferencesKey("launch_count")] ?: 0
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = SettingsUiState()
)
}
Frequently Asked Questions
DataStore works asynchronously (does not block the UI thread), supports concurrent access through transactions, and allows reactive subscription to changes via Flow. SharedPreferences is a synchronous API with the risk of ANR with large data volumes and no built-in reactivity support.
DataStore is written in Kotlin and requires Kotlin Coroutines. Using it from Java is possible but inconvenient: you would need to create wrappers with CompletableFuture or manually manage coroutines. For Java projects, Google recommends keeping SharedPreferences or adding Kotlin to the module.
DataStore loads the entire file into memory when reading, so it is not suitable for storing lists or large objects. For such scenarios, use Room or SQLite. DataStore is optimized for settings and small structured data — up to hundreds of kilobytes.
When creating a DataStore, you can pass a corruptionHandler — a function called when the file is corrupted. By default, DataStore throws a CorruptionException. In the corruptionHandler, you can return empty data, after which DataStore will overwrite the file with a correct state.
Yes, Proto DataStore requires defining a schema in a .proto file and connecting the protobuf-gradle-plugin. If the project is small and the data is simple, it is easier to use Preferences DataStore — it does not require additional build configuration.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also