Search code examples
kotlinkotlin-flowandroid-jetpack-datastore

DataStore preferences. Consume with collect and map{} and consume with first()


I'm goin crazy reading and testing about Flows and Datastore Preferences.

I've seen many examples that consumes the preferences from Datastore using two ways:

  1. With collect and map:
override fun getFlowBoolean(key: String): Flow<Boolean> {
        return settingsDataStore.data
            .catch {
                if (it is IOException) emit(emptyPreferences())
                else it.printStackTrace()
            }
            .map { preferences ->
                val preferenceKey = booleanPreferencesKey(key)
                preferences[preferenceKey] ?: true
            }
}
  1. with first():
    override suspend fun getBoolean(key: String): Boolean? {
        val preferenceKey = booleanPreferencesKey(key)
        return settingsDataStore.data.first()[preferenceKey]
    }

I understand that, to display values in a form on UI, it's better get static/inmutable data, the current state of a specific preference. For example: a switch state (checked or not).

But i don't understand why first() returns the current value of the Flow, and not the first, the initial value. The first() function doc says:

The terminal operator that returns the first element emitted by the flow and then cancels flow's collection. Throws NoSuchElementException if the flow was empty.

Why?

  1. I'm receiving the current value of the preference in the preferences flow, and not the first, the original value.
  2. Other consumers can still go on collecting values. The doc says first() cancels the flow...

Thanks in advance

Update: Thanks to Edric. I've tested it right and yes, i'm receiving the original value when the app was launched. Then, how i could get the current static value for using it in fragments onViewCreated to set the initial state of my views?

This is my ViewModel:

    //dynamic flow values to collect
    val displayIdFlow = settingsRepository.getFlowBoolean(DISPLAY_ID_PREFERENCE)
    val sortFieldFlow = settingsRepository.getFlowString(SORT_FIELD_PREFERENCE)
    val sortAscFlow = settingsRepository.getFlowBoolean(SORT_ASC_PREFERENCE)
    //Static values with first()
    val displayId = runBlocking { settingsRepository.getBoolean(DISPLAY_ID_PREFERENCE) }
    val sortField = runBlocking { settingsRepository.getString(SORT_FIELD_PREFERENCE) }
    val sortAsc = runBlocking { settingsRepository.getBoolean(SORT_ASC_PREFERENCE) }

    fun setSortField(modulesSortFields: ModulesSortFields){
        viewModelScope.launch(Dispatchers.IO) {
            settingsRepository.putString(SORT_FIELD_PREFERENCE,modulesSortFields.fieldName)
            updateAllModulesData()
        }

    }

    fun setIsSortAsc(sortAsc: Boolean){
        viewModelScope.launch(Dispatchers.IO) {
            settingsRepository.putBoolean(SORT_ASC_PREFERENCE,sortAsc)
            updateAllModulesData()
        }
    }

    fun setDisplayIdSetting(displayId: Boolean){
        viewModelScope.launch(Dispatchers.IO) {
            settingsRepository.putBoolean(DISPLAY_ID_PREFERENCE, displayId)
        }
    }

And this is my fragment (displayed with Navigation Component):

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)


        //getSettingsStateWithCollect()
        getSettingsStateWithFirst()


        //Al activar/desactivar el switch, actualizamos el valor del datastore
        binding.displayIdSwitch.setOnCheckedChangeListener { buttonView, isChecked ->
            viewModel.setDisplayIdSetting(isChecked)
        }

        //Al cambiar el check marcado del radiogroup de campo de ordenación, actualizamos el datastore
        binding.radioGroup.setOnCheckedChangeListener{ _, _ ->
            val sortFieldChecked: ModulesSortFields = when{
                binding.rbtSortName.isChecked -> ModulesSortFields.NAME
                binding.rbtSortCredits.isChecked -> ModulesSortFields.CREDITS
                else -> ModulesSortFields.ID
            }
            viewModel.setSortField(sortFieldChecked)
        }
        binding.sortAscSwitch.setOnCheckedChangeListener { _, _ ->
            viewModel.setIsSortAsc(binding.sortAscSwitch.isChecked)
        }
    }

    private fun getSettingsStateWithFirst(){
        binding.displayIdSwitch.isChecked = viewModel.displayId

        val sortField = viewModel.sortField
        when (sortField) { //Usamos el ID como campo de ordenación por defecto
            ModulesSortFields.NAME.fieldName -> binding.rbtSortName.isChecked = true
            ModulesSortFields.CREDITS.fieldName -> binding.rbtSortCredits.isChecked = true
            else -> binding.rbtSortId.isChecked = true
        }

        binding.sortAscSwitch.isChecked = viewModel.sortAsc
    }

Solution

    A Flow is cold. It starts emitting values when you start collecting it. In the case of a Flow from DataStore, it is set up so every time you begin collecting it, it first emits the value currently saved in the store, and then continues emitting new values if the contents of the store change while you are still collecting the flow.

    There is no history of values kept. The Flow doesn’t know what the original value in the store was. It only knows the current value.

    When you call first() on a flow, it is the same as calling collect() on it, waiting for the first value emitted, then returning that value and cancelling any further collection of that flow.

    So first() will always give you the current value saved in the store.

    2)

    Since flows are cold you can collect them any number of times in parallel at different places in your app and each spot it’s getting collected will be starting the collection behavior from scratch. Cancelling a collect call in one place has zero effect on other collect calls that are currently ongoing.

    Updated question:

    The correct way to get the current value saved in the store at the time you are requesting it is to call first(). If that is not the behavior you’re seeing, maybe your code that is changing values in the store is not behaving as intended.