DataStore Part of Android Jetpack.
Jetpack DataStore is a data storage solution that lets you store key-value pairs or typed objects with protocol buffers. DataStore uses Kotlin coroutines and Flow to store data asynchronously, consistently, and transactionally.
If you're using SharedPreferences to store data, consider migrating to
DataStore instead.
DataStore API
The DataStore interface provides the following API:
A flow that can be used to read data from the DataStore
val data: Flow<T>A function to update data in the DataStore
suspend updateData(transform: suspend (t) -> T)
DataStore Configurations
If you want to store and access data using keys, use the Preferences
DataStore implementation which does not require a predefined schema, and it does
not provide type safety. It has a SharedPreferences-like API but doesn't
have the drawbacks associated with shared preferences.
DataStore lets you persist custom classes. To do this, you must define a
schema for the data and provide a Serializer to convert it into a persistable
format. You can choose to use Protocol Buffers, JSON or any other serialization
strategy.
Setup
To use Jetpack DataStore in your app, add the following to your Gradle file depending on which implementation you want to use:
Preferences DataStore
Add the following lines to the dependencies part of your gradle file:
Groovy
dependencies { // Preferences DataStore (SharedPreferences like APIs) implementation "androidx.datastore:datastore-preferences:1.2.1" // Alternatively - without an Android dependency. implementation "androidx.datastore:datastore-preferences-core:1.2.1" }
Kotlin
dependencies { // Preferences DataStore (SharedPreferences like APIs) implementation("androidx.datastore:datastore-preferences:1.2.1") // Alternatively - without an Android dependency. implementation("androidx.datastore:datastore-preferences-core:1.2.1") }
To add optional RxJava support, add the following dependencies:
Groovy
dependencies { // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.2.1" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.2.1" }
Kotlin
dependencies { // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.2.1") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.2.1") }
DataStore
Add the following lines to the dependencies part of your gradle file:
Groovy
dependencies { // Typed DataStore for custom data objects (for example, using Proto or JSON). implementation "androidx.datastore:datastore:1.2.1" // Alternatively - without an Android dependency. implementation "androidx.datastore:datastore-core:1.2.1" }
Kotlin
dependencies { // Typed DataStore for custom data objects (for example, using Proto or JSON). implementation("androidx.datastore:datastore:1.2.1") // Alternatively - without an Android dependency. implementation("androidx.datastore:datastore-core:1.2.1") }
Add the following optional dependencies for RxJava support:
Groovy
dependencies { // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.2.1" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.2.1" }
Kotlin
dependencies { // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.2.1") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.2.1") }
To serialize content, add dependencies for either Protocol Buffers or JSON serialization.
JSON serialization
To use JSON serialization, add the following to your Gradle file:
Groovy
plugins { id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" } dependencies { implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0" }
Kotlin
plugins { id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" } dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") }
Protobuf serialization
To use Protobuf serialization, add the following to your Gradle file:
Groovy
plugins { id("com.google.protobuf") version "0.9.5" } dependencies { implementation "com.google.protobuf:protobuf-kotlin-lite:4.32.1" } protobuf { protoc { artifact = "com.google.protobuf:protoc:4.32.1" } generateProtoTasks { all().forEach { task -> task.builtins { create("java") { option("lite") } create("kotlin") } } } }
Kotlin
plugins { id("com.google.protobuf") version "0.9.5" } dependencies { implementation("com.google.protobuf:protobuf-kotlin-lite:4.32.1") } protobuf { protoc { artifact = "com.google.protobuf:protoc:4.32.1" } generateProtoTasks { all().forEach { task -> task.builtins { create("java") { option("lite") } create("kotlin") } } } }
Use DataStore correctly
In order to use DataStore correctly always keep in mind the following rules:
Never create more than one instance of
DataStorefor a given file in the same process. Doing so can break all DataStore functionality. If there are multiple DataStores active for a given file in the same process, DataStore will throwIllegalStateExceptionwhen reading or updating data.The generic type of the
DataStore<T>must be immutable. Mutating a type used in DataStore invalidates the consistency that DataStore provides and creates potentially serious, hard-to-catch bugs. We recommend that you use protocol buffers, which help ensure immutability, a clear API, and efficient serialization.Do not mix usages of
SingleProcessDataStoreandMultiProcessDataStorefor the same file. If you intend to access theDataStorefrom more than one process, you must useMultiProcessDataStore.
Data Definition
Preferences DataStore
Define a key that will be used to persist data to disk.
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
JSON DataStore
For JSON datastore, add a @Serialization annotation to the data that you
want to persist.
@Serializable
data class Settings(
val exampleCounter: Int
)
Define a class that implements Serializer<T>, where T is the type of the
class you added the earlier annotation to. Make sure you include a default
value for the serializer to be used if there is no file created yet.
object SettingsSerializer : Serializer<Settings> {
override val defaultValue: Settings = Settings(exampleCounter = 0)
override suspend fun readFrom(input: InputStream): Settings =
try {
Json.decodeFromString<Settings>(
input.readBytes().decodeToString()
)
} catch (serialization: SerializationException) {
throw CorruptionException("Unable to read Settings", serialization)
}
override suspend fun writeTo(t: Settings, output: OutputStream) {
output.write(
Json.encodeToString(t)
.encodeToByteArray()
)
}
}
Proto DataStore
The Proto DataStore implementation uses DataStore and protocol buffers to persist typed objects to disk.
Proto DataStore requires a predefined schema in a proto file in the
app/src/main/proto/ directory. This schema defines the type for the objects
that you persist in your Proto DataStore. To learn more about defining a proto
schema, see the protobuf language guide.
Add a file called settings.proto inside the src/main/proto folder:
syntax = "proto3";
option java_package = "com.example.datastore.snippets.proto";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
Define a class that implements Serializer<T>, where T is the type defined
in the proto file. This serializer class defines how DataStore reads and
writes your data type. Make sure you include a default value for the
serializer to be used if there is no file created yet.
object SettingsSerializer : Serializer<Settings> {
override val defaultValue: Settings = Settings.getDefaultInstance()
override suspend fun readFrom(input: InputStream): Settings {
try {
return Settings.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override suspend fun writeTo(t: Settings, output: OutputStream) {
return t.writeTo(output)
}
}
Create a DataStore
You need to specify a name for the file that is used to persist the data.
Preferences DataStore
The Preferences DataStore implementation uses the DataStore and
Preferences classes to persist key-value pairs to disk. Use the
property delegate created by preferencesDataStore to create an instance
of DataStore<Preferences>. Call it once at the top level of your Kotlin
file. Access DataStore through this property throughout the rest of your
application. This makes it easier to keep your DataStore as a singleton.
The mandatory name parameter is the name of the Preferences DataStore.
// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
JSON DataStore
Use the property delegate created by dataStore to create an instance of
DataStore<T>, where T is the serializable data class. Call it once
at the top level of your Kotlin file and access it through this property
delegate throughout the rest of your app. The fileName parameter tells
DataStore which file to use to store the data, and the serializer parameter
tells DataStore the name of the serializer class defined earlier.
val Context.dataStore: DataStore<Settings> by dataStore(
fileName = "settings.json",
serializer = SettingsSerializer,
)
Proto DataStore
Use the property delegate created by dataStore to create an instance of
DataStore<T>, where T is the type defined in the proto file. Call it
once at the top level of your Kotlin file and access it through this property
delegate throughout the rest of your app. The fileName parameter tells
DataStore which file to use to store the data, and the serializer parameter
tells DataStore the name of the serializer class defined earlier.
val Context.dataStore: DataStore<Settings> by dataStore(
fileName = "settings.pb",
serializer = SettingsSerializer,
)
Read from DataStore
You need to specify a name for the file that is used to persist the data.
Preferences DataStore
Because Preferences DataStore doesn't use a predefined schema, you must use
the corresponding key type function to define a key for each value that you
need to store in the DataStore<Preferences> instance. For example, to define
a key for an int value, use intPreferencesKey. Then, use the
DataStore.data property to expose the appropriate stored value using a
Flow.
fun counterFlow(): Flow<Int> = context.dataStore.data.map { preferences ->
preferences[EXAMPLE_COUNTER] ?: 0
}
JSON DataStore
Use DataStore.data to expose a Flow of the appropriate property from your
stored object.
fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
settings.exampleCounter
}
Proto DataStore
Use DataStore.data to expose a Flow of the appropriate property from your
stored object.
fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
settings.exampleCounter
}
Use collectAsStateWithLifecycle to consume the Flow produced by
a ViewModel in a composable.
This safely converts the DataStore Flow into Compose State that triggers
recomposition.
@Composable
fun SomeScreen(counterFlow: Flow<Int>) {
val counter by counterFlow.collectAsStateWithLifecycle(initialValue = 0)
Text(text = "Example counter: ${counter}")
}
For more information on collectAsStateWithLifecycle,
see State and Jetpack Compose.
Write to DataStore
DataStore provides an updateData function that transactionally updates a
stored object. updateData gives you the current state of the data as an
instance of your data type and updates the data transactionally in an atomic
read-write-modify operation. All of the code in the updateData block is
treated as a single transaction.
Preferences DataStore
suspend fun incrementCounter() {
context.dataStore.updateData {
it.toMutablePreferences().also { preferences ->
preferences[EXAMPLE_COUNTER] = (preferences[EXAMPLE_COUNTER] ?: 0) + 1
}
}
}
JSON DataStore
suspend fun incrementCounter() {
context.dataStore.updateData { settings ->
settings.copy(exampleCounter = settings.exampleCounter + 1)
}
}
Proto DataStore
suspend fun incrementCounter() {
context.dataStore.updateData { settings ->
settings.copy { exampleCounter = exampleCounter + 1 }
}
}
Use DataStore in a Compose app
To use DataStore in a Compose app, follow Android app architecture guidelines by
keeping DataStore operations in your data layer (such as a repository) and
exposing the data to your UI through a ViewModel.
Avoid reading from or writing to DataStore directly within your composable functions.
Expose DataStore through a ViewModel. Pass your repository (which wraps the DataStore) into your
ViewModeland convert theFlowto aStateFlowso the UI can easily observe it, as shown in the following snippet:class SettingsViewModel( private val userPreferencesRepository: UserPreferencesRepository ) : ViewModel() { // Expose the DataStore flow as a StateFlow for Compose val userSettings: StateFlow<UserSettings> = userPreferencesRepository.userSettingsFlow .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = UserSettings.getDefaultInstance() ) fun updateCounter(newValue: Int) { viewModelScope.launch { userPreferencesRepository.updateCounter(newValue) } } }Observe and write from your composable. Use
collectAsStateWithLifecycleto safely observe theStateFlowin your UI, and call theViewModelfunctions to handle writes, as shown in the following snippet:@Composable fun SettingsScreen( viewModel: SettingsViewModel = viewModel() ) { // Safely collect the state val settings by viewModel.userSettings.collectAsStateWithLifecycle() Column(modifier = Modifier.padding(16.dp)) { Text(text = "Current counter: ${settings.counter}") Spacer(modifier = Modifier.height(8.dp)) Button(onClick = { viewModel.updateCounter(settings.counter + 1) }) { Text("Increment Counter") } } }
Use DataStore in multi-process code
You can configure DataStore to access the same data across different processes with the same data consistency properties as from within a single process. In particular, DataStore provides the following properties:
- Reads return only the data that has been persisted to disk.
- Read-after-write consistency.
- Writes are serialized.
- Reads are never blocked by writes.
Consider a sample application with a service and an activity where the service is running in a separate process and periodically updates the DataStore.
This example uses a JSON datastore, but you can also use a Preferences or Proto DataStore.
@Serializable
data class Time(
val lastUpdateMillis: Long
)
A serializer tells DataStore how to read and write your data type. Make sure
you include a default value for the serializer to be used if there is no file
created yet. The following is an example implementation using
kotlinx.serialization:
object TimeSerializer : Serializer<Time> {
override val defaultValue: Time = Time(lastUpdateMillis = 0L)
override suspend fun readFrom(input: InputStream): Time =
try {
Json.decodeFromString<Time>(
input.readBytes().decodeToString()
)
} catch (serialization: SerializationException) {
throw CorruptionException("Unable to read Time", serialization)
}
override suspend fun writeTo(t: Time, output: OutputStream) {
output.write(
Json.encodeToString(t)
.encodeToByteArray()
)
}
}
To be able to use DataStore across different processes, you need to construct
the DataStore object using the MultiProcessDataStoreFactory for both the app
and the service code:
val dataStore = MultiProcessDataStoreFactory.create(
serializer = TimeSerializer,
produceFile = {
File("${context.filesDir.path}/time.pb")
},
corruptionHandler = null
)
Add the following to your AndroidManifiest.xml:
<service
android:name=".TimestampUpdateService"
android:process=":my_process_id" />
The service periodically calls updateLastUpdateTime, which writes to the
datastore using updateData.
suspend fun updateLastUpdateTime() {
dataStore.updateData { time ->
time.copy(lastUpdateMillis = System.currentTimeMillis())
}
}
The app reads the value written by the service using the data flow:
fun timeFlow(): Flow<Long> = dataStore.data.map { time ->
time.lastUpdateMillis
}
Now, we can put all these functions together in a class called
MultiProcessDataStore and use it in an App.
Here is the service code:
class TimestampUpdateService : Service() {
val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
val multiProcessDataStore by lazy { MultiProcessDataStore(applicationContext) }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
serviceScope.launch {
while (true) {
multiProcessDataStore.updateLastUpdateTime()
delay(1000)
}
}
return START_NOT_STICKY
}
override fun onDestroy() {
super.onDestroy()
serviceScope.cancel()
}
}
And the app code:
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val multiProcessDataStore = remember(context) { MultiProcessDataStore(context) }
// Display time written by other process.
val lastUpdateTime by multiProcessDataStore.timeFlow()
.collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
text = "Last updated: $lastUpdateTime",
fontSize = 25.sp
)
DisposableEffect(context) {
val serviceIntent = Intent(context, TimestampUpdateService::class.java)
context.startService(serviceIntent)
onDispose {
context.stopService(serviceIntent)
}
}
You can use Hilt dependency injection so that your DataStore instance is unique per process:
@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
MultiProcessDataStoreFactory.create(...)
Handle file corruption
There are rare occasions where DataStore's persistent on-disk file could get
corrupted. By default, DataStore doesn't automatically recover from corruption,
and attempts to read from it will cause the system to throw a
CorruptionException.
DataStore offers a corruption handler API that can help you recover gracefully in such a scenario, and avoid throwing the exception. When configured, the corruption handler replaces the corrupted file with a new one containing a predefined default value.
To set up this handler, provide a corruptionHandler when creating the
DataStore instance in by dataStore or in the DataStoreFactory factory
method:
val dataStore: DataStore<Settings> = DataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = {
File("${context.filesDir.path}/myapp.preferences_pb")
},
corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)
Provide feedback
Share your feedback and ideas with us through these resources:
- Issue tracker:
- Report issues so we can fix bugs.
Additional resources
To learn more about Jetpack DataStore, see the following additional resources:
Samples
Blogs
Codelabs
Recommended for you
- Note: link text is displayed when JavaScript is off
- Load and display paged data
- LiveData overview
- Layouts and binding expressions