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.

3m read timeFrom johnoreilly.dev
Post cover image
Table of contents
The shared APIAndroid: CameraX + ML KitiOS: AVCaptureSession + VisionMatching the text to a stopWiring it into the app

Questions 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.

311 Impressions