Flutter相机权限等待 授权后红屏及dispose异常问题
问题根因
- 权限申请无响应:测试permission_handler时误用了
Permission.contacts(联系人权限),相机场景需要调用Permission.camera发起申请;同时未在Android、iOS原生配置文件中声明相机权限的话,系统会直接拒绝权限、不弹出申请弹窗。 - 授权后红屏:
initState执行时直接调用相机初始化逻辑,此时权限申请流程还未完成,相机无访问权限导致初始化抛异常,FutureBuilder未处理异常状态直接触发红屏。 - dispose异常触发:系统弹出权限申请弹窗时,应用会进入
AppLifecycleState.inactive状态,原有生命周期监听逻辑判断错误,只要相机未初始化就会执行dispose,提前释放了还在等待权限的相机实例,授权返回后操作已销毁的实例就会触发生命周期异常。 - 原有生命周期逻辑判断写反:
didChangeAppLifecycleState中判断「相机已初始化就直接返回」,导致应用切后台再回到前台时,已初始化的相机无法正常重建,会出现预览黑屏。
前置原生配置(必须完成,否则权限申请无法正常弹出)
Android 配置
打开android/app/src/main/AndroidManifest.xml,在manifest根节点下添加权限声明:
<uses-permission android:name="android.permission.CAMERA" />
iOS 配置
打开ios/Runner/Info.plist,在dict节点下添加权限说明:
<key>NSCameraUsageDescription</key> <string>需要访问相机拍摄视频</string>
修复后的实现代码
import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:permission_handler/permission_handler.dart'; // 替换成你项目里的实际导入 import 'package:name/Model/FileIO/file_io.dart'; import 'package:name/Views/main_view.dart'; import 'package:name/main.dart'; class CameraView extends StatefulWidget { const CameraView({Key? key}) : super(key: key); @override State<CameraView> createState() => _CameraViewState(); } // 页面状态:加载中、权限通过、权限拒绝 enum _CameraPageState { loading, permissionGranted, permissionDenied } class _CameraViewState extends State<CameraView> with WidgetsBindingObserver { CameraController? controller; Future<void>? _initializeController; List<CameraDescription> rearCameras = List.empty(growable: true); int _currentCameraIndex = 0; _CameraPageState _pageState = _CameraPageState.loading; @override void initState() { super.initState(); SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeRight, DeviceOrientation.landscapeLeft, DeviceOrientation.portraitUp ]); WidgetsBinding.instance.addObserver(this); // 初始化后置相机列表 for (var description in cameras) { if (description.lensDirection == CameraLensDirection.back) { rearCameras.add(description); } } // 第一步先申请权限,不直接初始化相机 _requestCameraPermission(); } // 申请相机权限 Future<void> _requestCameraPermission() async { final status = await Permission.camera.request(); if (status.isGranted) { // 权限通过后再初始化相机 _pageState = _CameraPageState.permissionGranted; if (rearCameras.isNotEmpty) { onNewCameraSelected(rearCameras[_currentCameraIndex]); } } else { _pageState = _CameraPageState.permissionDenied; } if (mounted) setState(() {}); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); // 释放相机控制器 controller?.dispose(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { final CameraController? cameraController = controller; // 控制器为空或者未初始化时直接返回,不执行dispose if (cameraController == null || !cameraController.value.isInitialized) { return; } if (state == AppLifecycleState.inactive) { cameraController.dispose(); } if (state == AppLifecycleState.resumed) { onNewCameraSelected(rearCameras[_currentCameraIndex]); } } Future<void> onNewCameraSelected(CameraDescription description) async { if (controller != null) { await controller!.dispose(); } final CameraController cameraController = CameraController( description, ResolutionPreset.max, enableAudio: false, imageFormatGroup: ImageFormatGroup.jpeg, ); controller = cameraController; cameraController.addListener(() { if (mounted) setState(() {}); if (cameraController.value.hasError) { showInSnackbar("Camera error: ${cameraController.value.errorDescription}"); } }); try { _initializeController = cameraController.initialize(); } on CameraException catch (e) { print('Error initializing camera: $e'); } if (mounted) setState(() {}); } Future<void> _onCameraSwitch() async { if (controller == null) return; _currentCameraIndex = (_currentCameraIndex + 1) % rearCameras.length; final CameraDescription cameraDescription = rearCameras[_currentCameraIndex]; onNewCameraSelected(cameraDescription); } void showInSnackbar(String message) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); } bool isRecording() { return controller != null && controller!.value.isRecordingVideo; } Widget _cameraPreviewWidget(DeviceOrientation orientation) { if (controller != null && controller!.value.isInitialized) { final size = MediaQuery.of(context).size; final deviceRatio = size.width / size.height; controller!.lockCaptureOrientation(orientation); return Center( child: AspectRatio( aspectRatio: deviceRatio, child: CameraPreview(controller!), ), ); } return const Text("Camera not ready or available"); } Widget _captureRowWidget() { final CameraController? cameraController = controller; return Container( color: const Color.fromARGB(50, 255, 255, 255), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ IconButton( icon: const Icon(Icons.videocam), color: Colors.blue, onPressed: cameraController != null && cameraController.value.isInitialized && !cameraController.value.isRecordingVideo ? onVideoRecordButtonPressed : null, ), IconButton( onPressed: isRecording() ? onStopRecordingButtonPressed : null, icon: const Icon(Icons.stop), ), TextButton( onPressed: _onCameraSwitch, child: const Text("Switch Camera")) ], ), ); } void onVideoRecordButtonPressed() { startVideoRecording().then((_) { if (mounted) setState(() {}); }); } void onStopRecordingButtonPressed() { stopVideoRecording().then((video) async { if (video != null) { final path = await localPath; final file = File('$path/video/${timestamp() + ".mp4"}'); await file.create(recursive: true); video.saveTo(file.path); } }); } Future<void> startVideoRecording() async { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isInitialized) { showInSnackbar("Error: Camera unavailable"); return; } if (cameraController.value.isRecordingVideo) return; try { await cameraController.startVideoRecording(); } on CameraException catch (e) { _showCameraException(e); return; } } Future<XFile?> stopVideoRecording() async { final CameraController? cameraController = controller; if (cameraController == null || !cameraController.value.isInitialized) { return null; } try { return cameraController.stopVideoRecording(); } on CameraException catch (e) { _showCameraException(e); return null; } } Widget getBody() { // 权限拒绝状态显示引导 if (_pageState == _CameraPageState.permissionDenied) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text("需要相机权限才能拍摄视频"), const SizedBox(height: 16), ElevatedButton( onPressed: () async { // 跳转到系统设置页开启权限 openAppSettings(); }, child: const Text("去设置开启权限"), ), const SizedBox(height: 8), TextButton( onPressed: () => _requestCameraPermission(), child: const Text("重新申请权限"), ) ], ), ); } return OrientationBuilder( builder: (context, orientation) { var deviceOrientation = DeviceOrientation.landscapeLeft; if (orientation == Orientation.portrait) { deviceOrientation = DeviceOrientation.portraitUp; } return Stack( children: [ _cameraPreviewWidget(deviceOrientation), Positioned( bottom: 2, left: 2, right: 2, child: _captureRowWidget(), ), ], ); }, ); } void _showCameraException(CameraException e) { _logError(e.code, e.description); showInSnackbar('Error: ${e.code}\n${e.description}'); } void _navigateBack() { Navigator.pushAndRemoveUntil( context, MaterialPageRoute( builder: (context) => const MainView(), ), (Route<dynamic> route) => false, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Video Recorder"), leading: GestureDetector( onTap: _navigateBack, child: const Icon(Icons.arrow_back), ), ), body: SafeArea( child: _pageState == _CameraPageState.loading ? const Center(child: CircularProgressIndicator()) : _pageState == _CameraPageState.permissionDenied ? getBody() : FutureBuilder<void>( future: _initializeController, builder: (context, snapshot) { // 处理初始化错误 if (snapshot.hasError) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text("相机初始化失败"), const SizedBox(height: 12), ElevatedButton( onPressed: () => onNewCameraSelected(rearCameras[_currentCameraIndex]), child: const Text("重试"), ) ], ), ); } if (snapshot.connectionState == ConnectionState.done) { return getBody(); } return const Center(child: CircularProgressIndicator()); }, ), ), ); } void _logError(String code, String? message) { if (message != null) { print('Error: $code\nError Message: $message'); } else { print('Error: $code'); } } String timestamp() { return DateTime.now().microsecondsSinceEpoch.toString(); } }
关键修改说明
- 移除了无用的
_ambiguate方法,高版本Flutter不需要该方法做兼容。 - 新增页面状态枚举,权限申请流程完成前不会触发相机初始化,从根源避免无权限时初始化相机导致的报错。
- 修正了生命周期监听的判断逻辑,只有相机已经初始化完成时,才会在应用进入后台时释放相机,避免权限弹窗弹出时误销毁相机实例。
- 补全了
dispose方法中相机控制器的释放逻辑,避免内存泄漏。 - 给FutureBuilder增加了错误状态处理,初始化失败时显示重试按钮,不会直接红屏。
- 新增权限拒绝状态的引导,支持跳转系统设置开权限、重新申请权限。
- 修正了相机预览的判断逻辑,只有相机初始化完成后才会渲染预览组件,避免操作未初始化的控制器。
内容的提问来源于stack exchange,提问作者El_Loco
相关产品推荐
相关产品推荐

