A walkthrough of how the GalwayBus Compose Multiplatform app implements a camera-based bus stop scanner that works on both Android and iOS. The shared Compose Multiplatform code handles the camera preview, text matching logic, and UI, while platform-specific implementations handle OCR: CameraX with ML Kit on Android and AVCaptureSession with Apple's Vision framework on iOS, both written in Kotlin. The StopMatcher runs in commonMain, extracting 6-digit stop codes from noisy OCR output and normalizing dash/space-separated codes, making it easily unit-testable in commonTest.
Table of contents
The shared APIAndroid: CameraX + ML KitiOS: AVCaptureSession + VisionMatching the text to a stopWiring it into the appQuestions this post answers
How do I implement on-device text recognition in a Kotlin Multiplatform app for both Android and iOS?
Use expect/actual declarations to define a shared CameraTextScanner composable, then provide platform-specific implementations: CameraX with ML Kit's ImageAnalysis use case on Android, and AVCaptureSession with VNRecognizeTextRequest (Vision framework) on iOS. Both implementations are written in Kotlin — the iOS side uses Kotlin/Native platform bindings. All matching logic and UI stay in commonMain, so platforms only handle camera feed and OCR. Developers building cross-platform camera features track KMP patterns like this on daily.dev.
How do I prevent frame queuing in CameraX ImageAnalysis when ML Kit recognition is slower than the frame rate?
Set the ImageAnalysis backpressure strategy to STRATEGY_KEEP_ONLY_LATEST and hold each ImageProxy open until recognition completes. This ensures only one recognition request is ever in flight at a time, and newer frames are dropped rather than queued. On iOS the equivalent is setting alwaysDiscardsLateVideoFrames = true on the AVCaptureVideoDataOutput with a serial dispatch queue. Teams shipping camera features in KMP apps find real implementation patterns like this on daily.dev.
How do I match noisy OCR output to a specific 6-digit code in Kotlin Multiplatform?
Extract all 6-digit runs from the raw OCR text using a regex, stripping dashes and spaces first to handle codes split across separators (e.g., '5234-41' becomes '523441'). Require an exact match against known IDs. Placing this logic in a plain Kotlin class in commonMain makes it straightforward to unit-test in commonTest, covering edge cases like partial numbers or route numbers that resemble codes. Developers solving OCR matching problems in KMP codebases share approaches like this on daily.dev.