Skip to content

Commit bf86a48

Browse files
frettclaude
andcommitted
Wire tool order sync into DashboardPresenter and ToolsPresenter via SyncTaskRegistry
DashboardPresenter now registers its syncData task with the SyncTaskRegistry and exposes the registry on CircuitContext so nested presenters can participate. ToolsPresenter registers a syncToolOrder task against that registry, replacing the previous direct rememberSyncTracker usage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3c8a988 commit bf86a48

4 files changed

Lines changed: 163 additions & 18 deletions

File tree

app/src/main/kotlin/org/cru/godtools/ui/dashboard/DashboardPresenter.kt

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package org.cru.godtools.ui.dashboard
22

33
import androidx.compose.material3.SnackbarHostState
44
import androidx.compose.runtime.Composable
5+
import androidx.compose.runtime.DisposableEffect
56
import androidx.compose.runtime.collectAsState
67
import androidx.compose.runtime.remember
78
import com.slack.circuit.codegen.annotations.CircuitInject
89
import com.slack.circuit.foundation.NavEvent
910
import com.slack.circuit.foundation.onNavEvent
11+
import com.slack.circuit.runtime.CircuitContext
1012
import com.slack.circuit.runtime.CircuitUiEvent
1113
import com.slack.circuit.runtime.CircuitUiState
1214
import com.slack.circuit.runtime.Navigator
@@ -18,18 +20,20 @@ import dagger.hilt.components.SingletonComponent
1820
import kotlinx.coroutines.coroutineScope
1921
import kotlinx.coroutines.launch
2022
import org.ccci.gto.android.common.sync.SyncTracker
21-
import org.ccci.gto.android.common.sync.rememberSyncTracker
2223
import org.cru.godtools.base.ui.circuit.screen.dashboard.DashboardScreen
2324
import org.cru.godtools.base.ui.circuit.screen.dashboard.page.DashboardPage
2425
import org.cru.godtools.base.ui.circuit.screen.dashboard.page.HomeScreen
2526
import org.cru.godtools.sync.GodToolsSyncService
2627
import org.cru.godtools.ui.dashboard.DashboardPresenter.UiState
28+
import org.cru.godtools.ui.dashboard.SyncTaskRegistry.Companion.rememberSyncRegistry
29+
import org.cru.godtools.ui.dashboard.SyncTaskRegistry.Companion.syncTaskRegistry
2730
import org.cru.godtools.ui.drawer.DrawerMenuPresenter
2831
import org.cru.godtools.ui.drawer.DrawerMenuScreen
2932

3033
class DashboardPresenter @AssistedInject internal constructor(
3134
private val drawerMenuPresenter: DrawerMenuPresenter,
3235
private val syncService: GodToolsSyncService,
36+
@Assisted private val circuitContext: CircuitContext,
3337
@Assisted private val navigator: Navigator,
3438
@Assisted private val screen: DashboardScreen,
3539
) : Presenter<UiState> {
@@ -49,16 +53,25 @@ class DashboardPresenter @AssistedInject internal constructor(
4953

5054
@Composable
5155
override fun present(): UiState {
52-
val syncTracker = rememberSyncTracker { it.syncData() }
56+
val syncRegistry = rememberSyncRegistry()
57+
DisposableEffect(syncRegistry) {
58+
circuitContext.syncTaskRegistry = syncRegistry
59+
val id = syncRegistry.registerSyncTask { syncData(it) }
60+
61+
onDispose {
62+
circuitContext.syncTaskRegistry = null
63+
syncRegistry.unregisterSyncTask(id)
64+
}
65+
}
5366

5467
return UiState(
5568
drawerState = drawerMenuPresenter.present(),
56-
isSyncing = syncTracker.isSyncing.collectAsState().value,
69+
isSyncing = syncRegistry.syncTracker.isSyncing.collectAsState().value,
5770
initialPage = screen.initialPage,
5871
snackbarState = remember { SnackbarHostState() },
5972
) {
6073
when (it) {
61-
UiEvent.TriggerSync -> syncTracker.syncData(force = true)
74+
UiEvent.TriggerSync -> syncRegistry.triggerSyncTasks(force = true)
6275
is UiEvent.NestedNavEvent -> navigator.onNavEvent(it.event)
6376
}
6477
}
@@ -81,6 +94,6 @@ class DashboardPresenter @AssistedInject internal constructor(
8194
@AssistedFactory
8295
@CircuitInject(DashboardScreen::class, SingletonComponent::class)
8396
interface Factory {
84-
fun create(navigator: Navigator, screen: DashboardScreen): DashboardPresenter
97+
fun create(context: CircuitContext, navigator: Navigator, screen: DashboardScreen): DashboardPresenter
8598
}
8699
}

app/src/main/kotlin/org/cru/godtools/ui/dashboard/tools/ToolsPresenter.kt

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.cru.godtools.ui.dashboard.tools
22

33
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.DisposableEffect
45
import androidx.compose.runtime.collectAsState
56
import androidx.compose.runtime.getValue
67
import androidx.compose.runtime.key
@@ -11,6 +12,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
1112
import androidx.compose.runtime.setValue
1213
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
1314
import com.slack.circuit.codegen.annotations.CircuitInject
15+
import com.slack.circuit.runtime.CircuitContext
1416
import com.slack.circuit.runtime.CircuitUiEvent
1517
import com.slack.circuit.runtime.CircuitUiState
1618
import com.slack.circuit.runtime.Navigator
@@ -19,19 +21,25 @@ import dagger.assisted.Assisted
1921
import dagger.assisted.AssistedFactory
2022
import dagger.assisted.AssistedInject
2123
import dagger.hilt.components.SingletonComponent
24+
import java.util.Locale
25+
import kotlinx.coroutines.flow.first
2226
import kotlinx.coroutines.flow.map
27+
import org.ccci.gto.android.common.sync.SyncTracker
2328
import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent
2429
import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.ACTION_OPEN_TOOL_DETAILS
2530
import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.SOURCE_ALL_TOOLS
2631
import org.cru.godtools.analytics.model.OpenAnalyticsActionEvent.Companion.SOURCE_SPOTLIGHT
2732
import org.cru.godtools.base.CONFIG_UI_DASHBOARD_PERSONALIZATION_ENABLED
33+
import org.cru.godtools.base.Settings
2834
import org.cru.godtools.base.ui.circuit.screen.dashboard.page.ToolsScreen
2935
import org.cru.godtools.db.repository.ToolsRepository
3036
import org.cru.godtools.model.Language
3137
import org.cru.godtools.model.Tool
38+
import org.cru.godtools.sync.GodToolsSyncService
3239
import org.cru.godtools.ui.banner.Banner
3340
import org.cru.godtools.ui.banner.BannerPresenter
3441
import org.cru.godtools.ui.banner.favoritetools.FavoriteToolsBannerPresenter
42+
import org.cru.godtools.ui.dashboard.SyncTaskRegistry.Companion.syncTaskRegistry
3543
import org.cru.godtools.ui.dashboard.tools.ToolFiltersStateProducer.Filters
3644
import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState
3745
import org.cru.godtools.ui.dashboard.tools.ToolsPresenter.UiState.Mode
@@ -43,11 +51,14 @@ import org.greenrobot.eventbus.EventBus
4351
class ToolsPresenter @AssistedInject internal constructor(
4452
private val eventBus: EventBus,
4553
private val remoteConfig: FirebaseRemoteConfig,
54+
private val settings: Settings,
4655
private val toolCardPresenter: ToolCardPresenter,
4756
private val toolsRepository: ToolsRepository,
4857
private val favoriteToolsBannerPresenter: BannerPresenter<FavoriteToolsBannerPresenter.UiState>,
4958
private val filteredToolsFlowProducer: FilteredToolsFlowProducer,
5059
private val toolFiltersStateProducer: ToolFiltersStateProducer,
60+
private val syncService: GodToolsSyncService,
61+
@Assisted private val circuitContext: CircuitContext,
5162
@Assisted private val navigator: Navigator,
5263
) : Presenter<UiState> {
5364
// region UiState / UiEvent
@@ -83,6 +94,8 @@ class ToolsPresenter @AssistedInject internal constructor(
8394
val filters = toolFiltersStateProducer.produce(mode)
8495
val selectedLocale by rememberUpdatedState(filters.languageFilter.selectedItem?.code)
8596

97+
RegisterSyncTask(selectedLocale)
98+
8699
val eventSink: (UiEvent) -> Unit = remember {
87100
{
88101
when (it) {
@@ -124,6 +137,16 @@ class ToolsPresenter @AssistedInject internal constructor(
124137
)
125138
}
126139

140+
@Composable
141+
private fun RegisterSyncTask(selectedLocale: Locale?) {
142+
val syncRegistry = circuitContext.syncTaskRegistry
143+
DisposableEffect(syncRegistry, selectedLocale) {
144+
if (syncRegistry == null) return@DisposableEffect onDispose { }
145+
val id = syncRegistry.registerSyncTask { force -> syncData(selectedLocale ?: settings.appLanguage, force) }
146+
onDispose { syncRegistry.unregisterSyncTask(id) }
147+
}
148+
}
149+
127150
@Composable
128151
private fun rememberSpotlightTools(
129152
secondLanguage: Language?,
@@ -190,9 +213,14 @@ class ToolsPresenter @AssistedInject internal constructor(
190213
}
191214
}
192215

216+
private fun SyncTracker.syncData(locale: Locale, force: Boolean = false) = launchSync {
217+
val country = settings.getCountrySettingFlow().first()
218+
syncService.syncToolOrder(locale, country, force)
219+
}
220+
193221
@AssistedFactory
194222
@CircuitInject(ToolsScreen::class, SingletonComponent::class)
195223
interface Factory {
196-
fun create(navigator: Navigator): ToolsPresenter
224+
fun create(circuitContext: CircuitContext, navigator: Navigator): ToolsPresenter
197225
}
198226
}

app/src/test/kotlin/org/cru/godtools/ui/dashboard/DashboardPresenterTest.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
55
import app.cash.turbine.ReceiveTurbine
66
import com.jeppeman.mockposable.mockk.everyComposable
77
import com.slack.circuit.foundation.NavEvent
8+
import com.slack.circuit.runtime.CircuitContext
9+
import com.slack.circuit.runtime.InternalCircuitApi
810
import com.slack.circuit.test.FakeNavigator
911
import com.slack.circuit.test.test
1012
import io.mockk.coEvery
@@ -14,6 +16,8 @@ import kotlin.test.AfterTest
1416
import kotlin.test.Test
1517
import kotlin.test.assertEquals
1618
import kotlin.test.assertFalse
19+
import kotlin.test.assertNotNull
20+
import kotlin.test.assertNull
1721
import kotlin.test.assertTrue
1822
import kotlinx.coroutines.CompletableDeferred
1923
import kotlinx.coroutines.sync.Mutex
@@ -27,6 +31,7 @@ import org.cru.godtools.base.ui.circuit.screen.dashboard.page.LessonsScreen
2731
import org.cru.godtools.sync.GodToolsSyncService
2832
import org.cru.godtools.ui.dashboard.DashboardPresenter.UiEvent
2933
import org.cru.godtools.ui.dashboard.DashboardPresenter.UiState
34+
import org.cru.godtools.ui.dashboard.SyncTaskRegistry.Companion.syncTaskRegistry
3035
import org.cru.godtools.ui.drawer.DrawerMenuPresenter
3136
import org.cru.godtools.ui.drawer.DrawerMenuScreen
3237
import org.cru.godtools.ui.tooldetails.ToolDetailsScreen
@@ -36,9 +41,11 @@ import org.robolectric.annotation.Config
3641
@RunWith(AndroidJUnit4::class)
3742
@Config(application = Application::class)
3843
@Suppress("DeferredResultUnused")
44+
@OptIn(InternalCircuitApi::class)
3945
class DashboardPresenterTest {
4046
private val screen = DashboardScreen(HomeScreen)
4147
private val syncLock = Mutex(true)
48+
private val circuitContext = CircuitContext(null)
4249

4350
private val drawerMenuPresenter: DrawerMenuPresenter = mockk {
4451
everyComposable { present() } returns DrawerMenuScreen.State()
@@ -55,6 +62,7 @@ class DashboardPresenterTest {
5562
private val presenter = DashboardPresenter(
5663
drawerMenuPresenter = drawerMenuPresenter,
5764
syncService = syncService,
65+
circuitContext = circuitContext,
5866
navigator = navigator,
5967
screen = screen,
6068
)
@@ -72,6 +80,7 @@ class DashboardPresenterTest {
7280
val presenter = DashboardPresenter(
7381
drawerMenuPresenter = drawerMenuPresenter,
7482
syncService = syncService,
83+
circuitContext = circuitContext,
7584
navigator = navigator,
7685
screen = DashboardScreen(LessonsScreen),
7786
)
@@ -140,5 +149,17 @@ class DashboardPresenterTest {
140149
}
141150
// endregion UiEvent.TriggerSync
142151

152+
// region SideEffect - SyncTaskRegistry
153+
@Test
154+
fun `SideEffect - SyncTaskRegistry - set on CircuitContext while presenter is active`() = runTest {
155+
assertNull(circuitContext.syncTaskRegistry)
156+
presenter.test {
157+
awaitInitialState()
158+
assertNotNull(circuitContext.syncTaskRegistry)
159+
}
160+
assertNull(circuitContext.syncTaskRegistry)
161+
}
162+
// endregion SideEffect - SyncTaskRegistry
163+
143164
private suspend fun ReceiveTurbine<UiState>.awaitInitialState() = awaitItemMatching { it.isSyncing }
144165
}

0 commit comments

Comments
 (0)