From 98b775d33565bb6f2e10297888878802ebd49046 Mon Sep 17 00:00:00 2001 From: Trey T Date: Sat, 18 Apr 2026 19:06:16 -0500 Subject: [PATCH] P0.1: extract IDataManager interface + LocalDataManager ambient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a narrow IDataManager contract covering the 5 DataManager members referenced from ui/screens/** (currentUser, residences, totalSummary, featureBenefits, subscription) and a staticCompositionLocalOf ambient (LocalDataManager) that defaults to the DataManager singleton. No screen call-sites change in this commit — screens migrate in P0.2. ViewModels, APILayer, and PersistenceManager continue to depend on the concrete DataManager singleton directly; the interface is deliberately scoped to the screen surface the parity-gallery needs to substitute. Includes IDataManagerTest (DataManager is IDataManager) and LocalDataManagerTest (ambient val is exposed + default type-checks to the real singleton). runComposeUiTest intentionally avoided — consistent with ThemeSelectionScreenTest's convention, since commonTest composition runtime is flaky on iosSimulator. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../com/tt/honeyDue/data/DataManager.kt | 12 +++--- .../com/tt/honeyDue/data/IDataManager.kt | 37 ++++++++++++++++ .../com/tt/honeyDue/data/LocalDataManager.kt | 14 +++++++ .../com/tt/honeyDue/data/IDataManagerTest.kt | 26 ++++++++++++ .../tt/honeyDue/data/LocalDataManagerTest.kt | 42 +++++++++++++++++++ 5 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/IDataManager.kt create mode 100644 composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/LocalDataManager.kt create mode 100644 composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/IDataManagerTest.kt create mode 100644 composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/LocalDataManagerTest.kt diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/DataManager.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/DataManager.kt index dd23b06..9139932 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/DataManager.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/DataManager.kt @@ -32,7 +32,7 @@ import kotlin.time.ExperimentalTime * Data Flow: * User Action → API Call → Server Response → DataManager Updated → All Screens React */ -object DataManager { +object DataManager : IDataManager { // ==================== CACHE CONFIGURATION ==================== @@ -107,7 +107,7 @@ object DataManager { val authToken: StateFlow = _authToken.asStateFlow() private val _currentUser = MutableStateFlow(null) - val currentUser: StateFlow = _currentUser.asStateFlow() + override val currentUser: StateFlow = _currentUser.asStateFlow() // ==================== APP PREFERENCES ==================== @@ -122,13 +122,13 @@ object DataManager { // ==================== RESIDENCES ==================== private val _residences = MutableStateFlow>(emptyList()) - val residences: StateFlow> = _residences.asStateFlow() + override val residences: StateFlow> = _residences.asStateFlow() private val _myResidences = MutableStateFlow(null) val myResidences: StateFlow = _myResidences.asStateFlow() private val _totalSummary = MutableStateFlow(null) - val totalSummary: StateFlow = _totalSummary.asStateFlow() + override val totalSummary: StateFlow = _totalSummary.asStateFlow() private val _residenceSummaries = MutableStateFlow>(emptyMap()) val residenceSummaries: StateFlow> = _residenceSummaries.asStateFlow() @@ -158,13 +158,13 @@ object DataManager { // ==================== SUBSCRIPTION ==================== private val _subscription = MutableStateFlow(null) - val subscription: StateFlow = _subscription.asStateFlow() + override val subscription: StateFlow = _subscription.asStateFlow() private val _upgradeTriggers = MutableStateFlow>(emptyMap()) val upgradeTriggers: StateFlow> = _upgradeTriggers.asStateFlow() private val _featureBenefits = MutableStateFlow>(emptyList()) - val featureBenefits: StateFlow> = _featureBenefits.asStateFlow() + override val featureBenefits: StateFlow> = _featureBenefits.asStateFlow() private val _promotions = MutableStateFlow>(emptyList()) val promotions: StateFlow> = _promotions.asStateFlow() diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/IDataManager.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/IDataManager.kt new file mode 100644 index 0000000..5594297 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/IDataManager.kt @@ -0,0 +1,37 @@ +package com.tt.honeyDue.data + +import com.tt.honeyDue.models.FeatureBenefit +import com.tt.honeyDue.models.Residence +import com.tt.honeyDue.models.SubscriptionStatus +import com.tt.honeyDue.models.TotalSummary +import com.tt.honeyDue.models.User +import kotlinx.coroutines.flow.StateFlow + +/** + * Minimal contract covering the [DataManager] surface consumed by Compose screens. + * + * This interface exists solely so screens can depend on an abstraction that tests, + * previews, and the parity-gallery can substitute via [LocalDataManager]. It is + * deliberately narrow — only members referenced from the ui/screens package tree + * are included. + * + * ViewModels, [com.tt.honeyDue.network.APILayer], and [PersistenceManager] continue + * to use the concrete [DataManager] singleton directly; widening this interface to + * cover their surface is explicitly out of scope for the ambient refactor. + */ +interface IDataManager { + /** Observed by [com.tt.honeyDue.ui.screens.ProfileScreen], [com.tt.honeyDue.ui.screens.ResidenceDetailScreen], [com.tt.honeyDue.ui.screens.ResidenceFormScreen]. */ + val currentUser: StateFlow + + /** Observed by [com.tt.honeyDue.ui.screens.ContractorDetailScreen] and the onboarding first-task screen. */ + val residences: StateFlow> + + /** Observed by [com.tt.honeyDue.ui.screens.HomeScreen] and [com.tt.honeyDue.ui.screens.ResidencesScreen]. */ + val totalSummary: StateFlow + + /** Observed by [com.tt.honeyDue.ui.screens.subscription.FeatureComparisonScreen]. */ + val featureBenefits: StateFlow> + + /** Observed by [com.tt.honeyDue.ui.screens.ProfileScreen]. */ + val subscription: StateFlow +} diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/LocalDataManager.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/LocalDataManager.kt new file mode 100644 index 0000000..05fb1e2 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/LocalDataManager.kt @@ -0,0 +1,14 @@ +package com.tt.honeyDue.data + +import androidx.compose.runtime.staticCompositionLocalOf + +/** + * Screens resolve their data source via this ambient. Production resolves + * to the singleton [DataManager]. Tests, previews, and the parity-gallery + * override via `CompositionLocalProvider(LocalDataManager provides fake) { ... }`. + * + * Uses [staticCompositionLocalOf] (not `compositionLocalOf`) because the + * DataManager reference is stable for the app's lifetime — reading it + * shouldn't trigger recomposition of everything below the provider. + */ +val LocalDataManager = staticCompositionLocalOf { DataManager } diff --git a/composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/IDataManagerTest.kt b/composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/IDataManagerTest.kt new file mode 100644 index 0000000..54f6b45 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/IDataManagerTest.kt @@ -0,0 +1,26 @@ +package com.tt.honeyDue.data + +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * Compile + runtime guard that the production [DataManager] singleton still + * satisfies the [IDataManager] contract. If a future refactor drops a member + * from the interface — or worse, drops the `: IDataManager` supertype from + * DataManager — this test fails loud, before any screen loses its data source. + */ +class IDataManagerTest { + + // The `is IDataManager` check is statically `true` today — the compiler + // warning confirms DataManager satisfies the interface. If either side + // ever drifts, that status changes (or the file fails to compile), so + // this test acts as a compile-time + runtime guard. + @Suppress("USELESS_IS_CHECK") + @Test + fun dataManagerSingletonImplementsIDataManager() { + assertTrue( + DataManager is IDataManager, + "DataManager must implement IDataManager so screens can resolve it through LocalDataManager." + ) + } +} diff --git a/composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/LocalDataManagerTest.kt b/composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/LocalDataManagerTest.kt new file mode 100644 index 0000000..67ee043 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/LocalDataManagerTest.kt @@ -0,0 +1,42 @@ +package com.tt.honeyDue.data + +import kotlin.test.Test +import kotlin.test.assertNotNull + +/** + * P0 — LocalDataManager ambient. + * + * We deliberately avoid `runComposeUiTest { }` here (same reasoning as + * [com.tt.honeyDue.ui.screens.theme.ThemeSelectionScreenTest] — Compose UI + * testing in commonTest is flaky on iosSimulator for this project). + * + * Instead this asserts the two invariants the parity-gallery plan relies on: + * 1. [LocalDataManager] itself is a non-null reference exposed from the + * `com.tt.honeyDue.data` package (so screens can import it). + * 2. The default value the ambient resolves to in production is the real + * [DataManager] singleton — verified indirectly by the fact that + * [DataManager] satisfies [IDataManager] (see [IDataManagerTest]). + * + * The override behavior via `CompositionLocalProvider(LocalDataManager provides fake)` + * is exercised by the parity-gallery screens in a later phase. We don't + * duplicate the Compose-runtime machinery here. + */ +class LocalDataManagerTest { + + @Test + fun ambientIsExposedForScreens() { + // Sanity: the val exists and is resolvable at the package path screens import from. + assertNotNull(LocalDataManager, "LocalDataManager must be a top-level val in com.tt.honeyDue.data") + } + + @Test + fun defaultResolvesToRealDataManagerSingleton() { + // If someone swaps the default factory to a fake by accident, this test + // won't catch the runtime value (we can't invoke the factory without a + // Composer), but we CAN guarantee the type contract holds — DataManager + // must implement IDataManager so the default factory `{ DataManager }` + // in LocalDataManager.kt type-checks. + val manager: IDataManager = DataManager + assertNotNull(manager) + } +}