
Minimal cross-platform library offers essential camera functionality for app integration. Features live preview, photo capture, camera controls, and gesture support, with a simple, predictable API.
Minimal, Cross‑Platform Camera for Kotlin Multiplatform
Preview · Controller · Config
Kuva (Finnish: image/picture) is a lean Kotlin Multiplatform camera library that provides just the essentials for modern app camera integration. No complex state machines or domain abstractions—just a clean, predictable API that works identically on Android and iOS.
import dev.nathanmkaya.kuva.core.*
import dev.nathanmkaya.kuva.ui.Preview
@Composable
fun CameraScreen(lifecycleOwner: LifecycleOwner) {
val context = rememberPlatformContext()
val host = remember { PreviewHost(context) }
val controller = remember {
createController(
config = Config(
lens = Lens.BACK,
aspectRatio = AspectRatioHint.RATIO_16_9,
enableTapToFocus = true
),
previewHost = host
)
}
val scope = rememberCoroutineScope()
// Automatic lifecycle binding (Android)
DisposableEffect(controller, lifecycleOwner.lifecycle) {
val binding = controller.bindTo(lifecycleOwner.lifecycle, scope)
onDispose { binding.close() }
}
Box(Modifier.fillMaxSize()) {
Preview(controller, host, Modifier.fillMaxSize())
// Tap-to-focus overlay
Box(
Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures { offset ->
val normalizedX = offset.x / size.width
val normalizedY = offset.y / size.height
scope.launch {
controller.tapToFocus(normalizedX, normalizedY)
}
}
}
)
}
}import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.launchFor simpler integration, use the high-level Camera composable:
import dev.nathanmkaya.kuva.core.*
import dev.nathanmkaya.kuva.ui.Camera
@Composable
fun SimpleCameraScreen() {
val lifecycleOwner = LocalLifecycleOwner.current // Android
var controller by remember { mutableStateOf<Controller?>(null) }
Camera(
config = Config(
lens = Lens.BACK,
flash = Flash.OFF,
aspectRatio = AspectRatioHint.RATIO_16_9,
enableTapToFocus = true
),
lifecycleOwner = lifecycleOwner,
modifier = Modifier.fillMaxSize(),
onControllerReady = { controller = it }
)
}Note: On iOS, lifecycle binding is handled automatically by the library.
// Simple capture
val result = controller.capturePhoto()
println("Captured ${result.bytes.size} bytes, ${result.width}x${result.height}")
// Access EXIF orientation (Android) or embedded orientation (iOS)
val orientationTag = result.exifOrientationTag // Android: ExifInterface constant, iOS: null
val rotationDegrees = result.rotationDegrees // Usually 0 (orientation in EXIF)
val mimeType = result.mimeType // "image/jpeg"// Lens switching
val newLens = controller.switchLens() // Returns Lens.FRONT or Lens.BACK
// Flash modes
controller.setFlash(Flash.AUTO)
controller.setTorch(enabled = true)
// Zoom control with bounds
val minZoom = controller.minZoom // e.g., 1.0f
val maxZoom = controller.maxZoom // e.g., 10.0f
controller.setZoom(2.5f)
// Observe zoom changes
val currentZoom by controller.zoomRatio.collectAsState()Access camera controls through the controller:
// Basic controls
val newLens = controller.switchLens()
controller.setFlash(Flash.AUTO)
controller.setTorch(enabled = true)
controller.setZoom(2.5f)
// Observe state
val status by controller.status.collectAsState()
val zoom by controller.zoomRatio.collectAsState()For a complete example with UI controls, see the :kuva-samples module.
Lens.BACK or Lens.FRONT
Flash.OFF, Flash.ON, Flash.AUTO
DEFAULT, RATIO_4_3, RATIO_16_9, SQUARE
true)false)Monitor camera status and handle common errors:
val status by controller.status.collectAsState()
// Status types: Idle, Initializing, Running, Error
try {
controller.start()
} catch (e: Error.PermissionDenied) {
// Request camera permission
} catch (e: Error.CameraInUse) {
// Handle camera in use by another app
}For complete error handling patterns, see ARCHITECTURE.md.
Coming Soon: Kuva will be published to Maven Central.
For now, you can use the library by including it as a Git submodule or composite build:
// settings.gradle.kts
includeBuild("path/to/kuva")
// build.gradle.kts
implementation(project(":kuva"))Android: API 21+ (Android 5.0)
iOS: iOS 13.0+
Add to AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA" />Note: On Android 6.0+ you must also request CAMERA permission at runtime before starting the camera.
Add to Info.plist:
<key>NSCameraUsageDescription</key>
<string>This app uses the camera to capture photos</string>For detailed API reference, see:
Check the :kuva-samples module for a complete example featuring:
Run the sample:
# Android
./gradlew :kuva-samples:installDebug
# iOS (requires Xcode)
./gradlew :kuva-samples:iosAppKuva provides a unified camera API across Android (CameraX) and iOS (AVFoundation) with clean separation between business logic and platform implementations.
For detailed architecture information, see ARCHITECTURE.md.
Kuva focuses on essential camera operations only:
❌ Video recording
❌ RAW/DNG/HEIF capture
❌ Advanced camera modes (portrait, night, HDR)
❌ Multi-lens orchestration
❌ Built-in gallery/storage integration
❌ Frame analysis / ML integration
❌ Complex error taxonomies
Future: These may be added as separate optional modules.
./gradlew build to ensure everything worksMIT License - see LICENSE for details.
Copyright (c) 2025 Nathan Mkaya
Kuva — Finnish for "image" or "picture", representing the library's focus on essential camera functionality with cross-platform clarity.
Minimal, Cross‑Platform Camera for Kotlin Multiplatform
Preview · Controller · Config
Kuva (Finnish: image/picture) is a lean Kotlin Multiplatform camera library that provides just the essentials for modern app camera integration. No complex state machines or domain abstractions—just a clean, predictable API that works identically on Android and iOS.
import dev.nathanmkaya.kuva.core.*
import dev.nathanmkaya.kuva.ui.Preview
@Composable
fun CameraScreen(lifecycleOwner: LifecycleOwner) {
val context = rememberPlatformContext()
val host = remember { PreviewHost(context) }
val controller = remember {
createController(
config = Config(
lens = Lens.BACK,
aspectRatio = AspectRatioHint.RATIO_16_9,
enableTapToFocus = true
),
previewHost = host
)
}
val scope = rememberCoroutineScope()
// Automatic lifecycle binding (Android)
DisposableEffect(controller, lifecycleOwner.lifecycle) {
val binding = controller.bindTo(lifecycleOwner.lifecycle, scope)
onDispose { binding.close() }
}
Box(Modifier.fillMaxSize()) {
Preview(controller, host, Modifier.fillMaxSize())
// Tap-to-focus overlay
Box(
Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures { offset ->
val normalizedX = offset.x / size.width
val normalizedY = offset.y / size.height
scope.launch {
controller.tapToFocus(normalizedX, normalizedY)
}
}
}
)
}
}import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.launchFor simpler integration, use the high-level Camera composable:
import dev.nathanmkaya.kuva.core.*
import dev.nathanmkaya.kuva.ui.Camera
@Composable
fun SimpleCameraScreen() {
val lifecycleOwner = LocalLifecycleOwner.current // Android
var controller by remember { mutableStateOf<Controller?>(null) }
Camera(
config = Config(
lens = Lens.BACK,
flash = Flash.OFF,
aspectRatio = AspectRatioHint.RATIO_16_9,
enableTapToFocus = true
),
lifecycleOwner = lifecycleOwner,
modifier = Modifier.fillMaxSize(),
onControllerReady = { controller = it }
)
}Note: On iOS, lifecycle binding is handled automatically by the library.
// Simple capture
val result = controller.capturePhoto()
println("Captured ${result.bytes.size} bytes, ${result.width}x${result.height}")
// Access EXIF orientation (Android) or embedded orientation (iOS)
val orientationTag = result.exifOrientationTag // Android: ExifInterface constant, iOS: null
val rotationDegrees = result.rotationDegrees // Usually 0 (orientation in EXIF)
val mimeType = result.mimeType // "image/jpeg"// Lens switching
val newLens = controller.switchLens() // Returns Lens.FRONT or Lens.BACK
// Flash modes
controller.setFlash(Flash.AUTO)
controller.setTorch(enabled = true)
// Zoom control with bounds
val minZoom = controller.minZoom // e.g., 1.0f
val maxZoom = controller.maxZoom // e.g., 10.0f
controller.setZoom(2.5f)
// Observe zoom changes
val currentZoom by controller.zoomRatio.collectAsState()Access camera controls through the controller:
// Basic controls
val newLens = controller.switchLens()
controller.setFlash(Flash.AUTO)
controller.setTorch(enabled = true)
controller.setZoom(2.5f)
// Observe state
val status by controller.status.collectAsState()
val zoom by controller.zoomRatio.collectAsState()For a complete example with UI controls, see the :kuva-samples module.
Lens.BACK or Lens.FRONT
Flash.OFF, Flash.ON, Flash.AUTO
DEFAULT, RATIO_4_3, RATIO_16_9, SQUARE
true)false)Monitor camera status and handle common errors:
val status by controller.status.collectAsState()
// Status types: Idle, Initializing, Running, Error
try {
controller.start()
} catch (e: Error.PermissionDenied) {
// Request camera permission
} catch (e: Error.CameraInUse) {
// Handle camera in use by another app
}For complete error handling patterns, see ARCHITECTURE.md.
Coming Soon: Kuva will be published to Maven Central.
For now, you can use the library by including it as a Git submodule or composite build:
// settings.gradle.kts
includeBuild("path/to/kuva")
// build.gradle.kts
implementation(project(":kuva"))Android: API 21+ (Android 5.0)
iOS: iOS 13.0+
Add to AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA" />Note: On Android 6.0+ you must also request CAMERA permission at runtime before starting the camera.
Add to Info.plist:
<key>NSCameraUsageDescription</key>
<string>This app uses the camera to capture photos</string>For detailed API reference, see:
Check the :kuva-samples module for a complete example featuring:
Run the sample:
# Android
./gradlew :kuva-samples:installDebug
# iOS (requires Xcode)
./gradlew :kuva-samples:iosAppKuva provides a unified camera API across Android (CameraX) and iOS (AVFoundation) with clean separation between business logic and platform implementations.
For detailed architecture information, see ARCHITECTURE.md.
Kuva focuses on essential camera operations only:
❌ Video recording
❌ RAW/DNG/HEIF capture
❌ Advanced camera modes (portrait, night, HDR)
❌ Multi-lens orchestration
❌ Built-in gallery/storage integration
❌ Frame analysis / ML integration
❌ Complex error taxonomies
Future: These may be added as separate optional modules.
./gradlew build to ensure everything worksMIT License - see LICENSE for details.
Copyright (c) 2025 Nathan Mkaya
Kuva — Finnish for "image" or "picture", representing the library's focus on essential camera functionality with cross-platform clarity.