Using PetnowCameraHelper
How to implement a fully custom camera UI using PetnowCameraHelper.
Before starting this guide, read the UI Module Overview.
Overview
PetnowCameraHelper is a UI-independent alternative to PetnowCameraFragment. It provides only the camera detection logic without Fragment inheritance, allowing developers to implement a fully custom UI.
PetnowCameraFragment vs PetnowCameraHelper
| Feature | PetnowCameraFragment | PetnowCameraHelper |
|---|---|---|
| Camera preview | Built-in | Implement yourself |
| Detection overlay | Built-in (Lottie) | Implement yourself |
| Camera permissions | Auto-requested | Handle yourself |
| Lifecycle management | Automatic | Manual |
| Compose compatible | Limited | Fully compatible |
| React Native compatible | No | Yes |
| UI flexibility | Overlay layout | 100% custom |
When to Use
- Jetpack Compose based apps
- React Native or Flutter bridges
- Architecture where Fragment inheritance is difficult
- When a fully custom camera UI is required
Architecture
PetnowCameraHelper manages the entire camera detection lifecycle but does not handle UI rendering. State changes are delivered via StateFlow, and events via PetnowCameraDetectionListener callbacks.
Basic Integration
Step 1: Create Helper
import io.petnow.ui.PetnowCameraHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
class CameraScreen(private val context: Context) {
private val scope = CoroutineScope(Dispatchers.Main)
private val cameraHelper = PetnowCameraHelper(context, scope)
}Step 2: Initialize
// PetnowUiClient.initialize() must be called beforehand
suspend fun setup() {
cameraHelper.initialize()
cameraHelper.initializeDetector()
cameraHelper.initializeSpecies(isDog = true) // true: dog, false: cat
}Step 3: Register Listener
import io.petnow.callback.PetnowCameraDetectionListener
import io.petnow.ui.DetectionCaptureResult
import io.petnow.ui.status.PetnowDetectionStatus
cameraHelper.setDetectionListener(object : PetnowCameraDetectionListener {
override fun onDetectionStatus(primaryDetectionStatus: PetnowDetectionStatus) {
// Detection status update (3-second debounce applied)
when (primaryDetectionStatus) {
PetnowDetectionStatus.Detected -> showMessage("Detecting...")
PetnowDetectionStatus.NoObject -> showMessage("Center your pet in the frame")
PetnowDetectionStatus.TooFarAway -> showMessage("Move closer")
PetnowDetectionStatus.TooClose -> showMessage("Move farther away")
else -> showMessage(primaryDetectionStatus.name)
}
}
override fun onDetectionProgress(progress: Int) {
// Progress update (0-100)
updateProgressBar(progress)
}
override fun onDetectionFinished(result: DetectionCaptureResult) {
when (result) {
is DetectionCaptureResult.Success -> {
// result.noseImageFiles: Nose print image files
// result.faceImageFiles: Face image files
handleSuccess(result)
}
is DetectionCaptureResult.Fail -> {
handleFailure()
}
}
}
})Step 4: Open Camera and Start Detection
import java.util.UUID
suspend fun startCapture(surface: Surface, captureSessionId: UUID) {
// Set configuration (optional)
val config = DetectionConfiguration(
species = PetSpecies.DOG,
purpose = DetectionPurpose.PET_PROFILE_REGISTRATION,
requestImageCount = 5
)
cameraHelper.setDetectionConfiguration(config)
// Open camera
cameraHelper.openCamera(surface, captureSessionId)
// Start detection session
cameraHelper.startDetectionSession()
}Step 5: Observe State (StateFlow)
In addition to callbacks, you can observe state via StateFlow. This is especially useful in Compose.
// Observe StateFlow
scope.launch {
cameraHelper.state.collect { state ->
// state.progressPercent: Progress (0-100)
// state.detectionStatusList: Current frame's detection status list
// state.isDetectionFinished: Whether detection is complete
// state.isCameraOpened: Camera open state
// state.isDetectionRunning: Whether detection is running
// state.isTemporaryPause: Temporary pause state
// state.currentCameraId: Current camera ID (front/back)
}
}Step 6: Release Resources
fun cleanup() {
cameraHelper.closeCamera()
cameraHelper.release()
}Detection Session Control
Auto Capture (Default)
cameraHelper.startDetectionSession()Nose-only Capture
cameraHelper.startNoseOnlyDetectionSession()Manual Capture
// Start manual mode
cameraHelper.startManualDetectionSession(maxImageCount = 5)
// Call on each capture button click
cameraHelper.captureManually(captureCountPerClick = 1)Retake
cameraHelper.startDetectionSession()Pause / Resume Detection
You can temporarily pause and resume detection. Useful while displaying overlay UI (e.g., tips sheet).
// Pause detection (progress preserved)
cameraHelper.pauseDetection()
// Resume detection (continues from pause point)
cameraHelper.resumeDetection()pauseDetection() pauses while preserving progress.
resumeDetection() resumes from where it was paused.
For retake (reset progress), use startDetectionSession().
Temporary Pause
When using external UI like a gallery picker, you may need to prevent the camera from closing.
// Before opening gallery
cameraHelper.setTemporaryPause(true)
// After returning from gallery
cameraHelper.setTemporaryPause(false)Check this state in your lifecycle management code:
override fun onPause() {
super.onPause()
// Only close camera when not in temporary pause
if (!cameraHelper.state.value.isTemporaryPause) {
cameraHelper.closeCamera()
}
}Camera Switching
val result = cameraHelper.switchCamera()
result.onSuccess {
// Front ↔ Back switch successful
}
result.onFailure { e ->
// e.g., device has no front camera
}Bracketing Mode
Set bracketing mode for automatic exposure compensation adjustment.
// Must be set before starting detection session
cameraHelper.setBracketingMode(enabled = true)
// Check current state
val isEnabled = cameraHelper.isBracketingModeEnabled()Bracketing mode must be set before calling startDetectionSession().
Changes after session start will take effect from the next session.
Sound Playback
import io.petnow.ui.sound.SoundType
// Play sound
val streamId = cameraHelper.playSound(SoundType.CAPTURE)
// Stop sound
cameraHelper.stopSound(streamId)Jetpack Compose Example
@Composable
fun PetnowCameraScreen(captureSessionId: UUID) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val cameraHelper = remember {
PetnowCameraHelper(context, scope)
}
val cameraState by cameraHelper.state.collectAsState()
DisposableEffect(Unit) {
scope.launch {
cameraHelper.initialize()
cameraHelper.initializeDetector()
cameraHelper.initializeSpecies(isDog = true)
}
onDispose {
cameraHelper.closeCamera()
cameraHelper.release()
}
}
// Set listener
LaunchedEffect(Unit) {
cameraHelper.setDetectionListener(object : PetnowCameraDetectionListener {
override fun onDetectionStatus(status: PetnowDetectionStatus) { /* ... */ }
override fun onDetectionProgress(progress: Int) { /* ... */ }
override fun onDetectionFinished(result: DetectionCaptureResult) { /* ... */ }
})
}
Column(modifier = Modifier.fillMaxSize()) {
// Camera preview (wrap SurfaceView with AndroidView)
AndroidView(
factory = { ctx ->
SurfaceView(ctx).apply {
holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
scope.launch {
cameraHelper.openCamera(holder.surface, captureSessionId)
cameraHelper.startDetectionSession()
}
}
override fun surfaceChanged(h: SurfaceHolder, f: Int, w: Int, ht: Int) {}
override fun surfaceDestroyed(holder: SurfaceHolder) {
cameraHelper.closeCamera()
}
})
}
},
modifier = Modifier.weight(1f)
)
// Progress bar
LinearProgressIndicator(
progress = { cameraState.progressPercent / 100f },
modifier = Modifier.fillMaxWidth()
)
// Detection status text
Text(
text = cameraState.detectionStatusList.firstOrNull()?.name ?: "",
modifier = Modifier.padding(16.dp),
textAlign = TextAlign.Center
)
}
}False-Negative Collection
Collect false-negative images to improve detection quality.
import io.petnow.callback.PetnowCameraFalseNegativeListener
import io.petnow.ui.model.FalseNegativeUiInfo
// Enable false-negative collection
cameraHelper.setFalseNegativeCollect(enable = true, intervalMillis = 1000L)
// Set listener
cameraHelper.setFalseNegativeListener(object : PetnowCameraFalseNegativeListener {
override fun onDetectionFalseNegative(falseNegativeInfo: FalseNegativeUiInfo) {
// Process false-negative data
}
})API Summary
| Method | Description |
|---|---|
initialize() | Initialize monitoring service |
initializeDetector() | Initialize ObjectDetector |
initializeSpecies(isDog) | Set dog/cat mode |
openCamera(surface, captureSessionId) | Open camera |
closeCamera() | Close camera |
startDetectionSession() | Start auto detection session |
startNoseOnlyDetectionSession() | Start nose-only capture session |
startManualDetectionSession(maxImageCount) | Start manual capture session |
captureManually(captureCountPerClick) | Trigger manual capture |
resumeDetection() | Resume paused detection |
pauseDetection() | Pause detection (preserves progress) |
setTemporaryPause(temporary) | Set temporary pause state |
switchCamera() | Switch front/back camera |
setBracketingMode(enabled) | Set bracketing mode |
setDetectionConfiguration(config) | Apply detection configuration |
setDetectionListener(listener) | Register detection listener |
setFalseNegativeCollect(enable, intervalMillis) | False-negative collection |
playSound(soundType) / stopSound(id) | Play/stop sound |
release() | Release resources |
Next Steps
- UI Customization - PetnowCameraFragment UI customization
- Sound Guide - Detailed sound playback guide