mirror of
https://github.com/tiennm99/loto.git
synced 2026-10-13 02:14:42 +00:00
11 KiB
11 KiB
Android review fixes (2026-08-31)
Source review: plans/reports/code-reviewer-260831-2213-android-review.md.
Scope: android/ only (per delegation; web/ read-only for M2 parity check;
.github/workflows/ and android/local.properties untouched).
Fixed
H1 — restore/user-action race
state/PlayerBoardViewModel.kt:restore()now only applies the saved roundif (saved != null && grid == null)— agenerate()that lands while the DataStore read is still in flight can no longer be clobbered. Addedloading: BooleantoPlayerUiState(trueuntil restore resolves);PlayerBoardScreen.kt's "Tạo bảng mới" button is nowenabled = !state.loading, closing the no-confirm branch at its root (previouslygrid == nullmeant both "no card" and "not loaded yet").state/MasterStore.kt: addedloading: StateFlow<Boolean>and amutatedBeforeRestoreguard set bystartNewGame()/drawNext();restore()skips applying the saved snapshot once either has run.state/MasterPanelViewModel.ktexposesloading;ui/master/MasterPanelScreen.kt's "Ván mới" button is nowenabled = !loading, same shape as the player fix.- Regression tests: added
app/src/test/java/com/miti99/loto/SlowDataStore.kt(delays a wrappedDataStore's first.dataemission until aCompletableDeferredgate completes —InMemoryDataStorealone resolves synchronously so the race was invisible to the existing suite, per the report's test-quality note). Addedrestore does not clobber a card generated while the load is still in flight(PlayerBoardViewModelTest) andrestore does not clobber a new game started while the load is still in flight(newMasterStoreTest.kt) — both fail without the fix, pass with it.
H2 — tier-2 confetti invisible
ui/player/KinhDialog.kt:Dialognow usesDialogProperties(usePlatformDefaultWidth = false)so the dialog window spans the full screen, andConfettiOverlay()is composed inside that window (drawn after/above the cardColumnin aBox), gated by a newshowConfetti: Booleanparam.PlayerBoardScreen.ktno longer emitsConfettiOverlay()as a scroll-column sibling. This is option (b) from the report (embed in the dialog's own window) rather than a separatePopup, since it guarantees same-window z-order over the scrim without relying on cross-window layering behavior.
M1 — RGB truncation
ui/settings/EmptyCellColorPicker.kt:r/g/bnow useroundToInt()instead oftoInt().
M2 — force-scroll to hero in both mode
- Checked
web/src/lib/MasterPanel.svelte:86-158(read-only):handleDrawNext()setsscrollOnNextDraw = trueunconditionally, called by both the manual draw button and the auto-callsetInterval, with nosettings.modegate.MasterPanel.svelteis rendered the same way inmasterandbothmode (+page.svelte:37). Web already force-scrolls on every draw inbothmode — Android'sLaunchedEffect(tickKey) { ... heroRequester.bringIntoView() }inMasterPanelScreen.ktalready matches. Left as-is; added a comment recording the parity check so this isn't re-litigated as a bug.
M3 — hardcoded toast copy / unused strings
PlayerUiState.toast: String?→waitingNumber: Int?. The ViewModel now stores the raw number;PlayerBoardScreen.ktformats it withstringResource(R.string.toast_waiting, waitNum).- Deleted 4 of the 5 unreferenced resources from
strings.xml:toast_dismiss,player_board_label,master_board_label,kinh_close.toast_waitingis now wired (not deleted). - Did not move
PlayerCell.kt'sstateDescriptionfragments intostrings.xml— the task scope named only the toast + the five unused resources; that's a separate, additive accessibility cleanup the report suggests but didn't require. Noted below as deferred.
M4 — cold-start settings/master ordering
LotoApplication.kt: splitonCreate's single coroutine into twoappScope.launchblocks (masterStore.restore()andsettingsState.collect { ... }) so a slow round-state restore can't stall voice-id sync.PlayerBoardViewModel.kt:initnow doescombine(masterStore.state, settings) { master, _ -> master }.collect { onMasterState(it) }instead of a plainmasterStore.state.collect, so a settings arrival re-runs the replay against the current master snapshot.- Scope note (see Unresolved below):
PlayerAutoCross.applyMasterCallsadvances the cursor tocalled.sizeon every non-BOTHpass, even a no-op one.combine()closes the ordering where settings resolves to its real value no later thanmasterStore.restore()'s first real emission (verified by test). It does not close the reverse ordering — master's restore landing with full history before settings ever resolves — because by the time settings arrives, the cursor has already consumed that history under the wrong mode. Fully closing that needs a "settings loaded" signal threaded throughSettingsRepository/LotoApplication, which is a larger change than this narrow, "usually invisible" (report's words) edge case warrants; documented as a deferred follow-up rather than silently left unfixed.
M5 — generateGrid() off the main thread
PlayerBoardViewModel.generate(): the pureCardGenerator.generateGrid()call now runs on an injectablecomputeDispatcher(defaults toDispatchers.Default), thenwithContext(Dispatchers.Main.immediate)applies the result via a new privateapplyGeneratedGrid(). Only the computation is offloaded; allViewModelfield mutations still happen on Main, so there's no new cross-thread mutable-state hazard.- Added the
computeDispatcherconstructor param (defaulted) specifically so unit tests can pass the sameStandardTestDispatcherused for Main — otherwisegenerate()would race a real background thread againstrunCurrent()/advanceUntilIdle(), making the suite flaky.LotoViewModelFactory.ktneeded no change (named-arg construction, new param is defaulted).
M6 — master-panel recomposition
- Deferred, not applied. The report frames this as "not a blocker...
worth measuring," conditional on API 24 device profiling that
TODO.mdshows as still unchecked. The suggested mitigations (key(num)blocks or lambda-based cell props) touch the hotMasterBoard/cell-render path with no compose UI test harness in this project to catch a visual regression — the risk/verification ratio didn't clear the bar for a code-review-driven change without a measured baseline. Flagging back to the user rather than guessing at scope.
L1 — ExoPlayer never released
MainActivity.kt:onDestroy()now calls(application as LotoApplication).voicePlayer.release()whenisFinishingis true. Guarded byisFinishing(not fired on a rotation/config-change recreate) sinceExoPlayer.release()is terminal — the single Activity finishing via the exit-confirm dialog'sfinish()is the one path where no furtherspeak()call can follow.
Manifest/CI hygiene (clear-cut only)
AndroidManifest.xml(L8): droppedandroid:launchMode="singleTask"(defaultstandard) — no deep links or externalstartActivitytargets.app/build.gradle.kts(L11): moved the Vietnamese-only trim from the deprecateddefaultConfig.resourceConfigurationstoandroidResources.localeFilters(AGP 8.13).- L10 (CI keystore cleanup) not applied:
.github/workflows/android-release.ymlis outsideandroid/, and the file-ownership instruction for this task is explicit ("Files you may modify: anything under .../android"). Flagged, not fixed — a one-lineif: always()rm step for whoever owns that file. - L9 (
allowBackup/dataExtractionRules) not applied: the report itself frames this as conditional ("if a restoredloto_game_statefrom another device is undesirable") — that's a product decision about cross-device transfer behavior, not a clear-cut technical fix. Left as-is pending a decision. - L7 (
Color.parseColor→toColorInt()) not applied: not in the task's enumerated item list (task named Manifest/CI hygiene specifically);ColorParsing.ktis core app code, out of the explicitly requested scope.
Verification
JAVA_HOME="C:/Program Files/Eclipse Adoptium/jdk-21.0.10.7-hotspot" ./gradlew :app:testDebugUnitTest→ BUILD SUCCESSFUL.PlayerBoardViewModelTest: 17/17 (14 original + 3 new).MasterStoreTest(new): 2/2. All other existing suites unaffected (no other test files touched)../gradlew :app:lintDebug→ BUILD SUCCESSFUL, no findings in any file touched by this change (checked by grepping the HTML report for each modified filename).- No gradle daemon left running beyond the two builds above (Gradle's default idle-timeout will reap it; not force-killed since this repo's standard flow doesn't request that and the daemon is reused by whoever runs the next Gradle command in this workspace).
Deferred / out of scope
- M6 (master-panel recomposition) — needs a profiled baseline first.
- L9 (dataExtractionRules) — needs a product decision on cross-device round-state transfer.
- L10 (CI keystore cleanup) — file outside
android/, blocked by file-ownership scope. - L7 (
Color.parseColordeprecation) — not in the delegated item list. PlayerCell.kt's hardcodedstateDescriptionVietnamese fragments — the report bundles this with M3 but the task scoped M3 to the toast + the five unused resources; left untouched.- L2–L6 (shared voice player double-cancel, restore shape validation,
swallowed
IOExceptionwith no log signal, staleautoRunningafter manual-draw exhaustion,manualUntickssurviving a non-BOTH reset) — not named in the task's item list; not touched.
Unresolved questions
- M4: is the residual race (master's restore landing with full history
before settings ever resolves) worth a "settings loaded" signal, or is
the report's own "usually invisible" framing enough to leave it as
documented risk? I implemented the report's suggested
combine()fix, which closes the more common ordering, but flagging the gap rather than silently claiming full closure. - L9: should
loto_game_statebe excluded from Android backup/device transfer viadataExtractionRules, or is cross-device restore of an in-progress round acceptable/desired? Needs a product call, not a code fix. - M6: is there an actual profiled slowdown on the API 24 device from
TODO.md, or should this stay deferred until QA reports one?
Status: DONE_WITH_CONCERNS Summary: Fixed H1, H2, M1, M3, M4 (partial, documented gap), M5, L1, and the two clear-cut manifest/gradle hygiene items (L8, L11); confirmed M2 already matches web and left as-is; deferred M6, L2-L7, L9, L10 with reasons. All unit tests (including 5 new race-regression tests) and lint pass. Concerns/Blockers: M4's fix does not close every interleaving (see report); M6/L9/L10 deferred rather than guessed at; none are build-blocking.