Camposer solves the problem of camera access in Kotlin Multiplatform apps. Camera hardware is
entirely platform-specific (Android: CameraX, iOS: AVFoundation), but the user-facing API must be
shared. The library bridges this gap with a unified Compose API over platform-specific engines,
using KMP's expect/actual mechanism as the primary abstraction tool.
┌─────────────────────────────────────────────────────────────┐
│ User Code │
└──────────────────────┬──────────────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────────────┐
│ Public API CameraPreview (@Composable) │
│ CameraSession · CameraController │
│ CameraState · CameraInfo │
└──────────────────────┬──────────────────────────────────────┘
│ state observation (StateFlow.collect)
┌──────────────────────▼──────────────────────────────────────┐
│ Engine CameraEngine (interface, internal) │
│ CameraEngineImpl (expect/actual) │
└──────────────────────┬──────────────────────────────────────┘
│ delegates per-concern
┌──────────────────────▼──────────────────────────────────────┐
│ Appliers PreviewApplier · VideoApplier │
│ ExposureZoomApplier · AnalyzerApplier │
│ SessionTopologyApplier │
└────────────┬──────────────────────────┬─────────────────────┘
│ │
┌────────────▼────────────┐ ┌──────────▼──────────────────────┐
│ AndroidCameraEngine │ │ IOSCameraEngine │
│ CameraX │ │ AVFoundation │
└─────────────────────────┘ └──────────────────────────────────┘
Setting change (e.g. user switches flash mode):
controller.setFlashMode(FlashMode.On)
→ CameraEngine.updateFlashMode(FlashMode.On)
→ CameraEngineImpl delegates to ExposureZoomApplier
→ applier calls platform API
Android: CameraXController.enableTorch() / setFlashMode()
iOS: AVCaptureDevice.flashMode / torchMode
→ cameraState.updateFlashMode(FlashMode.On) [state write — always last]
Still capture (takePicture):
CameraController.takePicture(executor, callback)
→ DefaultTakePictureCommand.execute()
→ platform TakePictureCommand (actual)
Android: ImageCapture.takePicture() via CameraX
iOS: AVCapturePhotoOutput.capturePhoto()
→ CaptureResult.Success(uri) or CaptureResult.Failure(e)
→ callback invoked on caller's executor/dispatcher
Video recording (startRecording / stopRecording):
CameraController.startRecording(...)
→ DefaultRecordController.startRecording()
→ platform RecordController (actual)
Android: VideoCapture.output.prepareRecording() → start()
iOS: IOSRecordController → AVAssetWriter pipeline
→ isRecording = true in CameraSession
CameraController.stopRecording()
→ RecordController.stopRecording()
→ recording finalized, CaptureResult delivered
→ isRecording = false in CameraSession
The entry points for library users:
CameraPreview— root@Composable. Renders the camera feed and owns the UI lifecycle.CameraSession—expectclass, the state holder. Created viarememberCameraSession(). Exposesstate,info,controller, and streaming/initialization flags.
Public state and control surface:
CameraState— all configurable camera properties asMutableStateFlow(selector, flash mode, zoom, capture mode, format, etc.). Mutating a flow triggers the engine.CameraController— user-facing capture API:takePicture,startRecording,stopRecording.CameraInfo/CameraInfoState— runtime capabilities reported by the hardware (supported features, zoom range, exposure compensation range).
Internal coordination (not part of the public API):
CameraEngine— internal interface. Receives state-change commands and applies them to hardware.CameraEngineImpl—expectclass wiring state observations to the applier layer. Each platform provides theactualimplementation.AndroidCameraEngine/IOSCameraEngine— the platformactualimpls. Touch hardware here only.
State application — the Applier layer:
Each applier owns one hardware concern. CameraEngineImpl delegates to them; it does not apply
state directly.
PreviewApplier— preview stream configurationVideoApplier— video capture configurationExposureZoomApplier— exposure compensation and zoom ratioAnalyzerApplier— image analysis pipelineSessionTopologyApplier— use-case binding (which CameraX use cases are active)
Code scanner extension (:camposer-code-scanner):
Optional module. Plugs into the ImageAnalyzer pipeline. Android uses ML Kit; iOS uses the Vision
framework. No dependency on this module from :camposer.
These are constraints that must hold. Violations will likely compile but break things subtly.
CameraEngine is internal. It is never exposed through CameraSession or any public type.
The public API surface is CameraState + CameraController + CameraInfo only.
commonMain has zero platform imports. No CameraX, no AVFoundation, no Android SDK types
anywhere in commonMain. KMP compilation enforces this, but be deliberate about it.
State reaches hardware only through appliers. CameraEngineImpl must not call platform APIs
directly — it delegates to the applier that owns that concern. This keeps each applier testable
and the engine impl readable.
No global/singleton camera state. Each CameraSession instance is independent. Multiple
sessions can coexist (though hardware may not support it). There is no static camera registry.
Public API requires explicit public modifier. explicitApi() is enforced in the Gradle
build. Every public declaration is intentional. ABI is validated by checkKotlinAbi in CI —
adding or removing public API without updating the baseline breaks the build.
commonMain ──── shared: interfaces, state, composables, applier contracts
androidMain / iosMain ── platform: hardware access, actual impls, applier impls
commonTest ──── unit tests via expect/actual fakes (no hardware)
androidDeviceTest ──── instrumented tests (real or emulated Android hardware)
iosTest ──── iOS unit tests (simulator)
The boundary between commonMain and platform source sets is enforced by KMP compilation.
The boundary between public and internal is enforced by explicit API mode + ABI validation.
Key files only — not exhaustive. Paths relative to camposer/src/.
commonMain/kotlin/com/ujizin/camposer/
CameraPreview.kt Root composable, camera UI entry point
session/
CameraSession.kt expect — state holder, public entry point
CameraSessionState.kt internal session lifecycle state machine
state/
CameraState.kt all configurable properties as MutableStateFlow
properties/ enums + value types (FlashMode, CaptureMode, etc.)
info/
CameraInfo.kt hardware capabilities (zoom range, features)
CameraInfoState.kt mutable capabilities holder
controller/
camera/CameraController.kt public capture API (takePicture, record, stop)
record/RecordController.kt internal recording contract
takepicture/TakePictureCommand.kt internal still capture contract
manager/
CameraDevicesManager.kt multi-camera device enumeration
internal/core/
CameraEngine.kt internal interface — state-change commands
CameraEngineImpl.kt expect — wires state observations to appliers
CameraEngineCore.kt shared base logic (non-platform)
applier/
PreviewApplier.kt preview stream configuration contract
VideoApplier.kt video capture configuration contract
ExposureZoomApplier.kt exposure + zoom contract
AnalyzerApplier.kt image analysis pipeline contract
SessionTopologyApplier.kt use-case binding contract
androidMain/kotlin/com/ujizin/camposer/
session/CameraSession.android.kt actual — wraps AndroidCameraEngine
internal/core/
CameraEngineImpl.android.kt actual — delegates to AndroidCameraEngine
AndroidCameraEngine.kt CameraX hardware logic (main Android impl)
camerax/
CameraXController.kt interface over CameraX LifecycleCameraController
CameraXControllerWrapper.kt concrete CameraX controller implementation
applier/*.android.kt Android applier impls (one per concern)
controller/camera/AndroidCameraController.kt CameraX capture implementation
internal/zoom/PinchToZoomController.kt gesture-driven zoom via CameraX
iosMain/kotlin/com/ujizin/camposer/
session/CameraSession.ios.kt actual — wraps IOSCameraEngine
internal/core/
CameraEngineImpl.ios.kt actual — delegates to IOSCameraEngine
IOSCameraEngine.kt AVFoundation hardware logic (main iOS impl)
ios/IOSCameraController.kt interface over AVCaptureSession
applier/*.ios.kt iOS applier impls (one per concern)
internal/view/
CameraViewController.kt UIViewController managing AVFoundation preview
gesture/PinchToZoomGestureHandler.kt pinch-to-zoom gesture recognizer
gesture/TapToFocusGestureHandler.kt tap-to-focus gesture recognizer
internal/controller/IOSRecordController.kt AVAssetWriter-based video recording
KMP expect/actual is used for CameraSession, CameraEngineImpl, and all test fakes
(FakeCameraEngine, FakeCameraTest, FakeCameraSession). Adding a method to any of these
requires updating three files: the common expect declaration plus both platform actual impls.
Coroutines drive all async state. CameraState properties are MutableStateFlow. Platform
engines collect them on appropriate dispatchers. No callbacks cross the public API boundary.
Compose lifecycle manages camera startup and teardown. CameraPreview uses
DisposableEffect to bind/unbind the engine to the composable's lifecycle. There is no
imperative start()/stop() API.