You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.01 19:31:41