0

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.

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

2 Comments

Oh, I see, the flow only gives me changes, not entries for the key. I don't want to detect any changes here, I simply want to check whether there is something stored for that key or not. Would first() not query changes too and indefinetely suspend? Or does first query the first value for the key instead of changes to that entry?
first() returns the first value emitted by the flow. The data store needs a while to initialize, but when it is ready it emits all preferences that are stored, so there will always be at least one value in the flow which you can obtain with first(). It is either the previously stored Set or null when there wasn't stored anything yet.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.