I am trying to persist some simple state of my android app using preferences with Jetpack DataStore. I want to fill the value of a key with a default value if there was nothing stored yet. However when I try to find out if there is a value for the key present the library call completely stops the execution of my whole function.
package com.example.spielwiese
import android.content.Context
import android.os.Bundle
import android.util.Log
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.stringSetPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.count
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
val Context.dataStore: DataStore<Preferences> by preferencesDataStore("settings")
class MainActivity : AppCompatActivity() {
private val scopeIo = CoroutineScope(Job() + Dispatchers.IO)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
scopeIo.launch { checkPreferences() }
}
private suspend fun checkPreferences() = withContext(Dispatchers.IO) {
Log.d("HUGELBUGEL", "Before dataStore access.")
val nothingThere = dataStore.data.map { preferences ->
preferences[stringSetPreferencesKey("myKey")]
}.count() == 0
// This log doesn't come out
Log.d("HUGELBUGEL", "After dataStore access: $nothingThere")
// if (nothingThere) putDefaultValueIntoDataStore()
}
}
The first log comes out at runtime, but the second doesn't. I tried to debug it, but when it comes to the call of count()
it just never comes back. I don't even know what's happending here. There is no exception (I tried to catch one), no nothing, it just stops execution there. Maybe I don't understand kotlin coroutines well enough, but even then: Why does it just stop execution and doesn't throw an exception there? Did I use the DataStore wrong? What can I do to fix it?
My build.gradly.kts
for the app module looks like this:
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.example.spielwiese"
compileSdk = 34
defaultConfig {
applicationId = "com.example.spielwiese"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.datastore.preferences)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
It's all default stuff, that AndroidStudio creates for you, when creating a new Project, just added the nessecary code for the question here.
The issue is that you call count
on the Flow, not on the Set. You do not count how many entries there are in the stored Set that you saved under the key myKey
, you count how many changes there are being made to that Set.
What you call count
on has the type Flow<Set<String>?>
, i.e. a flow that contains a set of Strings. The flow is there to supply you with a stream of updates: Whenever the set is changed the flow emits a new, updated version of the Set. With your code you try to count the number of updates. Since the final count can only be determined when the flow completes, the code waits here until all (future) updates or done. Since the flow cannot know if there will be more updates in the future it will keep running and never completes, so your attempt to count
will never finish too, suspending your function indefinitely.
If you want to count the elements of the Set instead you first need to collect the flow:
dataStore.data.map { preferences ->
preferences[stringSetPreferencesKey("myKey")]
}.collect {
val nothingThere = it.count() == 0
Log.d("HUGELBUGEL", "After dataStore access: $nothingThere")
}
The collect
lambda will now be executed whenever the Set changes.
It seems you just to want to execute once, though, to initialize your set. In that case you can abort the flow collection after the first value is retrieved. There is a handy function that does that for your, called first
:
val nothingThere = dataStore.data.map { preferences ->
preferences[stringSetPreferencesKey("myKey")]
}.first().count() == 0
This still leaves one issue left: The preference may simply not exist yet and the flow's value returns null
. So what you actually want to use will probably be something like this:
val nothingThere = dataStore.data.map { preferences ->
preferences[stringSetPreferencesKey("myKey")]
}.first()?.isEmpty() ?: true
If you only want to initialize the set when it doesn't exist (and not when it exists but is empty), simply test .first() == null
instead.
Final note: the datastore implementation uses the IO dispatcher internally so you should not switch to Dispatchers.IO
here yourself.