JetpackComposeでバーコード読み取り機能を共通部品化


# はじめに
今回は、以前紹介したJetpackComposeでのバーコード読み取り機能を共通部品として切り出して実装していきます

### コード
細かい説明はコード内にコメントとして置いておきます
```kotlin
// プレビュービューの範囲
private const val PreviewRatio = 16f / 9f
// スキャン間隔
private const val SCAN_INTERVAL = 1500L

@Composable
fun BarCodeCamera(
// バーコード読み取りコールバック
    onBarcodeDetect: (String) -> Unit,
    modifier: Modifier = Modifier,
) {
    var rect = Rect()
    val scope = rememberCoroutineScope()
    val qrCodeFlow = MutableSharedFlow<Barcode>()
    var lastTime = remember { 0L }

// 画像解析UseCase
    val qrCodeAnalyzeUseCase: ImageAnalysis = remember {
        ImageAnalysis.Builder()
            .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
            .build()
            .apply {
                setAnalyzer(
                    Executors.newSingleThreadExecutor(),
                    BarCodeScanner { barcode ->
                        scope.launch {
                        // 指定時間が経過してたらFlowに流すように関数を呼び出す
                            qrCodeFlow.intervalEmit(SCAN_INTERVAL, barcode, lastTime) { currentTime ->
                            // 最後に読み取った時間を更新
                                lastTime = currentTime
                            }
                        }
                    },
                )
            }
    }
    // 起動時にFlowを登録
    LaunchedEffect(qrCodeFlow) {
        qrCodeFlow.sample(100.milliseconds)
            .mapNotNull { it }
            .onEach { barcode ->
            // プレビュービューの範囲内かどうかを判定して範囲内であればコールバックを呼び出す
                if (rect.contains(barcode.boundingBox ?: return@onEach)) {
                    onBarcodeDetect(barcode.rawValue ?: return@onEach)
                }
            }
            .launchIn(scope)
    }
    Box(
        modifier =
        modifier
            .fillMaxWidth()
            // 公式でプレビュービューの表示比率は変えちゃいけないことになってるのでBox自体で制限をかけてその範囲しか読み取ってないように見せる
            .aspectRatio(PreviewRatio)
            // 表示範囲を取得してrectに登録する
            .onGloballyPositioned { layoutCoordinates ->
                rect = Rect(
                    layoutCoordinates.boundsInRoot().left.toInt(),
                    layoutCoordinates.boundsInRoot().top.toInt(),
                    layoutCoordinates.boundsInRoot().right.toInt(),
                    layoutCoordinates.boundsInRoot().bottom.toInt(),
                )
            },
        contentAlignment = Alignment.TopCenter,
    ) {
        val context: Context = LocalContext.current
        val lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current
        // カメラとプレビュービューをAndroidViewで作成
        AndroidView(
            modifier = Modifier.clipToBounds(),
            factory = { factoryContext ->
                val previewView = PreviewView(factoryContext).apply {
                    layoutParams = LinearLayout.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.WRAP_CONTENT,
                    )
                    implementationMode = PreviewView.ImplementationMode.COMPATIBLE
                    scaleType = PreviewView.ScaleType.FILL_CENTER
                }
                val previewUseCase = Preview.Builder()
                    .build()
                    .apply {
                        surfaceProvider = previewView.surfaceProvider
                    }
                    // カメラ起動とUseCaseの登録
                context.startCamera(
                    lifecycleOwner = lifecycleOwner,
                    useCases = arrayOf(previewUseCase, qrCodeAnalyzeUseCase),
                )
                previewView
            },
        )
        // よくある中央がわかるようにするための白線
        HorizontalDivider(
            modifier = Modifier
                .padding(horizontal = 20.dp)
                .align(Alignment.Center),
            color = Color.White,
            thickness = 4.dp,
        )
    }
}

// 指定時間が経過するごとにemitするための拡張関数
private suspend fun <T> MutableSharedFlow<T>.intervalEmit(
    interval: Long,
    value: T,
    lastTime: Long,
    updateTime: (Long) -> Unit,
) {
    val currentTime = Instant.now().toEpochMilli()
    if (currentTime - lastTime > interval) {
        updateTime(currentTime)
        this.emit(value)
    }
}
```

### 最後に
共通化するにあたっていろいろと使いやすいようにブラッシュアップしてみました
どなたかのおやくにたてればさいわ