When do I need to care about multiple processes?
In some occasions, we might need to access our Jetpack DataStore from different processes. You might encounter that this is necessary, when you write something to your Preferences DataStore but a service or activity in another part of your app does not pick up the change. This is likely because the read and write happen in different processes and consistency is not guaranteed there.
How it was before
The Preferences DataStore does not support multiple processes, but we can easily adapt an existing one to instead be accessed by multiple processes, while keeping a similar flexibility.
Let’s say, you’ve wrapped your Preferences DataStore in a class like this:
private val USER_PREFERENCES_NAME = "user_preferences"
private val Context.dataStore by preferencesDataStore(
name = USER_PREFERENCES_NAME
)
@Singleton
class DataStoreManager @Inject constructor(@ApplicationContext context: Context) {
companion object {
val TOKEN = stringPreferencesKey("token")
val LOGIN_COUNT = intPreferencesKey("loginCount")
}
private val dataStore = context.dataStore
suspend fun saveToken(token: String) {
dataStore.edit { preferences ->
preferences[TOKEN] = token
}
}
val tokenFlow = dataStore.data.map { preferences ->
preferences[TOKEN] ?: ""
}
// and so on
}
In this, we’re using the companion object to store the keys that the DataStore uses to access our data.
How to fix it
In order to adapt the DataStore for multiple processes, we need to initialize it differently and take care of a small amount of data handling. We’ll utilize the MultiProcessDataStoreFactory
for this.
First, let’s use the keys from the companion object to model our new data class. It will represent what the DataStore can save. We’ll also mark it as @Serializable
.
@Serializable
data class AppSettings(
val token: String?,
val loginCount: Int
)
Dependencies
You’ll notice that there is no import available for the annotation. To fix this, we’ll need to update the build.gradle
files in the project as well as the module.
For the project build.gradle
:
plugins {
// ...
id "org.jetbrains.kotlin.android" version "1.9.0" apply false
id "org.jetbrains.kotlin.plugin.serialization" version "1.9.0"
}
And some more in the module build.gradle
:
plugins {
// ... all other project plugins
id "kotlin-parcelize"
id "kotlinx-serialization"
id "org.jetbrains.kotlin.plugin.serialization"
}
android {
// ... all other configuration
dependencies {
// .. all your other imports
implementation "androidx.datastore:datastore:1.1.1"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.4.1"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
}
}
If any peculiar errors show up like Cannot access 'Serializable': it is internal in 'kotlin.io'
, be sure that the project’s plugin versions match and that you’ve changed the correct files.
Adding the Serializer
The MultiProcessDataStoreFactory
requires us to specify a Serializer
. To keep it simple, we’re going to save our preferences in JSON. This will handle read/write operations and provide default values to the DataStore.
@Singleton
class SettingsSerializer @Inject constructor() : Serializer<AppSettings> {
override val defaultValue = AppSettings(
token = "",
loginCount = 0
)
override suspend fun readFrom(input: InputStream): AppSettings =
try {
Json.decodeFromString(input.readBytes().decodeToString())
} catch (serialization: SerializationException) {
throw CorruptionException("Unable to read Settings", serialization)
}
override suspend fun writeTo(t: AppSettings, output: OutputStream) {
withContext(Dispatchers.IO) {
output.write(
Json.encodeToString(t)
.encodeToByteArray()
)
}
}
}
Tying it together
As we also need to specify to which file our preferences get written to, we’ll add a function for this. This will use the default DataStore location for your application.
fun Context.appSettingsDataStoreFile(name: String): File =
this.dataStoreFile("$name.appsettings.json")
And change the following in our DataStoreManager:
- remove the preferencesKeys from the companion object
- initialize the dataStore with MultiProcessDataStoreFactory.create
- change save calls to use
dataStore.updateData
- change Flows to use the AppSettings class
private val DATASTORE_NAME = "user_preferences"
@Singleton
class DataStoreManager @Inject constructor(@ApplicationContext context: Context) {
companion object {
val TOKEN = stringPreferencesKey("token")
val LOGIN_COUNT = intPreferencesKey("loginCount")
}
private val dataStore: DataStore<AppSettings> = MultiProcessDataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = { context.appSettingsDataStoreFile(DATASTORE_NAME) }
)
suspend fun saveToken(token: String) {
dataStore.updateData {
it.copy(token = token)
}
}
val tokenFlow = dataStore.data.map { preferences ->
preferences.token
}
// and so on
}
And as a bonus, the code that already accesses your DataStore should not need any additional adaptations, so it becomes a plug-in change.