P0.1: extract IDataManager interface + LocalDataManager ambient
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) <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,7 @@ import kotlin.time.ExperimentalTime
|
|||||||
* Data Flow:
|
* Data Flow:
|
||||||
* User Action → API Call → Server Response → DataManager Updated → All Screens React
|
* User Action → API Call → Server Response → DataManager Updated → All Screens React
|
||||||
*/
|
*/
|
||||||
object DataManager {
|
object DataManager : IDataManager {
|
||||||
|
|
||||||
// ==================== CACHE CONFIGURATION ====================
|
// ==================== CACHE CONFIGURATION ====================
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ object DataManager {
|
|||||||
val authToken: StateFlow<String?> = _authToken.asStateFlow()
|
val authToken: StateFlow<String?> = _authToken.asStateFlow()
|
||||||
|
|
||||||
private val _currentUser = MutableStateFlow<User?>(null)
|
private val _currentUser = MutableStateFlow<User?>(null)
|
||||||
val currentUser: StateFlow<User?> = _currentUser.asStateFlow()
|
override val currentUser: StateFlow<User?> = _currentUser.asStateFlow()
|
||||||
|
|
||||||
// ==================== APP PREFERENCES ====================
|
// ==================== APP PREFERENCES ====================
|
||||||
|
|
||||||
@@ -122,13 +122,13 @@ object DataManager {
|
|||||||
// ==================== RESIDENCES ====================
|
// ==================== RESIDENCES ====================
|
||||||
|
|
||||||
private val _residences = MutableStateFlow<List<Residence>>(emptyList())
|
private val _residences = MutableStateFlow<List<Residence>>(emptyList())
|
||||||
val residences: StateFlow<List<Residence>> = _residences.asStateFlow()
|
override val residences: StateFlow<List<Residence>> = _residences.asStateFlow()
|
||||||
|
|
||||||
private val _myResidences = MutableStateFlow<MyResidencesResponse?>(null)
|
private val _myResidences = MutableStateFlow<MyResidencesResponse?>(null)
|
||||||
val myResidences: StateFlow<MyResidencesResponse?> = _myResidences.asStateFlow()
|
val myResidences: StateFlow<MyResidencesResponse?> = _myResidences.asStateFlow()
|
||||||
|
|
||||||
private val _totalSummary = MutableStateFlow<TotalSummary?>(null)
|
private val _totalSummary = MutableStateFlow<TotalSummary?>(null)
|
||||||
val totalSummary: StateFlow<TotalSummary?> = _totalSummary.asStateFlow()
|
override val totalSummary: StateFlow<TotalSummary?> = _totalSummary.asStateFlow()
|
||||||
|
|
||||||
private val _residenceSummaries = MutableStateFlow<Map<Int, ResidenceSummaryResponse>>(emptyMap())
|
private val _residenceSummaries = MutableStateFlow<Map<Int, ResidenceSummaryResponse>>(emptyMap())
|
||||||
val residenceSummaries: StateFlow<Map<Int, ResidenceSummaryResponse>> = _residenceSummaries.asStateFlow()
|
val residenceSummaries: StateFlow<Map<Int, ResidenceSummaryResponse>> = _residenceSummaries.asStateFlow()
|
||||||
@@ -158,13 +158,13 @@ object DataManager {
|
|||||||
// ==================== SUBSCRIPTION ====================
|
// ==================== SUBSCRIPTION ====================
|
||||||
|
|
||||||
private val _subscription = MutableStateFlow<SubscriptionStatus?>(null)
|
private val _subscription = MutableStateFlow<SubscriptionStatus?>(null)
|
||||||
val subscription: StateFlow<SubscriptionStatus?> = _subscription.asStateFlow()
|
override val subscription: StateFlow<SubscriptionStatus?> = _subscription.asStateFlow()
|
||||||
|
|
||||||
private val _upgradeTriggers = MutableStateFlow<Map<String, UpgradeTriggerData>>(emptyMap())
|
private val _upgradeTriggers = MutableStateFlow<Map<String, UpgradeTriggerData>>(emptyMap())
|
||||||
val upgradeTriggers: StateFlow<Map<String, UpgradeTriggerData>> = _upgradeTriggers.asStateFlow()
|
val upgradeTriggers: StateFlow<Map<String, UpgradeTriggerData>> = _upgradeTriggers.asStateFlow()
|
||||||
|
|
||||||
private val _featureBenefits = MutableStateFlow<List<FeatureBenefit>>(emptyList())
|
private val _featureBenefits = MutableStateFlow<List<FeatureBenefit>>(emptyList())
|
||||||
val featureBenefits: StateFlow<List<FeatureBenefit>> = _featureBenefits.asStateFlow()
|
override val featureBenefits: StateFlow<List<FeatureBenefit>> = _featureBenefits.asStateFlow()
|
||||||
|
|
||||||
private val _promotions = MutableStateFlow<List<Promotion>>(emptyList())
|
private val _promotions = MutableStateFlow<List<Promotion>>(emptyList())
|
||||||
val promotions: StateFlow<List<Promotion>> = _promotions.asStateFlow()
|
val promotions: StateFlow<List<Promotion>> = _promotions.asStateFlow()
|
||||||
|
|||||||
@@ -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<User?>
|
||||||
|
|
||||||
|
/** Observed by [com.tt.honeyDue.ui.screens.ContractorDetailScreen] and the onboarding first-task screen. */
|
||||||
|
val residences: StateFlow<List<Residence>>
|
||||||
|
|
||||||
|
/** Observed by [com.tt.honeyDue.ui.screens.HomeScreen] and [com.tt.honeyDue.ui.screens.ResidencesScreen]. */
|
||||||
|
val totalSummary: StateFlow<TotalSummary?>
|
||||||
|
|
||||||
|
/** Observed by [com.tt.honeyDue.ui.screens.subscription.FeatureComparisonScreen]. */
|
||||||
|
val featureBenefits: StateFlow<List<FeatureBenefit>>
|
||||||
|
|
||||||
|
/** Observed by [com.tt.honeyDue.ui.screens.ProfileScreen]. */
|
||||||
|
val subscription: StateFlow<SubscriptionStatus?>
|
||||||
|
}
|
||||||
@@ -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<IDataManager> { DataManager }
|
||||||
@@ -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."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user