feat(core/store): Phase B — consumer Store5 seam (Theme + Koin + DB v2 + RoomSubmitOutbox) - #2683
Conversation
…ption
Phase B baseline: creates the single-discoverable customization seam that
sits between consumer features and the synced core-base/store /
core-base/ui framework modules.
New module ':core:store' with 4 .kt files (mirroring kmp-project-template's
core/store reference):
AppStoreRegistry.kt Named-qualifier registry — Phase C feature waves
add 'val Foo = store("foo")' as features migrate.
Reserved slots noted in comments per wave.
AppErrorMapper.kt Throwable -> user-message mapper. Routes through
template's ErrorCategory.{Network, Auth, RateLimit,
Server, Generic}. Phase C feature waves extend
with Fineract-specific exception branches above
the categorize() fallback (e.g.,
FineractValidationException,
OfflineSubmissionRejectedException).
AppScreenStateDefaults Branded empty / error / no-network / loading
visuals — Lottie + telemetry hooks reserved as
TODO. messageFor wired through mapErrorToUserMessage.
di/StoreModule.kt Koin 'appStoreModule' — Phase C registers
'single(qualifier = AppStoreRegistry.X) { ... }'
here as features migrate.
settings.gradle.kts:
+ ':core:store'
+ ':core-base:datastore', ':core-base:security', ':core-base:store'
(synced via openMF#2682 but not previously declared)
Phase B remaining work (separate commits on this branch):
1. Thread 'LocalScreenStateDefaults provides appScreenStateDefaults()'
into MifosTheme in core/designsystem
2. Wire 'appStoreModule' into Koin startup in cmp-android + cmp-ios
3. Add framework_submit_drafts entity + v4->v5 AutoMigration to
com.mifos.room.MifosDatabase
4. Boot OfflineSubmitSyncer.start() in MainActivity.onCreate (Android)
and iOS app entry point
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…SubmitOutbox
Phase B chunks 1-4 complete. Builds on the scaffold from prior commit:
Chunk 1 — Theme wiring (core/designsystem/.../Theme.kt)
+ import com.mifos.core.store.appScreenStateDefaults
+ import template.core.base.ui.screen.LocalScreenStateDefaults
+ KptMaterialTheme now wraps content in
CompositionLocalProvider(LocalScreenStateDefaults provides ...) — every
screen under MifosTheme inherits branded ScreenState defaults.
+ core/designsystem/build.gradle.kts: + implementation(projects.core.store)
Chunk 2 — Koin DI wiring
+ cmp-navigation/.../di/KoinModules.kt registers appStoreModule in
'allModules' list. Feature waves (Phase C) bind their Store<K,V>
factories qualifier-bound via AppStoreRegistry.
+ cmp-navigation/build.gradle.kts: + implementation(projects.core.store)
Chunk 3 — Database v1 -> v2 migration (Store5 submit-draft outbox)
+ core/database/src/commonMain/.../entities/framework/DraftEntity.kt —
@entity(tableName = 'framework_submit_drafts'); 7 columns matching
template's reference impl.
+ core/database/src/commonMain/.../dao/DraftDao.kt — @dao interface with
insert / getById / getPendingByFormKey / observePendingByFormKey /
getAllPending / markRetrying / markSubmitted / markFailed /
updatePayload / deleteByFormKey / deleteAll / deleteOlderThan.
+ commonMain MifosDatabase + 3 platform actuals (android, desktop,
native): import DraftEntity / DraftDao, add to entities[], add
'abstract val draftDao: DraftDao', bump VERSION 1 -> 2 with KDoc.
+ DatabaseModule.android.kt / desktop.kt / PlatformSpecificModule.kt:
add MIGRATION_1_2 (raw SQL CREATE TABLE) and chain .addMigrations()
onto the AppDatabaseFactory builder. SQL is platform-portable
(androidx.room3.migration.Migration + androidx.sqlite.execSQL).
+ DaoModule.kt: + single { get<MifosDatabase>().draftDao }
+ core/data/.../infra/impl/RoomSubmitOutbox.kt — Room-backed
SubmitOutbox<P>. Serializes payload via kotlinx.serialization,
stores in framework_submit_drafts. Idempotent save (upserts on
formKey collision). Wires markRetrying / markSubmitted /
markFailed to DraftDao status transitions.
+ core/data/build.gradle.kts: + api(projects.coreBase.store)
Chunk 4 — OfflineSubmitSyncer pattern documented in core/store/README.md
Syncer is parameterized by payload type P, so it is instantiated per-form
in feature DI modules (not globally in MainActivity). Pattern documented
with full Koin DI example under 'Phase C syncer pattern' heading.
This makes core/store / core/database / core/data ready for Phase C feature
waves to start migrating to Store5 with offline-resilient mutations.
0640cf6 to
bd07a56
Compare
|



Summary
Phase B of the Store5-template-adoption epic. Wires field-officer's consumer customization seam (
core/store) for offline-first reads/writes, hooksLocalScreenStateDefaultsintoMifosTheme, registersappStoreModulein central Koin, and adds theframework_submit_draftstable (DB v1 → v2) backed byRoomSubmitOutbox<P>.This unblocks Phase C — feature waves can now migrate to
Store5with branded loading / error / empty / no-network visuals and offline-resilient mutation submission, with zero per-screen state-handling code.Changes (17 files, +434 / -24)
1.
core/store/module — consumer customization seam (new module)build.gradle.kts— Compose + Koin convention plugins;api(projects.coreBase.store)+api(projects.coreBase.ui)so consumers getLocalScreenStateDefaults,ScreenState*,Store5Registryfromcore/storealoneREADME.md— full pattern documentation (what you get for free, customization points, Phase C syncer pattern)AppStoreRegistry.kt— named-qualifier registry; Phase C waves addval Foo = store("foo")hereAppErrorMapper.kt—Throwable → user-messagemapper. Routes throughtemplate.core.base.store.error.categorizewith Fineract-aware copy; ready to extend with Fineract exception branches as features landAppScreenStateDefaults.kt— brandedScreenStateDefaults(skeleton loading, branded empty/error/no-network copy,messageFor = ::mapErrorToUserMessage)di/StoreModule.kt— KoinappStoreModule; empty entry point that Phase C waves extend withsingle<Store<K,V>>(qualifier = AppStoreRegistry.X) { ... }registrations2.
core/designsystem/.../Theme.kt—LocalScreenStateDefaultswiringEvery screen under
MifosThemenow inherits branded defaults — no per-screen wiring.3.
cmp-navigation/.../di/KoinModules.kt— central Koin registrationAdds
appStoreModuleto theallModuleslist. Feature waves register theirStore<K,V>factories qualifier-bound viaAppStoreRegistry.4.
MifosDatabasev1 → v2 migration —framework_submit_draftstablecom.mifos.room.entities.framework.DraftEntity(@Entity(tableName = "framework_submit_drafts")) — 7 columns matching template's reference implcom.mifos.room.dao.DraftDao— insert / getById / getPendingByFormKey / observePendingByFormKey / getAllPending / markRetrying / markSubmitted / markFailed / updatePayload / deleteByFormKey / deleteAll / deleteOlderThanexpect+ 3 platform actuals (android / desktop / native) updated — entity added toentities[],abstract val draftDao: DraftDaoadded,VERSIONbumped 1 → 2 with KDocMIGRATION_1_2(raw SQLCREATE TABLE) added to each platform'sDatabaseModule.<platform>.kt, chained ontoAppDatabaseFactorybuilder via.addMigrations(). Portable viaandroidx.room3.migration.Migration+androidx.sqlite.execSQL.+ single { get<MifosDatabase>().draftDao }5.
RoomSubmitOutbox<P>—SubmitOutboxbridge toDraftDaoAt
com.mifos.core.data.infra.impl.RoomSubmitOutbox. Serializes payloads viakotlinx.serialization, persists toframework_submit_drafts. Idempotentsave(upserts onformKeycollision). Used by Phase C feature waves to make form submissions offline-resilient — payload survives process death; auto-retried byOfflineSubmitSynceron reconnect.core/data/build.gradle.kts—+ api(projects.coreBase.store)6.
OfflineSubmitSyncerpattern documentedOfflineSubmitSyncer<P, R>is parameterized by payload type. The original Phase B plan assumed a global syncer inMainActivity— corrected: syncers are instantiated per form within feature DI modules. Full pattern documented incore/store/README.md"Phase C syncer pattern".Phase C syncer pattern (example for one feature)
Test plan
./gradlew :core:store:build :core:database:build :core:data:build :core:designsystem:build :cmp-navigation:buildresolves all deps + compiles all modulesframework_submit_draftstable exists with correct columns (id,formKey,payloadJson,status,createdAtMs,updatedAtMs,errorMessage) and is emptyMifosTheme-wrapped screens still render normally; no NPE onLocalScreenStateDefaultsaccess from any screenstartKoin { modules(KoinModules.allModules) }boots cleanly withappStoreModuleadded (no missing-dep errors)store5MigrationPR review)Phase status
kmp-project-template#158+openMF/mifos-x-field-officer-app#2681+#2682mergedstore5Migration → developmentRelated
plan-layer/project-plans/mifos-x/mifos-x-field-officer-app/active/store5-adoption/PLAN.mdplan-layer/project-plans/mifos-x/mifos-x-field-officer-app/active/store5-adoption/B-core.mdopenMF/mifos-x-field-officer-app#2681openMF/mifos-x-field-officer-app#2682openMF/kmp-project-template#158