Jetpack Compose中navigation-compose与CameraX搭配使用崩溃问题咨询
问题根因
- 直接在Composable顶层执行
cameraProviderFuture.get():该方法为阻塞方法,会直接阻塞主线程,Future未完成时调用会抛出ExecutionException/InterruptedException,导航操作会触发NavHost页面重组,此时调用该方法大概率直接触发崩溃 - 相机核心实例(cameraProvider、imageCapture、camera、previewView)未使用
remember包裹:Composable触发重组时这些实例会被重新初始化,导致点击拍照时持有的imageCapture为无效实例,甚至直接为空 previewView直接在Composable顶层初始化:未和AndroidView的factory生命周期绑定,多次重组会创建多个PreviewView实例,和相机绑定的实例不一致- 所用依赖均为早期alpha版本:navigation-compose 2.4.0-alpha09和camerax 1.1.0-alpha08本身存在大量已知生命周期适配、状态同步类bug
修复方案
1. 相机核心实例加remember存储,避免重组丢失
@ExperimentalCoilApi @Composable fun CameraPreviewScreen( modifier: Modifier = Modifier, context: Context, lifecycleOwner: LifecycleOwner, outPutDirectory: File, onMediaCaptured: (Uri?) -> Unit ) { val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) } val executor = remember { ContextCompat.getMainExecutor(context) } var imageCapture by remember { mutableStateOf<ImageCapture?>(null) } var camera by remember { mutableStateOf<Camera?>(null) } val previewView = remember { PreviewView(context) } val preview = remember { Preview.Builder().build().also { it.setSurfaceProvider(previewView.surfaceProvider) } } var lensFacing by remember { mutableStateOf(CameraSelector.LENS_FACING_BACK) } var flashEnabled by remember { mutableStateOf(false) } var flashRes by remember { mutableStateOf(R.drawable.ic_outlined_flash_on) }
2. 移除顶层阻塞的get调用,所有相机操作放到异步回调中
Box { AndroidView( factory = { ctx -> cameraProviderFuture.addListener( { // 仅在Future回调中调用get,避免阻塞主线程 val cameraProvider = cameraProviderFuture.get() val cameraSelector = CameraSelector.Builder() .requireLensFacing(lensFacing) .build() val newImageCapture = ImageCapture.Builder() .setTargetRotation(previewView.display.rotation) .build() imageCapture = newImageCapture try { cameraProvider.unbindAll() camera = cameraProvider.bindToLifecycle( lifecycleOwner, cameraSelector, newImageCapture, preview ) } catch (e: Exception) { Log.e("CameraX", "相机绑定失败", e) } }, executor ) previewView }, modifier = modifier )
3. 调整切换摄像头逻辑,避免操作未初始化的实例
// 底部切换摄像头按钮逻辑修改 IconButton( onClick = { val newLensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) CameraSelector.LENS_FACING_FRONT else CameraSelector.LENS_FACING_BACK lensFacing = newLensFacing cameraProviderFuture.addListener({ val cameraProvider = cameraProviderFuture.get() val cameraSelector = CameraSelector.Builder() .requireLensFacing(newLensFacing) .build() val currentImageCapture = imageCapture ?: return@addListener try { cameraProvider.unbindAll() camera = cameraProvider.bindToLifecycle( lifecycleOwner, cameraSelector, currentImageCapture, preview ) } catch (e: Exception) { Log.e("CameraX", "切换摄像头失败", e) } }, executor) }) { Icon( modifier = Modifier.size(34.dp), painter = painterResource(id = R.drawable.ic_outlined_rotate), contentDescription = null, tint = MaterialTheme.colors.surface ) }
4. 拍照回调增加生命周期判断,避免页面销毁后触发导航
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { Log.d(TAG, "image saved: ${photoFile.path}") // 仅页面处于活跃状态时触发回调导航 if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { onMediaCaptured(Uri.fromFile(photoFile)) } }
5. 依赖版本升级
建议将依赖至少升级到正式稳定版,避免alpha版本自带bug:
- navigation-compose升级到2.4.1及以上稳定版
- camerax相关库升级到1.2.0及以上稳定版
内容的提问来源于stack exchange,提问作者Annon
相关产品推荐
相关产品推荐

