kotlinkotlin-coroutineskotlin-flowandroid-jetpack-datastore

Proper way to collect values from flow in android and coroutines


I am new to Kotlin Coroutines and flow. I am using datastore to store some boolean data and the only way to read data from datastore is using flow according to documentation.

I have this code in my ViewModel

fun getRestroProfileComplete(): Boolean {
        var result = false
        viewModelScope.launch {
            readRestroDetailsValue.collect { pref ->
                result = pref.restroProfileCompleted
            }
        }

       Log.d(ContentValues.TAG, "getRestroProfileComplete outside: $result")
        return result
    }

And in my fragments onCreateView method this code

if(restroAdminViewModel.getRestroProfileComplete()){
       
        Log.d(TAG, "Profile Completed")
        profileCompleted(true)
    }else{
        Log.d(TAG, "Profile not completed ")
        profileCompleted(false)
    }

Sometimes I get the data from datastore other times it is always false.

I know that the getRestroProfileComplete method function does not wait for the launch code block to complete and gives a default false results.

what would be the best way to do this?


Solution

  • You are launching an asynchronous coroutine (with launch) and then, without waiting for it to do its job, you return whatever is in the result variable. Sometimes the result will be set, sometimes it won't, depending on how long datastore takes to load the preference.

    If you need the preference value in a non-coroutine context then you must use runBlocking, like this:

    fun getRestroProfileComplete(): Boolean {
            val result = runBlocking {
                readRestroDetailsValue.collect { pref ->
                    pref.restroProfileCompleted
                }
            }
    
           Log.d(ContentValues.TAG, "getRestroProfileComplete outside: $result")
            return result
        }
    

    However, that's not a good thing to do at all! You should expose a flow and consume that flow from the fragment, so you don't block the main thread and your UI can react to preference changes.

    fun getRestroProfileComplete(): Flow<Boolean> {
        return readRestroDetailsValue.map { pref ->
            pref.restroProfileCompleted
        }
    }
    

    and in your fragment's onCreateView you launch a coroutine:

    viewLifecycleOwner.lifecycleScope.launchWhenStarted {
        restroAdminViewModel.getRestroProfileComplete().collect { c ->
            if(c) {
                Log.d(TAG, "Profile Completed")
                profileCompleted(true)
            } else {
                Log.d(TAG, "Profile not completed ")
                profileCompleted(false)
            }
    }
    
    

    (This will keep monitoring the preference, you can do other things with the flow, like get the first true element with .first { it })