Repository: thunderbird/thunderbird-android (language: kotlin)

Issue:
Show/Hide star count does not update immediately
### Checklist

- [x] I have used the search function to see if someone else has already submitted the same bug report.
- [x] I will describe the problem with as much detail as possible.

### App

Thunderbird for Android

### App version

10.0-SNAPSHOT

### Where did you get the app from?

None

### Android version

Android 12

### Device model

_No response_

### Steps to reproduce

1. Open The Application.
2. Click on Side Menu
3. Click on` Setting Gear` at the bottom of menu
4. Click on `General Settings`-> `Display`
5.  Scroll to `Account List` Section
6. Toggle `Show starred count`
7. Navigate back to Home screen
8. In side menu change is not reflected.

If you close the app and open again, change is reflected.

### Expected behavior

Toggle `Show starred count` should reflect immediately without re-starting the app.

### Actual behavior

Toggle `Show starred count` does not reflect immediately without re-starting the app.


### Logs

_No response_


Discussion hints:
Hi! @wmontwe  I’ve been investigating the issue and here are my findings:

1.The value for show_starred_count (and other similar settings) is being updated directly via K9.isShowStarredCount in GeneralSettingsDataStore.
2.However, after the change, there doesn't seem to be any UI refresh or reactive update to reflect the change.

Before I proceed with the fix, I wanted to confirm the best approach:

1.Should I trigger a UI refresh after changing the setting (e.g., by using a StateFlow or other mechanisms to propagate the change)?
2.Or is there another approach you’d recommend to ensure the UI reflects the updated settings after they’ve been changed?

Any guidance would be really helpful before I start implementing the fix. Thanks!

Pull request: Add settings change notifier
This introduces a new settings change notification system to the project. The main changes include adding a publish/subscribe pattern with a broker and publisher for settings changes, updating existing classes to use this new system, and adding tests for the new functionality.

The drawer is now immediatelly updated whenever a setting is changed.

Resolves #8765





Test command: ./gradlew :legacy:preferences:testDebugUnitTest --tests "app.k9mail.legacy.preferences.DefaultSettingsChangeBrokerTest"

Fix patch:
diff --git a/legacy/core/src/main/java/com/fsck/k9/preferences/KoinModule.kt b/legacy/core/src/main/java/com/fsck/k9/preferences/KoinModule.kt
index 41de59f552c..0000661227f 100644
--- a/legacy/core/src/main/java/com/fsck/k9/preferences/KoinModule.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/preferences/KoinModule.kt
@@ -1,10 +1,14 @@
 package com.fsck.k9.preferences
 
 import app.k9mail.legacy.account.AccountManager
+import app.k9mail.legacy.preferences.DefaultSettingsChangeBroker
 import app.k9mail.legacy.preferences.GeneralSettingsManager
+import app.k9mail.legacy.preferences.SettingsChangeBroker
+import app.k9mail.legacy.preferences.SettingsChangePublisher
 import com.fsck.k9.Preferences
 import org.koin.core.qualifier.named
 import org.koin.dsl.bind
+import org.koin.dsl.binds
 import org.koin.dsl.module
 
 val preferencesModule = module {
@@ -24,12 +28,14 @@ val preferencesModule = module {
         RealGeneralSettingsManager(
             preferences = get(),
             coroutineScope = get(named("AppCoroutineScope")),
+            changePublisher = get(),
         )
     } bind GeneralSettingsManager::class
     single {
         RealDrawerConfigManager(
             preferences = get(),
             coroutineScope = get(named("AppCoroutineScope")),
+            changeBroker = get(),
         )
     } bind DrawerConfigManager::class
 
@@ -77,4 +83,7 @@ val preferencesModule = module {
             unifiedInboxConfigurator = get(),
         )
     }
+
+    single { DefaultSettingsChangeBroker() }
+        .binds(arrayOf(SettingsChangePublisher::class, SettingsChangeBroker::class))
 }
diff --git a/legacy/core/src/main/java/com/fsck/k9/preferences/RealDrawerConfigManager.kt b/legacy/core/src/main/java/com/fsck/k9/preferences/RealDrawerConfigManager.kt
index 33f18e336df..48269d118ab 100644
--- a/legacy/core/src/main/java/com/fsck/k9/preferences/RealDrawerConfigManager.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/preferences/RealDrawerConfigManager.kt
@@ -1,20 +1,32 @@
 package com.fsck.k9.preferences
 
 import app.k9mail.feature.navigation.drawer.NavigationDrawerExternalContract.DrawerConfig
+import app.k9mail.legacy.preferences.SettingsChangeBroker
+import app.k9mail.legacy.preferences.SettingsChangeSubscriber
 import com.fsck.k9.K9
 import com.fsck.k9.Preferences
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.launch
 
 internal class RealDrawerConfigManager(
     private val preferences: Preferences,
     private val coroutineScope: CoroutineScope,
+    private val changeBroker: SettingsChangeBroker,
 ) : DrawerConfigManager {
     private val drawerConfigFlow = MutableSharedFlow<DrawerConfig>(replay = 1)
-    private var drawerConfig: DrawerConfig? = null
+
+    init {
+        coroutineScope.launch {
+            asSettingsFlow().collect { config ->
+                drawerConfigFlow.emit(config)
+            }
+        }
+    }
 
     override fun save(config: DrawerConfig) {
         saveDrawerConfig(config)
@@ -22,15 +34,11 @@ internal class RealDrawerConfigManager(
     }
 
     private fun loadDrawerConfig(): DrawerConfig {
-        val drawerConfig = DrawerConfig(
+        return DrawerConfig(
             showAccountSelector = K9.isShowAccountSelector,
             showStarredCount = K9.isShowStarredCount,
             showUnifiedFolders = K9.isShowUnifiedInbox,
         )
-
-        updateDrawerConfigFlow(drawerConfig)
-
-        return drawerConfig
     }
 
     private fun updateDrawerConfigFlow(config: DrawerConfig) {
@@ -41,14 +49,31 @@ internal class RealDrawerConfigManager(
 
     @Synchronized
     override fun getConfig(): DrawerConfig {
-        return drawerConfig ?: loadDrawerConfig().also { drawerConfig = it }
+        return loadDrawerConfig().also {
+            updateDrawerConfigFlow(it)
+        }
     }
 
     override fun getConfigFlow(): Flow<DrawerConfig> {
-        getConfig()
         return drawerConfigFlow.distinctUntilChanged()
     }
 
+    private fun asSettingsFlow(): Flow<DrawerConfig> {
+        return callbackFlow {
+            send(loadDrawerConfig())
+
+            val subscriber = SettingsChangeSubscriber {
+                drawerConfigFlow.tryEmit(loadDrawerConfig())
+            }
+
+            changeBroker.subscribe(subscriber)
+
+            awaitClose {
+                changeBroker.unsubscribe(subscriber)
+            }
+        }
+    }
+
     @Synchronized
     private fun saveDrawerConfig(config: DrawerConfig) {
         val editor = preferences.createStorageEditor()
diff --git a/legacy/core/src/main/java/com/fsck/k9/preferences/RealGeneralSettingsManager.kt b/legacy/core/src/main/java/com/fsck/k9/preferences/RealGeneralSettingsManager.kt
index ddedaac37e4..ef7ae99d171 100644
--- a/legacy/core/src/main/java/com/fsck/k9/preferences/RealGeneralSettingsManager.kt
+++ b/legacy/core/src/main/java/com/fsck/k9/preferences/RealGeneralSettingsManager.kt
@@ -6,6 +6,7 @@ import app.k9mail.legacy.preferences.AppTheme
 import app.k9mail.legacy.preferences.BackgroundSync
 import app.k9mail.legacy.preferences.GeneralSettings
 import app.k9mail.legacy.preferences.GeneralSettingsManager
+import app.k9mail.legacy.preferences.SettingsChangePublisher
 import app.k9mail.legacy.preferences.SubTheme
 import com.fsck.k9.K9
 import com.fsck.k9.Preferences
@@ -30,6 +31,7 @@ import timber.log.Timber
 internal class RealGeneralSettingsManager(
     private val preferences: Preferences,
     private val coroutineScope: CoroutineScope,
+    private val changePublisher: SettingsChangePublisher,
     private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO,
 ) : GeneralSettingsManager {
     private val settingsFlow = MutableSharedFlow<GeneralSettings>(replay = 1)
@@ -88,6 +90,8 @@ internal class RealGeneralSettingsManager(
         K9.save(editor)
         writeSettings(editor, settings)
         editor.commit()
+
+        changePublisher.publish()
     }
 
     @Synchronized
diff --git a/legacy/preferences/src/main/java/app/k9mail/legacy/preferences/DefaultSettingsChangeBroker.kt b/legacy/preferences/src/main/java/app/k9mail/legacy/preferences/DefaultSettingsChangeBroker.kt
new file mode 100644
index 00000000000..68e6753070f
--- /dev/null
+++ b/legacy/preferences/src/main/java/app/k9mail/legacy/preferences/DefaultSettingsChangeBroker.kt
@@ -0,0 +1,28 @@
+package app.k9mail.legacy.preferences
+
+class DefaultSettingsChangeBroker(
+    private val subscribers: MutableSet<SettingsChangeSubscriber> = mutableSetOf(),
+) : SettingsChangeBroker, SettingsChangePublisher {
+
+    private val lock = Any()
+
+    override fun subscribe(subscriber: SettingsChangeSubscriber) {
+        synchronized(lock) {
+            subscribers.add(subscriber)
+        }
+    }
+
+    override fun unsubscribe(subscriber: SettingsChangeSubscriber) {
+        synchronized(lock) {
+            subscribers.remove(subscriber)
+        }
+    }
+
+    override fun publish() {
+        val currentSubscribers = synchronized(lock) { HashSet(subscribers) }
+
+        for (subscriber in currentSubscribers) {
+            subscriber.receive()
+        }
+    }
+}
diff --git a/legacy/preferences/src/main/java/app/k9mail/legacy/preferences/SettingsChangeBroker.kt b/legacy/preferences/src/main/java/app/k9mail/legacy/preferences/SettingsChangeBroker.kt
new file mode 100644
index 00000000000..d831cee2444
--- /dev/null
+++ b/legacy/preferences/src/main/java/app/k9mail/legacy/preferences/SettingsChangeBroker.kt
@@ -0,0 +1,22 @@
+package app.k9mail.legacy.preferences
+
+/**
+ * Broker to manage subscribers and notify them about changes in the settings, when the
+ * [SettingsChangePublisher] publishes a change.
+ */
+interface SettingsChangeBroker {
+
+    /**
+     * Subscribe to settings changes.
+     *
+     * @param subscriber The subscriber to be notified about settings changes.
+     */
+    fun subscribe(subscriber: SettingsChangeSubscriber)
+
+    /**
+     * Unsubscribe from settings changes.
+     *
+     * @param subscriber The subscriber that no longer wants to be notified about settings changes.
+     */
+    fun unsubscribe(subscriber: SettingsChangeSubscriber)
+}
diff --git a/legacy/preferences/src/main/java/app/k9mail/legacy/preferences/SettingsChangePublisher.kt b/legacy/preferences/src/main/java/app/k9mail/legacy/preferences/SettingsChangePublisher.kt
new file mode 100644
index 00000000000..9c859269c56
--- /dev/null
+++ b/legacy/preferences/src/main/java/app/k9mail/legacy/preferences/SettingsChangePublisher.kt
@@ -0,0 +1,12 @@
+package app.k9mail.legacy.preferences
+
+/**
+ * Publishes changes in the settings.
+ */
+interface SettingsChangePublisher {
+
+    /**
+     * Publish a change in the settings.
+     */
+    fun publish()
+}
diff --git a/legacy/preferences/src/main/java/app/k9mail/legacy/preferences/SettingsChangeSubscriber.kt b/legacy/preferences/src/main/java/app/k9mail/legacy/preferences/SettingsChangeSubscriber.kt
new file mode 100644
index 00000000000..7b3493ce8eb
--- /dev/null
+++ b/legacy/preferences/src/main/java/app/k9mail/legacy/preferences/SettingsChangeSubscriber.kt
@@ -0,0 +1,12 @@
+package app.k9mail.legacy.preferences
+
+/**
+ * Subscribe to be notified about changes in the settings.
+ */
+fun interface SettingsChangeSubscriber {
+
+    /**
+     * Called when settings change.
+     */
+    fun receive()
+}


Test patch:
diff --git a/legacy/preferences/src/test/java/app/k9mail/legacy/preferences/DefaultSettingsChangeBrokerTest.kt b/legacy/preferences/src/test/java/app/k9mail/legacy/preferences/DefaultSettingsChangeBrokerTest.kt
new file mode 100644
index 00000000000..99399bd450c
--- /dev/null
+++ b/legacy/preferences/src/test/java/app/k9mail/legacy/preferences/DefaultSettingsChangeBrokerTest.kt
@@ -0,0 +1,49 @@
+package app.k9mail.legacy.preferences
+
+import assertk.assertThat
+import assertk.assertions.contains
+import assertk.assertions.doesNotContain
+import assertk.assertions.isEqualTo
+import kotlin.test.Test
+
+class DefaultSettingsChangeBrokerTest {
+
+    @Test
+    fun `subscribe should add subscriber`() {
+        val subscriber = SettingsChangeSubscriber { }
+        val subscribers = mutableSetOf<SettingsChangeSubscriber>()
+        val broker = DefaultSettingsChangeBroker(subscribers)
+
+        broker.subscribe(subscriber)
+
+        assertThat(subscribers.size).isEqualTo(1)
+        assertThat(subscribers).contains(subscriber)
+    }
+
+    @Test
+    fun `unsubscribe should remove subscriber`() {
+        val subscriber = SettingsChangeSubscriber { }
+        val subscribers = mutableSetOf<SettingsChangeSubscriber>(subscriber)
+        val broker = DefaultSettingsChangeBroker(subscribers)
+
+        broker.unsubscribe(subscriber)
+
+        assertThat(subscribers.size).isEqualTo(0)
+        assertThat(subscribers).doesNotContain(subscriber)
+    }
+
+    @Test
+    fun `publish should notify subscribers`() {
+        var received = false
+        val subscriber = SettingsChangeSubscriber { received = true }
+        var receivedOther = false
+        val otherSubscriber = SettingsChangeSubscriber { receivedOther = true }
+        val subscribers = mutableSetOf<SettingsChangeSubscriber>(subscriber, otherSubscriber)
+        val broker = DefaultSettingsChangeBroker(subscribers)
+
+        broker.publish()
+
+        assertThat(received).isEqualTo(true)
+        assertThat(receivedOther).isEqualTo(true)
+    }
+}