feat(theme): persist ThemeMode preference + reactive PyrycodeMobileTheme wiring (#86)#109
Conversation
Adds a ThemeMode { SYSTEM, LIGHT, DARK } enum backed by a string-typed
DataStore key on AppPreferences, with a Flow getter and suspend setter.
The composition root collects the flow and resolves darkTheme inside
@composable scope (preserving isSystemInDarkTheme() on SYSTEM), then
passes the boolean into the existing PyrycodeMobileTheme signature.
The Settings Theme row's hardcoded "System" subtitle now reads from the
same flow, rendering "System default" / "Light" / "Dark". The row stays
non-interactive in this slice; the picker dialog is the sibling ticket.
Closes #86
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Code Review: #86Decision: PASS FindingsNo MUST FIX or SHOULD FIX findings.
SummarySlice lands cleanly. Data path ( Test coverage hits all three AC items (default → SYSTEM, round-trip every enum value, unparseable garbage falls back to SYSTEM). The unparseable test correctly bypasses the typed setter by writing the raw Compose correctness: Material 3 compliance: no new hardcoded colors, typography, or shapes. The diff is text-source-only on the Theme row established in #64 — no layout/decoration changes, so the Figma fidelity spec applies trivially. Kotlin idioms: no Architecture spec followed almost line-for-line; the one deviation (param order) is lint-driven and documented. |
Adds per-ticket notes for #86; updates app-preferences and settings-screen feature docs to cover the new themeMode flow, the caller-computes-darkTheme pattern at MainActivity's composition root, and the Settings Theme row's subtitle now reading from DataStore. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
What
Adds the data path for the theme-mode preference, so the sibling picker ticket has plumbing to write to and the app honors a stored mode across launches.
ThemeMode { SYSTEM, LIGHT, DARK }enum inde.pyryco.mobile.data.preferencesAppPreferences.themeMode: Flow<ThemeMode>andsuspend fun setThemeMode(mode), backed bystringPreferencesKey("theme_mode")holdingThemeMode.name. Absent or unparseable →SYSTEM.MainActivity.setContentcollects the flow withcollectAsStateWithLifecycle(initialValue = SYSTEM), resolvesdarkThemevia an exhaustivewhen(SYSTEM → isSystemInDarkTheme(),LIGHT → false,DARK → true), and passes it into the existingPyrycodeMobileTheme(darkTheme: Boolean)— signature unchanged so the 18@Previewcall sites stay untouched.SettingsScreengains athemeMode: ThemeMode = ThemeMode.SYSTEMparameter (default keeps the two@Previewcomposables compiling); the Theme row'ssupportingis nowthemeMode.label()returning "System default" / "Light" / "Dark". The row remains non-interactive per spec.PyryNavHostfor one screen.Issue
Closes #86
Testing
Test-first (RED → GREEN). Three new unit tests in
AppPreferencesTest:themeMode_defaultsToSystem— fresh DataStore returnsSYSTEMsetThemeMode_roundTripsAllValues— loopsThemeMode.entries, writes + reads eachthemeMode_unparseableStoredValue_fallsBackToSystem— writes"PURPLE"to the underlying key, expectsSYSTEMLocal run status:
./gradlew test— all unit tests pass./gradlew lint— clean (had to reorderSettingsScreenparams:modifierbeforethemeModeperComposeParameterOrder)./gradlew assembleDebug— builds./gradlew connectedAndroidTest— not run (no emulator/device); spec explicitly says no instrumented coverage needed for this sliceArchitecture compliance
docs/specs/architecture/86-*.md:ThemeModelives indata.preferences,Theme.ktsignature staysdarkTheme: Boolean, resolution sits inside thesetContent@Composablescope, label mapping lives inSettingsScreen.kt(nostringResource— sibling picker reuses the same literal strings).SettingsScreenreceivesthemeModeas a parameter instead of injectingAppPreferencesitself.AppPreferencesis already asingle { … }).🤖 Generated with Claude Code