'Android Preferences DataStore Flow Doesn't Emit Same Value

Just testing Preferences DataStore and found out that the provided Flow output won't emit same value, my setup as followed:

DataStore Util Class:

object DataStore {
    private val Context.settings by preferencesDataStore("settings") 

    suspend fun saveBoolean(context: Context, keyResId: Int, value: Boolean) {
        val key = booleanPreferencesKey(context.getString(keyResId))

        context.settings.edit {
            it[key] = value
        }
    }

    fun getBooleanFlow(context: Context, keyResId: Int, defaultValueResId: Int): Flow<Boolean> {
        val key = booleanPreferencesKey(context.getString(keyResId))
        val defaultValue = context.resources.getBoolean(defaultValueResId)

        return context.settings.data.map {
            it[key] ?: defaultValue
        }
    }
}

ViewModel Class:

class FirstViewModel(application: Application) : AndroidViewModel(application) {
    private val uiScope = viewModelScope

    val isUpdateAvailable = DataStore.getBooleanFlow(
        getApplication(), R.string.is_update_available_key, R.bool.is_update_available_default
    )

    fun updateIsUpdateAvailable() = uiScope.launch {
        DataStore.saveBoolean(getApplication(), R.string.is_update_available_key, true)  //<- always set to true
    }
}

Fragment Class:

class FirstFragment : Fragment() {
    private lateinit var binding: FragmentFirstBinding
    private lateinit var viewModel: FirstViewModel

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_first, container, false)
        viewModel = ViewModelProvider(this).get(FirstViewModel::class.java)

        lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.isUpdateAvailable.collect {
                    Log.v("xxx", "isUpdateAvailable: $it")
                }
            }
        }

        binding.saveButton.setOnClickListener {
            viewModel.updateIsUpdateAvailable()
        }

        return binding.root
    }
}

Since I'm saving true each time, and the Log just shows once, which means the Flow doesn't emit same value. Am I correct? Is this intentional behavior?



Solution 1:[1]

I made a workaround for this.

  1. You store the previous value of the properties in a variable
private val dataStore = context.dataStore  
private var previousConfig: AppConfig = AppConfig.default() //Any default/emptyvalue

     val preferencesFlow = dataStore.data
        .catch { exception ->
            Log.e(TAG, "Error reading preferences", exception)
            emit(emptyPreferences())
        }
        .map { preferences ->
            val result = preferences[CONFIG]?.let {
                moshiAdapter.fromJson(it) ?: AppConfig.default()
            } ?: AppConfig.default()
            previousConfig = result //here you update your previous value
            return@map result
        }
  1. Each time you want to update data, you first compare the new value with the previous one.
  2. And if data is the same, before pushing your actual new data, you push some fake data which is guaranteed not same as the new data. This can be also achieved by introducing some extra variable in your configuration data class. This variable will serve exlusively to make equal() return false when comparing data class instances. Then you can push your new data (see code below).
suspend fun updateConfig(appConfig: AppConfig) {
        if (appConfig == previousConfig) {
            dataStore
                .edit { preferences -> // pushing fake data
                    preferences[PreferencesKeys.CONFIG] = moshiAdapter
.toJson(appConfig.copy(equalityIndicator++))
                }
        }
        dataStore
            .edit { preferences -> //now pushing the real valid new data
                preferences[PreferencesKeys.CONFIG] = moshiAdapter.toJson(appConfig)
            }
    }

It's an ugly workaround but it works. I hope Google will offer us some better way sometime.

Solution 2:[2]

In the case of WinFormApp in your Program.cs Class you can use public Static property to send it in other forms....program.cs is a static class where the Main function begins :

    static class Program
    {
        public static string userName = "";
    }

after logged in :

         if (txtUser.Text.Equals(dr[0].ToString()) && txtPassword.Text.Equals(dr[1].ToString()))
        {

            this.Close();
            th = new Thread(openMainForm);
            th.SetApartmentState(ApartmentState.STA);
            th.Start();
            Program.userName=txtUser.Text;//this make the variable global 
        }

and to use it in another form you should use it like this for Ex :

  private void Form_Load(object sender, EventArgs e)
        {
            lbl_welcome.Text += " " + Program.userName;
          
           
        }

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2