Flutter 小白入门教程Flutter 小白入门教程
首页
学习指南
项目实战
Flutter 官网
编程指南
首页
学习指南
项目实战
Flutter 官网
编程指南
  • 入门基础

    • 📚 基础教程
    • 第1章 - 认识 Flutter
    • 第2章 - 环境搭建
    • 第3章 - Dart 语言基础
    • 第4章 - 第一个 Flutter 应用
    • 第5章 - Widget 基础
    • 第6章 - 布局系统
    • 第7章 - 状态管理入门
    • 第8章 - 页面导航
    • 第9章 - 资源管理
  • 进阶开发

    • 第10章 - 网络请求
    • 第11章 - 本地存储
    • 第12章 - 对话框与反馈
    • 第13章 - 列表进阶
    • 第14章 - 主题定制
    • 第15章 - 状态管理进阶
    • 第16章 - 动画入门
    • 第17章 - 常用第三方包
  • 调试与发布

    • 第18章 - 调试与性能优化
    • 第19章 - 打包与发布
  • 附录

    • 附录A - UI 框架与组件库推荐
    • 附录B - 项目结构最佳实践
    • 附录C - 国际化配置
    • 附录D - 权限处理

附录D - 权限处理

嗨,朋友!我是长安。

现代移动应用经常需要访问用户的敏感数据,比如相机、相册、位置等。为了保护用户隐私,iOS 和 Android 都有严格的权限管理机制。

这一章,我会教你如何在 Flutter 中正确请求和管理各种权限!

🔐 权限基础知识

为什么需要权限?

  • 保护用户隐私:防止应用未经授权访问敏感数据
  • 提升用户信任:明确告知用户应用需要什么权限
  • 符合平台规范:iOS/Android 都强制要求权限管理

常见权限类型

权限用途AndroidiOS
相机拍照、扫码CAMERANSCameraUsageDescription
相册读取/保存图片READ_EXTERNAL_STORAGENSPhotoLibraryUsageDescription
位置定位、导航ACCESS_FINE_LOCATIONNSLocationWhenInUseUsageDescription
麦克风录音、语音RECORD_AUDIONSMicrophoneUsageDescription
通知推送消息POST_NOTIFICATIONS-
通讯录联系人READ_CONTACTSNSContactsUsageDescription
日历日程管理READ_CALENDARNSCalendarsUsageDescription
存储文件读写READ/WRITE_EXTERNAL_STORAGE-

📦 permission_handler 使用

permission_handler 是 Flutter 中最流行的权限管理包。

1. 添加依赖

# pubspec.yaml
dependencies:
  permission_handler: ^11.3.0

2. 平台配置

Android 配置

<!-- android/app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    
    <!-- 相机权限 -->
    <uses-permission android:name="android.permission.CAMERA"/>
    
    <!-- 相册/存储权限 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <!-- Android 13+ 图片权限 -->
    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
    
    <!-- 位置权限 -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <!-- 后台定位(可选) -->
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
    
    <!-- 麦克风权限 -->
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    
    <!-- 通知权限(Android 13+) -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
    
    <!-- 通讯录权限 -->
    <uses-permission android:name="android.permission.READ_CONTACTS"/>
    <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
    
    <!-- 日历权限 -->
    <uses-permission android:name="android.permission.READ_CALENDAR"/>
    <uses-permission android:name="android.permission.WRITE_CALENDAR"/>
    
    <!-- 蓝牙权限 -->
    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
    
    <application ...>
        ...
    </application>
</manifest>

iOS 配置

<!-- ios/Runner/Info.plist -->
<dict>
    <!-- 相机 -->
    <key>NSCameraUsageDescription</key>
    <string>我们需要使用相机来拍照和扫描二维码</string>
    
    <!-- 相册读取 -->
    <key>NSPhotoLibraryUsageDescription</key>
    <string>我们需要访问相册来选择图片</string>
    
    <!-- 相册写入 -->
    <key>NSPhotoLibraryAddUsageDescription</key>
    <string>我们需要保存图片到相册</string>
    
    <!-- 位置(使用时) -->
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>我们需要获取您的位置来提供定位服务</string>
    
    <!-- 位置(始终) -->
    <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
    <string>我们需要在后台获取您的位置来提供导航服务</string>
    
    <!-- 麦克风 -->
    <key>NSMicrophoneUsageDescription</key>
    <string>我们需要使用麦克风来录制语音</string>
    
    <!-- 通讯录 -->
    <key>NSContactsUsageDescription</key>
    <string>我们需要访问通讯录来添加好友</string>
    
    <!-- 日历 -->
    <key>NSCalendarsUsageDescription</key>
    <string>我们需要访问日历来添加日程提醒</string>
    
    <!-- 蓝牙 -->
    <key>NSBluetoothAlwaysUsageDescription</key>
    <string>我们需要使用蓝牙来连接设备</string>
    <key>NSBluetoothPeripheralUsageDescription</key>
    <string>我们需要使用蓝牙来连接设备</string>
    
    <!-- Face ID -->
    <key>NSFaceIDUsageDescription</key>
    <string>我们需要使用 Face ID 来验证身份</string>
</dict>

3. 基本使用

import 'package:permission_handler/permission_handler.dart';

// 检查权限状态
Future<void> checkCameraPermission() async {
  final status = await Permission.camera.status;
  
  if (status.isGranted) {
    print('相机权限已授权');
  } else if (status.isDenied) {
    print('相机权限被拒绝');
  } else if (status.isPermanentlyDenied) {
    print('相机权限被永久拒绝,需要去设置中开启');
  } else if (status.isRestricted) {
    print('相机权限受限(iOS 家长控制)');
  }
}

// 请求单个权限
Future<void> requestCameraPermission() async {
  final status = await Permission.camera.request();
  
  if (status.isGranted) {
    // 权限已授权,执行相关操作
    openCamera();
  } else if (status.isPermanentlyDenied) {
    // 引导用户去设置页面
    openAppSettings();
  } else {
    // 权限被拒绝
    showPermissionDeniedDialog();
  }
}

// 请求多个权限
Future<void> requestMultiplePermissions() async {
  Map<Permission, PermissionStatus> statuses = await [
    Permission.camera,
    Permission.microphone,
    Permission.photos,
  ].request();
  
  if (statuses[Permission.camera]!.isGranted &&
      statuses[Permission.microphone]!.isGranted) {
    // 相机和麦克风都已授权
    startVideoRecording();
  }
}

🎯 权限请求最佳实践

封装权限服务

// lib/services/permission_service.dart
import 'package:permission_handler/permission_handler.dart';
import 'package:flutter/material.dart';

class PermissionService {
  /// 请求权限的通用方法
  static Future<bool> requestPermission(
    Permission permission, {
    required BuildContext context,
    required String permissionName,
    required String description,
  }) async {
    // 1. 先检查当前状态
    final status = await permission.status;
    
    // 2. 如果已授权,直接返回
    if (status.isGranted) {
      return true;
    }
    
    // 3. 如果被永久拒绝,引导去设置
    if (status.isPermanentlyDenied) {
      final shouldOpenSettings = await _showSettingsDialog(
        context,
        permissionName,
        description,
      );
      if (shouldOpenSettings) {
        await openAppSettings();
      }
      return false;
    }
    
    // 4. 首次请求或之前拒绝过,显示说明后请求
    if (status.isDenied) {
      // 可选:先显示自定义说明对话框
      final shouldRequest = await _showExplanationDialog(
        context,
        permissionName,
        description,
      );
      
      if (!shouldRequest) {
        return false;
      }
      
      // 请求权限
      final result = await permission.request();
      return result.isGranted;
    }
    
    return false;
  }

  /// 显示权限说明对话框
  static Future<bool> _showExplanationDialog(
    BuildContext context,
    String permissionName,
    String description,
  ) async {
    return await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('需要$permissionName权限'),
        content: Text(description),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context, false),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () => Navigator.pop(context, true),
            child: const Text('确定'),
          ),
        ],
      ),
    ) ?? false;
  }

  /// 显示去设置对话框
  static Future<bool> _showSettingsDialog(
    BuildContext context,
    String permissionName,
    String description,
  ) async {
    return await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('$permissionName权限被禁用'),
        content: Text('$description\n\n请前往系统设置中手动开启权限。'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context, false),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () => Navigator.pop(context, true),
            child: const Text('去设置'),
          ),
        ],
      ),
    ) ?? false;
  }
}

使用封装的服务

class CameraPage extends StatelessWidget {
  const CameraPage({super.key});

  Future<void> _openCamera(BuildContext context) async {
    final hasPermission = await PermissionService.requestPermission(
      Permission.camera,
      context: context,
      permissionName: '相机',
      description: '我们需要使用相机来拍照和扫描二维码',
    );

    if (hasPermission) {
      // 打开相机
      _startCamera();
    }
  }

  void _startCamera() {
    // 相机逻辑
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('相机')),
      body: Center(
        child: ElevatedButton(
          onPressed: () => _openCamera(context),
          child: const Text('打开相机'),
        ),
      ),
    );
  }
}

📷 相机权限

完整相机权限处理

import 'package:permission_handler/permission_handler.dart';
import 'package:image_picker/image_picker.dart';

class CameraService {
  final ImagePicker _picker = ImagePicker();

  /// 拍照
  Future<String?> takePhoto(BuildContext context) async {
    // 请求相机权限
    final hasPermission = await _requestCameraPermission(context);
    if (!hasPermission) return null;

    try {
      final XFile? photo = await _picker.pickImage(
        source: ImageSource.camera,
        maxWidth: 1920,
        maxHeight: 1080,
        imageQuality: 85,
      );
      return photo?.path;
    } catch (e) {
      debugPrint('拍照失败: $e');
      return null;
    }
  }

  /// 录制视频
  Future<String?> recordVideo(BuildContext context) async {
    // 需要相机和麦克风权限
    final cameraGranted = await _requestCameraPermission(context);
    final micGranted = await _requestMicrophonePermission(context);
    
    if (!cameraGranted || !micGranted) return null;

    try {
      final XFile? video = await _picker.pickVideo(
        source: ImageSource.camera,
        maxDuration: const Duration(minutes: 5),
      );
      return video?.path;
    } catch (e) {
      debugPrint('录制失败: $e');
      return null;
    }
  }

  Future<bool> _requestCameraPermission(BuildContext context) async {
    final status = await Permission.camera.status;
    
    if (status.isGranted) return true;
    
    if (status.isPermanentlyDenied) {
      _showSettingsDialog(context, '相机');
      return false;
    }
    
    final result = await Permission.camera.request();
    return result.isGranted;
  }

  Future<bool> _requestMicrophonePermission(BuildContext context) async {
    final status = await Permission.microphone.status;
    
    if (status.isGranted) return true;
    
    if (status.isPermanentlyDenied) {
      _showSettingsDialog(context, '麦克风');
      return false;
    }
    
    final result = await Permission.microphone.request();
    return result.isGranted;
  }

  void _showSettingsDialog(BuildContext context, String permissionName) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('需要$permissionName权限'),
        content: Text('请在设置中开启$permissionName权限'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.pop(context);
              openAppSettings();
            },
            child: const Text('去设置'),
          ),
        ],
      ),
    );
  }
}

🖼️ 相册权限

相册读取和保存

import 'package:permission_handler/permission_handler.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';

class PhotoService {
  final ImagePicker _picker = ImagePicker();

  /// 从相册选择图片
  Future<String?> pickImage(BuildContext context) async {
    final hasPermission = await _requestPhotosPermission(context);
    if (!hasPermission) return null;

    try {
      final XFile? image = await _picker.pickImage(
        source: ImageSource.gallery,
        maxWidth: 1920,
        maxHeight: 1080,
        imageQuality: 85,
      );
      return image?.path;
    } catch (e) {
      debugPrint('选择图片失败: $e');
      return null;
    }
  }

  /// 从相册选择多张图片
  Future<List<String>> pickMultipleImages(BuildContext context) async {
    final hasPermission = await _requestPhotosPermission(context);
    if (!hasPermission) return [];

    try {
      final List<XFile> images = await _picker.pickMultiImage(
        maxWidth: 1920,
        maxHeight: 1080,
        imageQuality: 85,
      );
      return images.map((e) => e.path).toList();
    } catch (e) {
      debugPrint('选择图片失败: $e');
      return [];
    }
  }

  /// 从相册选择视频
  Future<String?> pickVideo(BuildContext context) async {
    final hasPermission = await _requestPhotosPermission(context);
    if (!hasPermission) return null;

    try {
      final XFile? video = await _picker.pickVideo(
        source: ImageSource.gallery,
        maxDuration: const Duration(minutes: 10),
      );
      return video?.path;
    } catch (e) {
      debugPrint('选择视频失败: $e');
      return null;
    }
  }

  /// 请求相册权限
  Future<bool> _requestPhotosPermission(BuildContext context) async {
    // Android 13+ 使用新的权限
    Permission permission;
    if (Platform.isAndroid) {
      // Android 13+ (API 33+)
      permission = Permission.photos;
    } else {
      permission = Permission.photos;
    }

    final status = await permission.status;
    
    if (status.isGranted || status.isLimited) {
      return true;
    }
    
    if (status.isPermanentlyDenied) {
      _showSettingsDialog(context, '相册');
      return false;
    }
    
    final result = await permission.request();
    return result.isGranted || result.isLimited;
  }

  void _showSettingsDialog(BuildContext context, String permissionName) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('需要$permissionName权限'),
        content: Text('请在设置中开启$permissionName权限'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.pop(context);
              openAppSettings();
            },
            child: const Text('去设置'),
          ),
        ],
      ),
    );
  }
}

保存图片到相册

import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:permission_handler/permission_handler.dart';
import 'dart:typed_data';

class ImageSaveService {
  /// 保存网络图片到相册
  Future<bool> saveNetworkImage(
    BuildContext context,
    String imageUrl,
  ) async {
    // 请求存储权限
    final hasPermission = await _requestStoragePermission(context);
    if (!hasPermission) return false;

    try {
      // 下载图片
      final response = await Dio().get(
        imageUrl,
        options: Options(responseType: ResponseType.bytes),
      );
      
      // 保存到相册
      final result = await ImageGallerySaver.saveImage(
        Uint8List.fromList(response.data),
        quality: 100,
        name: 'image_${DateTime.now().millisecondsSinceEpoch}',
      );
      
      return result['isSuccess'] ?? false;
    } catch (e) {
      debugPrint('保存图片失败: $e');
      return false;
    }
  }

  /// 保存本地图片到相册
  Future<bool> saveLocalImage(
    BuildContext context,
    String filePath,
  ) async {
    final hasPermission = await _requestStoragePermission(context);
    if (!hasPermission) return false;

    try {
      final result = await ImageGallerySaver.saveFile(filePath);
      return result['isSuccess'] ?? false;
    } catch (e) {
      debugPrint('保存图片失败: $e');
      return false;
    }
  }

  Future<bool> _requestStoragePermission(BuildContext context) async {
    // iOS 使用 photosAddOnly 权限
    // Android 使用 storage 权限
    final permission = Platform.isIOS 
        ? Permission.photosAddOnly 
        : Permission.storage;
    
    final status = await permission.status;
    
    if (status.isGranted) return true;
    
    if (status.isPermanentlyDenied) {
      _showSettingsDialog(context);
      return false;
    }
    
    final result = await permission.request();
    return result.isGranted;
  }

  void _showSettingsDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('需要存储权限'),
        content: const Text('保存图片需要存储权限,请在设置中开启'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.pop(context);
              openAppSettings();
            },
            child: const Text('去设置'),
          ),
        ],
      ),
    );
  }
}

📍 定位权限

位置服务

import 'package:permission_handler/permission_handler.dart';
import 'package:geolocator/geolocator.dart';

class LocationService {
  /// 获取当前位置
  Future<Position?> getCurrentLocation(BuildContext context) async {
    // 1. 检查位置服务是否开启
    final serviceEnabled = await Geolocator.isLocationServiceEnabled();
    if (!serviceEnabled) {
      _showLocationServiceDialog(context);
      return null;
    }

    // 2. 请求位置权限
    final hasPermission = await _requestLocationPermission(context);
    if (!hasPermission) return null;

    // 3. 获取位置
    try {
      final position = await Geolocator.getCurrentPosition(
        desiredAccuracy: LocationAccuracy.high,
        timeLimit: const Duration(seconds: 10),
      );
      return position;
    } catch (e) {
      debugPrint('获取位置失败: $e');
      return null;
    }
  }

  /// 监听位置变化
  Stream<Position>? watchPosition(BuildContext context) {
    return Geolocator.getPositionStream(
      locationSettings: const LocationSettings(
        accuracy: LocationAccuracy.high,
        distanceFilter: 10, // 移动 10 米才更新
      ),
    );
  }

  /// 计算两点距离(米)
  double calculateDistance(
    double startLat,
    double startLng,
    double endLat,
    double endLng,
  ) {
    return Geolocator.distanceBetween(startLat, startLng, endLat, endLng);
  }

  /// 请求位置权限
  Future<bool> _requestLocationPermission(BuildContext context) async {
    final status = await Permission.locationWhenInUse.status;
    
    if (status.isGranted) return true;
    
    if (status.isPermanentlyDenied) {
      _showSettingsDialog(context);
      return false;
    }
    
    final result = await Permission.locationWhenInUse.request();
    return result.isGranted;
  }

  /// 请求后台定位权限
  Future<bool> requestBackgroundLocation(BuildContext context) async {
    // 先确保有前台定位权限
    final foregroundGranted = await _requestLocationPermission(context);
    if (!foregroundGranted) return false;

    // 再请求后台定位权限
    final status = await Permission.locationAlways.status;
    
    if (status.isGranted) return true;
    
    if (status.isPermanentlyDenied) {
      _showSettingsDialog(context);
      return false;
    }
    
    final result = await Permission.locationAlways.request();
    return result.isGranted;
  }

  void _showLocationServiceDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('位置服务未开启'),
        content: const Text('请开启设备的位置服务'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.pop(context);
              Geolocator.openLocationSettings();
            },
            child: const Text('去设置'),
          ),
        ],
      ),
    );
  }

  void _showSettingsDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('需要位置权限'),
        content: const Text('获取位置需要定位权限,请在设置中开启'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.pop(context);
              openAppSettings();
            },
            child: const Text('去设置'),
          ),
        ],
      ),
    );
  }
}

使用位置服务

class LocationPage extends StatefulWidget {
  const LocationPage({super.key});

  @override
  State<LocationPage> createState() => _LocationPageState();
}

class _LocationPageState extends State<LocationPage> {
  final _locationService = LocationService();
  Position? _currentPosition;
  bool _isLoading = false;

  Future<void> _getLocation() async {
    setState(() => _isLoading = true);

    final position = await _locationService.getCurrentLocation(context);
    
    setState(() {
      _currentPosition = position;
      _isLoading = false;
    });

    if (position != null) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(
            '位置:${position.latitude.toStringAsFixed(6)}, '
            '${position.longitude.toStringAsFixed(6)}',
          ),
        ),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('位置')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            if (_currentPosition != null) ...[
              Text('纬度:${_currentPosition!.latitude}'),
              Text('经度:${_currentPosition!.longitude}'),
              Text('精度:${_currentPosition!.accuracy} 米'),
              const SizedBox(height: 20),
            ],
            ElevatedButton(
              onPressed: _isLoading ? null : _getLocation,
              child: _isLoading
                  ? const SizedBox(
                      width: 20,
                      height: 20,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Text('获取位置'),
            ),
          ],
        ),
      ),
    );
  }
}

🔔 通知权限

请求通知权限

import 'package:permission_handler/permission_handler.dart';

class NotificationService {
  /// 请求通知权限
  Future<bool> requestNotificationPermission(BuildContext context) async {
    // Android 13+ 需要请求通知权限
    // iOS 需要请求通知权限
    final status = await Permission.notification.status;
    
    if (status.isGranted) return true;
    
    if (status.isPermanentlyDenied) {
      _showSettingsDialog(context);
      return false;
    }
    
    // 显示说明对话框
    final shouldRequest = await showDialog<bool>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('开启通知'),
        content: const Text('开启通知后,您将及时收到消息提醒、活动通知等重要信息'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context, false),
            child: const Text('暂不开启'),
          ),
          ElevatedButton(
            onPressed: () => Navigator.pop(context, true),
            child: const Text('立即开启'),
          ),
        ],
      ),
    ) ?? false;
    
    if (!shouldRequest) return false;
    
    final result = await Permission.notification.request();
    return result.isGranted;
  }

  /// 检查通知权限状态
  Future<bool> isNotificationEnabled() async {
    final status = await Permission.notification.status;
    return status.isGranted;
  }

  void _showSettingsDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('通知权限被禁用'),
        content: const Text('请在设置中开启通知权限,以便接收重要消息'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('取消'),
          ),
          ElevatedButton(
            onPressed: () {
              Navigator.pop(context);
              openAppSettings();
            },
            child: const Text('去设置'),
          ),
        ],
      ),
    );
  }
}

🛡️ 权限状态组件

权限检查 Widget

import 'package:permission_handler/permission_handler.dart';

/// 权限状态检查组件
class PermissionGuard extends StatefulWidget {
  final Permission permission;
  final Widget child;
  final Widget Function(BuildContext context)? onDenied;
  final String permissionName;
  final String description;

  const PermissionGuard({
    super.key,
    required this.permission,
    required this.child,
    this.onDenied,
    required this.permissionName,
    required this.description,
  });

  @override
  State<PermissionGuard> createState() => _PermissionGuardState();
}

class _PermissionGuardState extends State<PermissionGuard>
    with WidgetsBindingObserver {
  PermissionStatus? _status;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _checkPermission();
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    // 从设置返回时重新检查权限
    if (state == AppLifecycleState.resumed) {
      _checkPermission();
    }
  }

  Future<void> _checkPermission() async {
    final status = await widget.permission.status;
    if (mounted) {
      setState(() => _status = status);
    }
  }

  Future<void> _requestPermission() async {
    final status = await widget.permission.request();
    if (mounted) {
      setState(() => _status = status);
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_status == null) {
      return const Center(child: CircularProgressIndicator());
    }

    if (_status!.isGranted) {
      return widget.child;
    }

    // 权限被拒绝时显示的内容
    return widget.onDenied?.call(context) ??
        _buildDefaultDeniedWidget(context);
  }

  Widget _buildDefaultDeniedWidget(BuildContext context) {
    return Center(
      child: Padding(
        padding: const EdgeInsets.all(32),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(
              Icons.lock_outline,
              size: 64,
              color: Colors.grey[400],
            ),
            const SizedBox(height: 16),
            Text(
              '需要${widget.permissionName}权限',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 8),
            Text(
              widget.description,
              textAlign: TextAlign.center,
              style: TextStyle(color: Colors.grey[600]),
            ),
            const SizedBox(height: 24),
            if (_status!.isPermanentlyDenied)
              ElevatedButton(
                onPressed: openAppSettings,
                child: const Text('去设置'),
              )
            else
              ElevatedButton(
                onPressed: _requestPermission,
                child: const Text('授权'),
              ),
          ],
        ),
      ),
    );
  }
}

// 使用示例
class CameraPage extends StatelessWidget {
  const CameraPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('相机')),
      body: PermissionGuard(
        permission: Permission.camera,
        permissionName: '相机',
        description: '我们需要使用相机来拍照和扫描二维码',
        child: const CameraPreview(), // 权限授权后显示的内容
      ),
    );
  }
}

📋 权限状态一览表

PermissionStatus 枚举

状态说明处理方式
granted已授权直接使用功能
denied被拒绝(可再次请求)显示说明后请求
permanentlyDenied被永久拒绝引导去设置
restricted受限(iOS 家长控制)提示用户
limited有限授权(iOS 14+ 相册)可以使用
provisional临时授权(iOS 通知)可以发静默通知

判断方法

final status = await Permission.camera.status;

// 常用判断
status.isGranted        // 已授权
status.isDenied         // 被拒绝
status.isPermanentlyDenied  // 永久拒绝
status.isRestricted     // 受限
status.isLimited        // 有限授权(iOS 相册)

📝 最佳实践总结

权限请求原则

  1. 适时请求:在需要使用功能时才请求,不要一启动就请求所有权限
  2. 说明用途:请求前先告知用户为什么需要这个权限
  3. 优雅降级:权限被拒绝时提供替代方案或明确提示
  4. 尊重选择:不要反复弹窗骚扰用户
  5. 引导设置:永久拒绝后引导用户去设置页面

请求时机建议

// ✅ 好的做法:用户点击相关功能时请求
ElevatedButton(
  onPressed: () async {
    final granted = await requestCameraPermission();
    if (granted) openCamera();
  },
  child: const Text('拍照'),
)

// ❌ 不好的做法:App 启动时批量请求所有权限
void main() async {
  await [
    Permission.camera,
    Permission.location,
    Permission.contacts,
    Permission.microphone,
  ].request();  // 用户还不知道这些权限用来干嘛
  
  runApp(const MyApp());
}

💪 练习题

  1. 实现一个完整的相机功能,包含权限请求和错误处理
  2. 实现一个位置打卡功能,获取并显示当前位置
  3. 创建一个权限管理页面,显示各权限状态并支持跳转设置
  4. 实现从相册选择多张图片并上传的功能

🎉 教程完结

恭喜你完成了 Flutter 基础教程的全部内容!现在你已经掌握了:

  • Flutter 开发环境搭建
  • Dart 语言基础
  • Widget 和布局系统
  • 状态管理(setState、Provider、GetX)
  • 页面导航和路由
  • 网络请求和本地存储
  • 动画和主题定制
  • 常用第三方包
  • 调试和性能优化
  • 打包和发布
  • 项目结构最佳实践
  • 国际化和权限处理

现在,是时候开始你自己的 Flutter 项目了!加油!💪


由 编程指南 提供

最近更新: 2026/2/3 16:24
Contributors: 王长安
Prev
附录C - 国际化配置