fix(setup): warm ML Kit barcode scanner at startup (v0.2.4.6-alpha) - #53
Conversation
…ut of measure pass
- MainActivity.onCreate: eagerly warm BarcodeScanning static holder once
on the main thread via reflection. Eliminates the cold-process race
where the static <clinit> could be entered from a Compose measure
pass and NPE on the lib's internal zzg.zza (zzi) field.
- InlineQrScannerCard: move BarcodeScanning.getClient(options) out of
remember{} into a LaunchedEffect(Unit) wrapped in runCatching. If the
call still fails for any reason, surface it via the existing
scannerError state so the manual-entry button remains usable.
- Bump versionName 0.2.4.5 -> 0.2.4.6, versionCode 8245 -> 8246.
- Update CLAUDE.md git-workflow section to use feature branches + PR
(replaces the old 'commit and push immediately' rule).
Fixes the startup crash when adding an i3 (Sinocare iCan) sensor.
|
| Filename | Overview |
|---|---|
| Common/src/main/java/tk/glucodata/MainActivity.java | Adds warmUpBarcodeScanner() in onCreate to eagerly initialise ML Kit; the reflective getMethod("setBarcodeFormats", int[].class) call always throws NoSuchMethodException because the real API is setBarcodeFormats(int, int[]), so the warm-up silently logs a failure on every launch for ML Kit flavors. |
| Common/src/mobile/java/tk/glucodata/ui/setup/InlineQrScannerCard.kt | Moves BarcodeScanning.getClient from remember {} to LaunchedEffect(Unit) with runCatching, preventing the NPE race during Compose measure; correctly nullifies the scanner reference before use and on disposal, but the camera-binding DisposableEffect can clear a scannerError set by the LaunchedEffect when keys change. |
| Common/build.gradle | Routine version bump: versionName 0.2.4.5 to 0.2.4.6, versionCode 8245 to 8246. |
| CLAUDE.md | Replaces the old commit-and-push workflow with a feature-branch PR squash-merge flow and adds a detailed release checklist; no code behaviour changes. |
Sequence Diagram
sequenceDiagram
participant MA as MainActivity.onCreate
participant WU as warmUpBarcodeScanner()
participant CE as Compose Engine
participant LE as LaunchedEffect(Unit)
participant BS as BarcodeScanning (ML Kit)
participant AE as analyzerExecutor
MA->>WU: call (main thread)
WU->>WU: getMethod("setBarcodeFormats", int[].class)
WU-->>MA: NoSuchMethodException caught, logs warm-up failed
MA->>CE: initComposeUI()
CE->>CE: measure / layout pass
note over CE: barcodeScanner = null during measure, no NPE
CE->>LE: launch coroutine (main thread, post-measure)
LE->>BS: BarcodeScanning.getClient(scannerOptions) in runCatching
BS-->>LE: BarcodeScanner instance or failure sets scannerError
LE->>CE: "barcodeScanner = instance, state update triggers recompose"
CE->>AE: ImageAnalysis.setAnalyzer
AE->>AE: "val scanner = barcodeScanner, null-check"
AE->>BS: scanner.process(inputImage)
BS-->>AE: Task with List of Barcode
AE->>CE: onScanResult(rawValue) via mainExecutor
Comments Outside Diff (1)
-
Common/src/mobile/java/tk/glucodata/ui/setup/InlineQrScannerCard.kt, line 203-217 (link)Camera-binding
DisposableEffectclearsscannerErrorset by theLaunchedEffectWhen the
LaunchedEffect(Unit)fails to initialize the scanner and setsscannerError, the composable re-renders and the camera-bindingDisposableEffecteventually re-executes (e.g. whenpreviewViewbecomes non-null on the first frame). Inside itselsebranch,scannerErroris reset tonull, silently clearing the error before the user sees it. Subsequent frames skip analysis becausescanner == null, but there is no longer any error indicator — the UI shows a live camera preview with scanning silently doing nothing. The manual-entry button remains available, but the user has no cue to use it.
Reviews (1): Last reviewed commit: "fix(setup): warm ML Kit barcode scanner ..." | Re-trigger Greptile
| optionsBuilderCls.getMethod("setBarcodeFormats", int[].class).invoke( | ||
| optionsBuilderCls.getConstructor().newInstance(), | ||
| new Object[]{ new int[]{ | ||
| barcodeCls.getField("FORMAT_QR_CODE").getInt(null), | ||
| barcodeCls.getField("FORMAT_DATA_MATRIX").getInt(null) | ||
| } } | ||
| ) | ||
| ); |
There was a problem hiding this comment.
Reflection warm-up always fails — wrong method signature
getMethod("setBarcodeFormats", int[].class) looks for the single-parameter overload setBarcodeFormats(int[]), which does not exist. The actual ML Kit API is setBarcodeFormats(int format, int... moreFormats), which compiles to the two-parameter bytecode signature setBarcodeFormats(int, int[]). The result is a NoSuchMethodException on every start for ML Kit-enabled flavors, silently caught by the outer Throwable block, so the log will always show "ML Kit barcode scanner warm-up failed" — never "ML Kit barcode scanner warmed up". The warm-up never actually executes.
The correct reflective call needs two parameter types — int.class for the mandatory first format and int[].class for the varargs remainder — and the invoke call must supply them as two separate arguments rather than wrapping both in a single int[].
Summary
Fixes a startup crash when adding an i3 (Sinocare iCan) sensor —
InlineQrScannerCardcould NPE on a nullzzifield of ML Kit's internalzzgstatic holder whenBarcodeScanning.getClient(options)raced with the Compose measure pass on cold start.Changes
MainActivity.onCreate— eagerly warmBarcodeScanningon the main thread via reflection (flavor-safe: skips if mlkit not on classpath). Removes the cold-init race for all QR-based sensor wizards (iCanHealth, Dexcom, AccuChek, CareSensAir, Anytime, MQ, Sibionics).InlineQrScannerCard— moveBarcodeScanning.getClient(options)out ofremember { }into aLaunchedEffect(Unit)wrapped inrunCatching. On failure, surfaces the error via the existingscannerErrorstate; the manual-entry button still works.Common/build.gradle— bumpversionName0.2.4.5 → 0.2.4.6,versionCode8245 → 8246.CLAUDE.md— replace old commit-and-push-immediately workflow with feature-branch + PR workflow.Root cause (from trace deobfuscation)
Deobfuscated stack:
x28.c(o30)=BarcodeScanning.getClient(BarcodeScannerOptions)→zzg.zzb(opts)readingzzg.zza(thezzifield). The NPE on thezzifield is the well-known standalone-MLKit static-init race when the call happens duringView.measure.Test plan
adb logcat | grep MainActivityshould showML Kit barcode scanner warmed upfrom the activity PID.Build
./gradlew :Common:assembleMobileLibre3SiDexNogoogleRelease -Pno_x86 -Pno_x86_64— nogoogle flavor, suitable for F-Droid / sideload.