Repository navigation
Android: make the SpO₂-candidate display toggle take effect - #1595
Merged
Merged
Conversation
`spo2CandidateByDay` combined against `flowOf(NoopPrefs.spo2CandidateDisplay(appContext))`. `flowOf` emits once and completes, so `combine` held that first value for the ViewModel's whole life — the toggle was read at construction and never again. The symptom is a setting that does nothing. Turn "show SpO2 candidate" off and the Key Metrics tile keeps showing strap estimates until the process restarts; turn it on and nothing appears until then. Both directions, and no way to tell from inside the app that the value it is using is stale. iOS reads PuffinExperiment.spo2CandidateDisplayEnabled per render and has never had the lag, so this was also a live parity divergence. NoopPrefs.spo2CandidateDisplayFlow now wraps the same key in a callbackFlow over OnSharedPreferenceChangeListener — the idiom already used by SettingsScreen and NoopMotion — seeded with the current value and distinctUntilChanged so an unrelated write to the prefs file cannot re-run the query. Two details carried over from the existing listener sites: a strong local reference, because Android holds these weakly and a listener kept alive only by the register call is collected and silently stops firing; and the null-key case, which arrives when the whole file is cleared and reads as "everything changed". Named for the one key rather than generic, so it sits beside the getter it mirrors and the two cannot drift on the default. `flowOf` had no other caller in the file and its import went with it. NOT unit-tested, and honestly so: the listener needs a real SharedPreferences and this module is deliberately Robolectric-free, so there is no way to pin the re-emit without adding that dependency. Verified by compile, the full unit suite (4294/0) and assembleFullDebug; the behaviour itself needs a device — toggle it and watch the Blood Oxygen tile change without a restart.
Re-review of my own commit. The flow read the current value and THEN registered the listener, so a write landing between the two was missed outright — and since nothing re-reads until the next change, the flow would then serve a stale value indefinitely. That is precisely the failure this function exists to remove, reintroduced in a smaller window. Registering first makes the worst case a duplicate emit, which `distinctUntilChanged` already drops. Full suite 4294/0, assembleFullDebug clean.
Every other NoopPrefs accessor is a point read that returns before the caller's Context can matter. This one captures it in a flow that lives as long as something collects, so an Activity passed by a future caller would be retained across a rotation. The only caller today passes an application Context already; resolving it inside means the function no longer depends on callers remembering to. Full suite 4294/0, assembleFullDebug and doc lint clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found while reviewing #1590, which is the second consumer of this flow. Independent of that PR — no conflict, either order.
The bug
flowOfemits once and completes, socombineholds that first value for the ViewModel's whole life. The toggle is read at construction and never again.The symptom is a setting that does nothing:
Both directions, with nothing in the app to indicate the value in use is stale.
iOS reads
PuffinExperiment.spo2CandidateDisplayEnabledper render and has never had the lag, so this was a live parity divergence too.The fix
NoopPrefs.spo2CandidateDisplayFlowwraps the same key in acallbackFlowoverOnSharedPreferenceChangeListener— the idiom already used bySettingsScreen(experiment keys) andNoopMotion— seeded with the current value anddistinctUntilChangedso an unrelated write to the prefs file can't re-run the database union behind this flow.Two details carried over from those existing listener sites rather than rediscovered:
SettingsScreenhas a comment saying exactly this.keyis@Nullableon modern SDKs — it arrives null when the whole file is cleared, which reads as "everything changed". Required to compile, not merely defensive.Named for the one key rather than made generic, so it sits beside the getter it mirrors and the two can't drift on the default.
flowOfhad no other caller inAppViewModel, so its import went with it.Scope
The
flowOf(NoopPrefs…)pattern appears exactly once in the codebase — this line. Not systemic, and now gone.Verification, and one honest gap
compileFullDebugKotlinclean, full unit suite 4294 tests / 0 failures,assembleFullDebugclean, doc lint clean.SharedPreferences, and this module is deliberately Robolectric-free (AndroidDiagnosticsTestand friends note the same constraint), so there's no way to pin the re-emit without adding that dependency for one test. I'd rather say that than claim coverage that isn't there.Android-only — iOS already reads the flag per render.