前言:
对于获取设备权限,这一章的重要性极大,一定注意!尤其是安卓/ios端的朋友,要注意了!这两端的配置很重要哦!
一、定位
本讲核心是讲解 Flutter 开发中,如何获取和管理设备核心权限(相机、相册、定位、存储),以及如何调用设备基础能力(设备信息、网络状态、电池状态)和实现本地通知与推送功能。通过本章学习,可掌握设备权限的申请、校验、管理逻辑,以及设备能力的调用方法,解决开发中“访问设备资源”的核心需求,为开发具备设备交互能力的应用(如拍照上传、位置定位、状态监控、消息提醒)奠定基础。
核心工具:permission_handler(Flutter 主流权限管理插件,统一处理各类设备权限的申请、查询、监听);辅助工具:device_info_plus(设备信息)、connectivity_plus(网络状态)、battery_plus(电池状态)、flutter_local_notifications(本地通知)、firebase_messaging(远程推送,可选)。
Flutter 中权限与设备能力调用的底层核心是“Flutter 插件层 → 原生层(Android/iOS)→ 设备系统层”的三层交互,其结构如下(清晰展示数据流转和权限管控逻辑):
原理说明:Flutter 本身无法直接访问设备底层资源,需通过第三方插件(如 permission_handler)作为“桥梁”,调用 Android/iOS 原生的权限管理接口和设备能力接口;设备系统层负责校验权限合法性,返回资源访问结果,再由插件将原生结果封装成 Flutter 可识别的对象,回调给应用层,完成整个交互流程。
二、分模块案例、属性与注意事项
模块1:权限管理(相机、相册、定位、存储)permission_handler
权限控制严格,都需要申请权限,才能够获得访问能力。
1.1 核心属性(permission_handler 关键API)
-
权限枚举(Permission) :对应四种核心权限,无需自定义,插件直接提供
- Permission.camera:相机权限
- Permission.photos:相册权限(iOS)/ Permission.storage:存储权限(Android,含相册、文件访问)
- Permission.location:定位权限(基础定位);Permission.locationAlways:后台定位权限;Permission.locationWhenInUse:前台定位权限
-
权限状态(PermissionStatus) :判断权限当前状态,核心状态如下
- granted:权限已允许
- denied:权限已拒绝(可再次申请)
- permanentlyDenied:权限永久拒绝(无法再次申请,需引导用户去系统设置开启)
- restricted:权限受限制(系统级限制,如家长控制)
-
核心方法
- Permission.status:查询单个权限状态(返回 Future)
- Permission.request():申请单个权限(返回 Future)
- [PermissionGroup].request():批量申请多个权限(如 [Permission.camera, Permission.photos].request())
- openAppSettings():打开当前应用的系统设置页面(用于引导用户开启永久拒绝的权限)
1.2 案例(单个权限申请+批量申请)
import 'package:permission_handler/permission_handler.dart';
// 1. 单个权限申请(以相机为例)
Future<void> requestCameraPermission() async {
// 查询当前相机权限状态
PermissionStatus status = await Permission.camera.status;
if (status == PermissionStatus.granted) {
// 权限已允许,执行相机相关操作(如打开相机)
print("相机权限已允许");
} else if (status == PermissionStatus.denied) {
// 权限未允许,发起申请
status = await Permission.camera.request();
if (status == PermissionStatus.granted) {
print("相机权限申请成功");
} else {
print("相机权限申请失败");
}
} else if (status == PermissionStatus.permanentlyDenied) {
// 权限永久拒绝,引导用户去系统设置开启
print("相机权限已永久拒绝,请去设置开启");
await openAppSettings();
}
}
// 2. 批量申请权限(相机+相册+存储)
Future<void> requestBatchPermissions() async {
Map<Permission, PermissionStatus> statuses = await [
Permission.camera,
Permission.photos, // iOS用photos,Android用storage
Permission.storage,
].request();
// 遍历权限状态
statuses.forEach((permission, status) {
if (status == PermissionStatus.granted) {
print("${permission.toString()} 权限已允许");
} else if (status == PermissionStatus.permanentlyDenied) {
print("${permission.toString()} 权限永久拒绝,请去设置开启");
}
});
}
1.3 注意事项
- 平台适配:Android 和 iOS 权限配置不同,需在各自原生配置文件中添加权限说明(必做,否则申请权限会直接失败):
Android:在 android/app/src/main/AndroidManifest.xml 中添加权限标签(如 )
- 标签必须放在 标签内
- 通常放在 标签之前
- 可以添加多个权限,每个权限一行
iOS:在 ios/Runner/Info.plist 中添加权限描述(如 NSCameraUsageDescription需要相机权限用于拍照上传,描述需明确,否则审核会被拒)
- 权限申请时机:不要一进入应用就批量申请所有权限,应在“即将使用该功能时”申请(如点击“拍照”按钮时,再申请相机权限),提升用户体验。
- 永久拒绝处理:当权限被永久拒绝时,无法通过代码再次申请,必须引导用户进入系统设置开启,否则相关功能无法使用。
- Android 13+ 适配:存储权限拆分更细,需根据需求申请 READ_MEDIA_IMAGES(读取图片)、READ_MEDIA_VIDEO(读取视频)等细分权限。
模块2:设备信息、网络状态、电池状态
2.1 核心工具与属性
| 功能 | 核心工具(插件) | 关键属性/方法 |
|---|---|---|
| 设备信息 | device_info_plus | - AndroidDeviceInfo:获取Android设备信息(如设备型号、系统版本、厂商) |
- IosDeviceInfo:获取iOS设备信息(如设备名称、系统版本、UUID)
- DeviceInfoPlugin().deviceInfo:获取当前设备的统一信息对象 | | 网络状态 | connectivity_plus | - ConnectivityResult:网络状态枚举(wifi、mobile、none)
- Connectivity().checkConnectivity():查询当前网络状态
- Connectivity().onConnectivityChanged:监听网络状态变化(实时回调) | | 电池状态 | battery_plus | - Battery().batteryLevel:获取当前电池电量(0-100)
- Battery().isInBatterySaveMode:判断是否开启省电模式
- Battery().onBatteryStateChanged:监听电池状态变化(充电中、放电中、充满) |
2.2 案例(获取设备信息+监听网络+监听电池)
import 'package:flutter/material.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:battery_plus/battery_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'dart:async';
// 1. 获取设备信息
Future<void> getDeviceInfo(BuildContext context) async {
// 首先请求权限
if (Theme.of(context).platform == TargetPlatform.android) {
// Android需要请求电话状态权限
PermissionStatus status = await Permission.phone.request();
if (status.isGranted) {
// 权限已授予,获取设备信息
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
print("Android设备型号:${androidInfo.model}");
print("Android系统版本:${androidInfo.version.release}");
print("设备厂商:${androidInfo.manufacturer}");
// 显示成功消息
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("设备信息获取成功!"))
);
} else {
// 权限被拒绝
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("需要权限才能获取设备信息"))
);
print("权限被拒绝,无法获取设备信息");
}
} else if (Theme.of(context).platform == TargetPlatform.iOS) {
// iOS通常不需要特殊权限获取设备信息
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
print("iOS设备名称:${iosInfo.name}");
print("iOS系统版本:${iosInfo.systemVersion}");
print("设备UUID:${iosInfo.identifierForVendor}");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("设备信息获取成功!"))
);
}
}
// 2. 监听网络状态(实时)
StreamSubscription? connectivitySubscription;
void listenNetworkStatus() {
connectivitySubscription = Connectivity().onConnectivityChanged.listen((List<ConnectivityResult> results) {
if (results.contains(ConnectivityResult.wifi)) {
print("当前网络:WiFi");
// 执行WiFi环境下的操作(如高清下载)
} else if (results.contains(ConnectivityResult.mobile)) {
print("当前网络:移动数据");
// 执行移动数据环境下的操作(如提醒用户节省流量)
} else if (results.contains(ConnectivityResult.none)) {
print("当前无网络");
// 提示用户检查网络
} else {
print("网络状态未知");
}
});
}
// 3. 监听电池状态(实时)
StreamSubscription? batterySubscription;
void listenBatteryStatus() {
batterySubscription = Battery().onBatteryStateChanged.listen((BatteryState state) {
switch (state) {
case BatteryState.charging:
print("电池正在充电");
break;
case BatteryState.discharging:
print("电池正在放电");
break;
case BatteryState.full:
print("电池已充满");
break;
case BatteryState.connectedNotCharging:
print("设备已连接电源但未充电");
break;
case BatteryState.unknown:
print("电池状态未知");
break;
}
// 获取当前电量
Battery().batteryLevel.then((value) {
print("当前电池电量:$value%");
});
});
}
// 4. 创建完整的Widget页面
class DeviceInfoPage extends StatefulWidget {
const DeviceInfoPage({super.key});
@override
State<DeviceInfoPage> createState() => _DeviceInfoPageState();
}
class _DeviceInfoPageState extends State<DeviceInfoPage> {
StreamSubscription? connectivitySubscription;
StreamSubscription? batterySubscription;
@override
void initState() {
super.initState();
// 启动网络状态监听
listenNetworkStatus();
// 启动电池状态监听
listenBatteryStatus();
}
// 页面销毁时,取消监听(避免内存泄漏)
@override
void dispose() {
connectivitySubscription?.cancel();
batterySubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("设备信息与状态")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => getDeviceInfo(context),
child: const Text("获取设备信息"),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => listenNetworkStatus(),
child: const Text("开始监听网络状态"),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => listenBatteryStatus(),
child: const Text("开始监听电池状态"),
),
],
),
),
);
}
}
// 5. 应用入口
void main() {
runApp(const MaterialApp(
home: DeviceInfoPage(),
));
}
2.3 注意事项
- 插件依赖:需在 pubspec.yaml 中添加对应插件依赖,并执行 pub get(如 device_info_plus: ^9.0.0)。
- 网络监听:需在 AndroidManifest.xml(Android)和 Info.plist(iOS)中添加网络访问权限(Android默认已添加,iOS需开启网络权限)。
<!-- 添加获取设备信息所需的权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
- 内存泄漏:监听网络、电池状态时,会创建 StreamSubscription,页面销毁时必须取消监听,否则会导致内存泄漏。
- 设备兼容性:部分老旧设备可能无法获取完整的设备信息或电池状态,需做异常捕获(try-catch)。
模块3:本地通知与推送
3.1 核心工具与属性(flutter_local_notifications)
-
核心对象:FlutterLocalNotificationsPlugin(全局单例,统一管理通知)
-
初始化配置:
- AndroidInitializationSettings:Android 通知初始化(设置通知图标)
- DarwinInitializationSettings:iOS 通知初始化(设置权限)
- InitializationSettings:统一初始化配置(整合Android和iOS配置)
-
通知属性(NotificationDetails) :
- title:通知标题
- body:通知内容
- android:Android 通知细节(如渠道、优先级、图标)
- iOS:iOS 通知细节(如声音、 badge 数量)
-
核心方法:
- initialize():初始化通知插件
- show():显示即时本地通知
- schedule():显示定时本地通知(如10分钟后提醒)
- cancel():取消单个通知;cancelAll():取消所有通知
3.2 案例(初始化+显示即时通知+定时通知)
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:permission_handler/permission_handler.dart';
// 全局单例
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
// 1. 初始化本地通知
Future<void> initializeLocalNotifications() async {
// 首先请求通知权限
if (await Permission.notification.request().isGranted) {
print("通知权限已授予");
} else {
print("通知权限被拒绝");
return;
}
// Android 配置
const AndroidInitializationSettings androidSettings =
AndroidInitializationSettings('@mipmap/ic_launcher');
// iOS 配置
const DarwinInitializationSettings iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
// 统一初始化配置
const InitializationSettings initializationSettings =
InitializationSettings(android: androidSettings, iOS: iosSettings);
// 初始化插件
await flutterLocalNotificationsPlugin.initialize(
settings:initializationSettings,
onDidReceiveNotificationResponse: (NotificationResponse response) {
if (response.payload != null) {
print("点击通知,payload:${response.payload}");
}
},
);
}
// 2. 显示即时本地通知
Future<void> showInstantNotification(BuildContext context) async {
try {
const AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
'channel_id_1',
'即时通知渠道',
importance: Importance.max,
priority: Priority.high,
);
const DarwinNotificationDetails iosNotificationDetails = DarwinNotificationDetails(
badgeNumber: 1,
);
const NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails,
iOS: iosNotificationDetails,
);
await flutterLocalNotificationsPlugin.show(
id:1,
title:'新消息提醒',
body:'您有一条新的系统通知,请查收',
notificationDetails:notificationDetails,
payload: 'notification_payload_1',
);
// 显示成功消息
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('通知发送成功!'))
);
print('通知发送成功');
} catch (e) {
// 显示错误消息
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('通知发送失败: $e'))
);
print('通知发送失败: $e');
}
}
// 3. 创建通知测试页面
class NotificationTestPage extends StatefulWidget {
const NotificationTestPage({super.key});
@override
State<NotificationTestPage> createState() => _NotificationTestPageState();
}
class _NotificationTestPageState extends State<NotificationTestPage> {
@override
void initState() {
super.initState();
initializeLocalNotifications();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("本地通知测试")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => showInstantNotification(context),
child: const Text("发送即时通知"),
),
],
),
),
);
}
}
// 4. 应用入口
void main() {
runApp(const MaterialApp(
home: NotificationTestPage(),
));
}
3.3 注意事项
安卓
<!-- 添加本地通知权限 -->
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
- Android 8.0+ 适配:必须创建通知渠道(AndroidNotificationDetails 中的 channelId),否则通知无法显示,且渠道名称、描述需清晰。
- 通知图标:Android 通知图标需为透明背景的矢量图(mipmap 目录下),否则会显示默认图标;iOS 通知图标需配置正确的尺寸。
- 定时通知:需初始化时区(tz.initializeTimeZones()),否则定时时间会异常;Android 休眠时,需设置 androidAllowWhileIdle: true,确保通知能正常触发。
- 权限申请:iOS 需通过代码申请通知权限,Android 需在 AndroidManifest.xml 中添加通知权限(,Android 13+ 必须)。
- 远程推送:若需实现远程推送(如服务器推送消息),可搭配 firebase_messaging 插件,需额外配置 Firebase 项目,流程与本地通知类似,多了“接收远程消息”的回调。
四、综合应用案例
案例需求
开发一个“设备状态监控+拍照上传+通知提醒”的综合功能:
- 进入页面后,批量申请相机、相册、存储权限;
- 权限允许后,显示当前设备信息、网络状态、电池状态;
- 点击“拍照”按钮,调用相机拍照,保存到相册(需存储权限);
- 拍照成功后,显示本地通知提醒“拍照成功,已保存到相册”;
- 实时监听网络状态和电池状态,状态变化时在控制台打印日志;
- 若权限被拒绝,提示用户去设置开启;若无网络,提示用户检查网络。
完整代码实现
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:battery_plus/battery_plus.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
import 'dart:async';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tz;
// 全局单例
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
final ImagePicker picker = ImagePicker();
// 初始化本地通知(全局方法)
Future<void> _initializeLocalNotifications() async {
// 创建通知渠道(Android 8.0+ 需要)
const AndroidNotificationChannel channel = AndroidNotificationChannel(
'channel_id_1',
'拍照通知渠道',
description: '用于拍照成功通知',
importance: Importance.max,
);
// 创建通知渠道
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(channel);
// 初始化设置
const AndroidInitializationSettings androidSettings =
AndroidInitializationSettings('@mipmap/ic_launcher');
const DarwinInitializationSettings darwinSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const InitializationSettings initializationSettings = InitializationSettings(
android: androidSettings,
iOS: darwinSettings,
macOS: darwinSettings,
);
// 初始化插件
await flutterLocalNotificationsPlugin.initialize(
settings: initializationSettings,
onDidReceiveNotificationResponse: (NotificationResponse response) {
print("通知被点击: ${response.payload}");
},
);
// 请求通知权限
await flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
}
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 初始化本地通知
await _initializeLocalNotifications();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '权限与设备能力综合案例',
theme: ThemeData(primarySwatch: Colors.blue),
home: const DeviceCapabilityPage(),
);
}
}
class DeviceCapabilityPage extends StatefulWidget {
const DeviceCapabilityPage({super.key});
@override
State<DeviceCapabilityPage> createState() => _DeviceCapabilityPageState();
}
class _DeviceCapabilityPageState extends State<DeviceCapabilityPage> {
// 状态管理
String deviceInfoText = "正在获取设备信息...";
String networkStatusText = "正在获取网络状态...";
String batteryStatusText = "正在获取电池状态...";
String permissionStatusText = "正在检查权限状态...";
File? takenImage; // 拍照后的图片
Map<String, PermissionStatus> permissionStatuses = {};
// 监听订阅
StreamSubscription? connectivitySubscription;
StreamSubscription? batterySubscription;
@override
void initState() {
super.initState();
// 初始化时区(用于本地通知)
tz.initializeTimeZones();
// 延迟执行权限申请,确保context可用
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
// 批量申请权限
requestBatchPermissions();
// 监听网络和电池状态
listenNetworkAndBattery();
}
});
}
// 1. 批量申请权限(相机、相册、存储、通知等)
Future<void> requestBatchPermissions() async {
try {
// 根据平台申请不同的权限
List<Permission> permissions = [
Permission.camera,
Permission.photos,
Permission.storage,
Permission.notification,
];
// 如果是Android,添加更多权限
if (Theme.of(context).platform == TargetPlatform.android) {
permissions.addAll([
Permission.accessMediaLocation,
Permission.manageExternalStorage,
]);
}
Map<Permission, PermissionStatus> statuses = await permissions.request();
// 检查权限状态,获取设备信息
bool allGranted = true;
List<String> deniedPermissions = [];
statuses.forEach((permission, status) {
if (status != PermissionStatus.granted) {
allGranted = false;
deniedPermissions.add(_getPermissionName(permission));
if (status == PermissionStatus.permanentlyDenied) {
_showPermissionDialog(
"权限被永久拒绝",
"${_getPermissionName(permission)} 权限已永久拒绝,请去系统设置开启",
);
} else if (status == PermissionStatus.denied) {
_showPermissionDialog(
"权限被拒绝",
"${_getPermissionName(permission)} 权限被拒绝,某些功能将无法使用",
);
}
}
});
// 所有权限都允许,获取设备信息
if (allGranted) {
await getDeviceInfo();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("所有权限已授权,应用功能已启用")),
);
} else {
setState(() {
deviceInfoText = "部分权限未授权,某些功能可能受限\n被拒绝的权限:${deniedPermissions.join(', ')}";
});
}
} catch (e) {
setState(() {
deviceInfoText = "权限申请失败:$e";
});
print("权限申请错误:$e");
}
}
// 获取权限的中文名称
String _getPermissionName(Permission permission) {
switch (permission) {
case Permission.camera:
return "相机";
case Permission.photos:
return "相册";
case Permission.storage:
return "存储";
case Permission.notification:
return "通知";
case Permission.accessMediaLocation:
return "媒体位置";
case Permission.manageExternalStorage:
return "外部存储管理";
default:
return permission.toString();
}
}
// 显示权限对话框
void _showPermissionDialog(String title, String content) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(title),
content: Text(content),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("取消"),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
await openAppSettings();
},
child: const Text("去设置"),
),
],
),
);
}
// 检查当前权限状态
Future<void> checkCurrentPermissions() async {
try {
List<Permission> permissions = [
Permission.camera,
Permission.photos,
Permission.storage,
Permission.notification,
];
if (Theme.of(context).platform == TargetPlatform.android) {
permissions.addAll([
Permission.accessMediaLocation,
Permission.manageExternalStorage,
]);
}
Map<String, PermissionStatus> statusMap = {};
for (var permission in permissions) {
PermissionStatus status = await permission.status;
statusMap[_getPermissionName(permission)] = status;
}
setState(() {
permissionStatuses = statusMap;
permissionStatusText = "权限状态检查完成";
});
} catch (e) {
setState(() {
permissionStatusText = "权限状态检查失败:$e";
});
}
}
// 获取权限状态的中文描述
String _getPermissionStatusText(PermissionStatus status) {
switch (status) {
case PermissionStatus.granted:
return "已授权";
case PermissionStatus.denied:
return "已拒绝";
case PermissionStatus.permanentlyDenied:
return "永久拒绝";
case PermissionStatus.restricted:
return "受限";
case PermissionStatus.limited:
return "有限授权";
case PermissionStatus.provisional:
return "临时授权";
default:
return "未知";
}
}
// 获取权限状态的颜色
Color _getPermissionStatusColor(PermissionStatus status) {
switch (status) {
case PermissionStatus.granted:
return Colors.green;
case PermissionStatus.denied:
return Colors.orange;
case PermissionStatus.permanentlyDenied:
return Colors.red;
case PermissionStatus.restricted:
return Colors.purple;
case PermissionStatus.limited:
return Colors.blue;
case PermissionStatus.provisional:
return Colors.cyan;
default:
return Colors.grey;
}
}
// 2. 获取设备信息
Future<void> getDeviceInfo() async {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
String info = "";
if (Theme.of(context).platform == TargetPlatform.android) {
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
info = "设备型号:${androidInfo.model}\n系统版本:${androidInfo.version.release}\n厂商:${androidInfo.manufacturer}";
} else if (Theme.of(context).platform == TargetPlatform.iOS) {
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
info = "设备名称:${iosInfo.name}\n系统版本:${iosInfo.systemVersion}\nUUID:${iosInfo.identifierForVendor}";
}
setState(() {
deviceInfoText = info;
});
}
// 3. 监听网络和电池状态
void listenNetworkAndBattery() {
// 监听网络状态
connectivitySubscription = Connectivity().onConnectivityChanged.listen((result) {
String status = "";
if (result.contains(ConnectivityResult.wifi)) {
status = "当前网络:WiFi";
} else if (result.contains(ConnectivityResult.mobile)) {
status = "当前网络:移动数据";
} else if (result.contains(ConnectivityResult.none)) {
status = "当前无网络";
// 提示无网络
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("无网络连接,请检查网络设置")),
);
}
} else {
status = "网络状态未知";
}
if (mounted) {
setState(() {
networkStatusText = status;
});
}
print(networkStatusText);
});
// 监听电池状态
batterySubscription = Battery().onBatteryStateChanged.listen((state) {
String status = "";
switch (state) {
case BatteryState.charging:
status = "电池状态:正在充电";
break;
case BatteryState.discharging:
status = "电池状态:正在放电";
break;
case BatteryState.full:
status = "电池状态:已充满";
break;
case BatteryState.unknown:
status = "电池状态:未知";
break;
case BatteryState.connectedNotCharging:
status = "电池状态:已连接但未充电";
break;
default:
status = "电池状态:未知状态";
}
// 获取电池电量
Battery().batteryLevel.then((level) {
if (mounted) {
setState(() {
batteryStatusText = "$status\n当前电量:$level%";
});
}
print(batteryStatusText);
});
});
}
// 4. 拍照功能
Future<void> takePhoto() async {
// 再次检查相机权限(防止用户中途关闭权限)
PermissionStatus cameraStatus = await Permission.camera.status;
if (cameraStatus != PermissionStatus.granted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("相机权限未开启,无法拍照")),
);
return;
}
// 调用相机拍照
final XFile? photo = await picker.pickImage(source: ImageSource.camera);
if (photo != null) {
setState(() {
takenImage = File(photo.path);
});
// 拍照成功,显示本地通知
await showInstantNotification("拍照成功", "照片已保存到相册");
}
}
// 5. 显示本地通知
Future<void> showInstantNotification(String title, String body) async {
const AndroidNotificationDetails androidNotificationDetails = AndroidNotificationDetails(
'channel_id_1',
'拍照通知渠道',
importance: Importance.max,
priority: Priority.high,
);
const NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails,
);
await flutterLocalNotificationsPlugin.show(
id:DateTime.now().millisecondsSinceEpoch, // 用当前时间戳作为通知ID,确保唯一
title: title ,
body: body ,
notificationDetails: notificationDetails,
);
}
@override
void dispose() {
// 取消监听,避免内存泄漏
connectivitySubscription?.cancel();
batterySubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("设备能力综合演示"),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
requestBatchPermissions();
checkCurrentPermissions();
},
tooltip: "刷新权限状态",
),
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 权限管理
const Text("一、权限管理", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(permissionStatusText),
const SizedBox(height: 8),
Row(
children: [
ElevatedButton(
onPressed: requestBatchPermissions,
child: const Text("申请权限"),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: checkCurrentPermissions,
child: const Text("检查权限"),
),
],
),
const SizedBox(height: 12),
// 显示权限状态列表
if (permissionStatuses.isNotEmpty)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: permissionStatuses.entries.map((entry) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: _getPermissionStatusColor(entry.value),
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Expanded(
child: Text("${entry.key}: ${_getPermissionStatusText(entry.value)}"),
),
],
),
);
}).toList(),
),
const SizedBox(height: 16),
// 设备信息
const Text("二、设备信息", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(deviceInfoText),
const SizedBox(height: 16),
// 网络状态
const Text("三、网络状态", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(networkStatusText),
const SizedBox(height: 16),
// 电池状态
const Text("四、电池状态", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(batteryStatusText),
const SizedBox(height: 16),
// 拍照功能
const Text("五、拍照上传", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
ElevatedButton(
onPressed: takePhoto,
child: const Text("点击拍照"),
),
const SizedBox(height: 12),
// 显示拍照后的图片
if (takenImage != null)
Image.file(
takenImage!,
width: double.infinity,
height: 200,
fit: BoxFit.cover,
),
],
),
),
);
}
}
案例说明与注意事项
1. 依赖添加
2. 原生配置(必做)
需要什么权限,增加什么权限
Android(android/app/src/main/AndroidManifest.xml):
<!-- 权限配置 -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- 相机功能声明 -->
<uses-feature android:name="android.hardware.camera" android:required="false"/>
iOS(ios/Runner/Info.plist):<!-- 权限描述 -->
<key>NSCameraUsageDescription</key>
<string>需要相机权限用于拍照上传</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要相册权限用于保存照片</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>需要相册权限用于保存照片</string>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
3. 核心亮点
- 整合了本章所有核心技术:权限管理、设备信息、网络状态、电池状态、本地通知、相机拍照。
- 权限处理完善:批量申请、状态判断、永久拒绝引导、二次校验(拍照前再次检查权限)。
- 用户体验优化:无网络提示、权限不足提示、拍照成功通知、图片预览。
- 内存管理:页面销毁时取消监听,避免内存泄漏。
4. 测试说明
测试时需使用真实设备(模拟器部分功能无法正常使用,如相机、电池状态),分别测试以下场景:
- 权限申请:拒绝权限、允许权限、永久拒绝权限,观察提示是否正常。
- 拍照功能:点击拍照,确认照片能正常预览、通知能正常显示。
- 状态监听:切换网络(WiFi/移动数据/飞行模式)、插拔充电器,观察状态显示是否实时更新。