平台能力:权限、相机、通知与插件
Flutter 的 UI 是自绘的,但相机、定位、通知、文件系统仍然属于操作系统,框架只能通过插件调用。平台能力的难点从来不是 API 记不住,而是三件事:声明漏了、权限被拒了、插件在该平台没实现。本章按“选插件 → 声明并申请权限 → 常见能力落地 → 不可用时降级”的顺序讲一遍。
插件与 pubspec 依赖
| 能力 | 推荐插件 | 平台差异 |
|---|---|---|
| 权限 | permission_handler | iOS 权限文案必须写进 Info.plist,否则审核被拒 |
| 相册与拍照 | image_picker | 无需自己写预览,适合“选一张图/拍一张” |
| 相机预览 | camera | 要自己管生命周期,前后台切换需重开 |
| 本地通知 | flutter_local_notifications | Android 13+ 需申请通知权限,iOS 需授权 |
| 打开链接/拨号 | url_launcher | Android 11+ 要配 <queries> |
dependencies:
permission_handler: ^11.3.1
image_picker: ^1.1.2
flutter_local_notifications: ^17.2.2
url_launcher: ^6.3.0
path_provider: ^2.1.4
权限:声明 + 运行时申请
权限要过两道关:先在原生工程里声明,再在运行时申请。漏掉第一道,第二道会被系统直接拒绝且不弹提示。
<!-- android/app/src/main/AndroidManifest.xml -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <!-- Android 13+ -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- ios/Runner/Info.plist:文案会显示在系统弹窗上,必须写清用途 -->
<key>NSCameraUsageDescription</key>
<string>用于拍摄商品图片并上传</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>用于选择相册中的图片</string>
Future<bool> ensureCameraPermission(BuildContext context) async {
var status = await Permission.camera.status;
if (status.isGranted) return true;
status = await Permission.camera.request(); // 首次弹窗
if (status.isGranted) return true;
if (status.isPermanentlyDenied && context.mounted) {
// 永久拒绝后系统不再弹窗,只能带用户去应用设置页
await showDialog<void>(context: context, builder: (ctx) => AlertDialog(
title: const Text('需要相机权限'),
content: const Text('请在系统设置中开启相机权限后重试'),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('取消')),
TextButton(onPressed: openAppSettings, child: const Text('去设置')),
],
));
}
return false;
}
相机与相册
final picker = ImagePicker();
final gallery = await picker.pickMultiImage(imageQuality: 80); // 压缩后再上传
// 在原生侧就压缩,比 Dart 侧再缩放省内存
final shot = await picker.pickImage(source: ImageSource.camera, maxWidth: 1920, imageQuality: 85);
if (shot == null) return; // 用户取消
final bytes = await shot.readAsBytes(); // 只读一次,别在 build 里反复解码
用 camera 做实时预览的关键点:CameraController 在 initState 里异步初始化、在 dispose 里释放,并在 AppLifecycleState.inactive 时暂停,否则 iOS 切后台容易黑屏或崩溃。
本地通知
Future<void> initNotifications() async {
final plugin = FlutterLocalNotificationsPlugin(); // 在 main 里初始化并缓存实例
await plugin.initialize(const InitializationSettings(
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
iOS: DarwinInitializationSettings(requestAlertPermission: true),
));
// Android 13+ 必须在运行时申请,否则通知被静默丢弃
await plugin.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()?.requestNotificationsPermission();
}
// 定时通知:本地触发,不依赖服务端推送
await plugin.zonedSchedule(
1, '该喝水了', '起来活动一下', tz.TZDateTime.now(tz.local).add(const Duration(minutes: 30)),
const NotificationDetails(android: AndroidNotificationDetails('reminder', '提醒')),
androidScheduleMode: AndroidScheduleMode.inexactAllowWhileIdle, // 精确闹钟需额外权限
);
打开外链、拨号与邮件
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication); // 跳出应用打开
}
await launchUrl(Uri(scheme: 'tel', path: '10086')); // 拨号页;邮件换成 mailto scheme
Android 11+ 需在 <queries> 中声明要探测的 scheme,否则 canLaunchUrl 恒为 false;iOS 需在 Info.plist 的 LSApplicationQueriesSchemes 里登记自定义 scheme。
文件与目录
final dir = await getApplicationDocumentsDirectory(); // 选文件用 file_picker,目录一律走 path_provider
await File('${dir.path}/favorites.json').writeAsString(jsonEncode(ids)); // 少量数据也可用 shared_preferences
| 目录 | 特点 | 用途 |
|---|---|---|
getTemporaryDirectory | 系统可随时清理 | 图片缓存、临时下载 |
getApplicationDocumentsDirectory | 应用私有、随系统备份 | 用户数据、离线内容 |
插件不可用时的降级
Future<void> scanQr() async {
if (!Platform.isAndroid && !Platform.isIOS) {
showToast('当前平台暂不支持扫码'); // 明确提示,而不是抛异常
return;
}
await _startScan();
}
Web 端读 Platform 会直接抛异常,所以判断顺序是先 kIsWeb、再 Platform,最后才到具体能力实现。
常见坑
| 坑 | 现象 | 正确做法 |
|---|---|---|
| 权限只申请不声明 | iOS 审核被拒、Android 直接返回拒绝 | 先改 AndroidManifest.xml 与 Info.plist |
| 永久拒绝后反复申请 | 弹窗不再出现,用户以为坏了 | 用 isPermanentlyDenied 引导 openAppSettings |
| 大图直接解码 | 内存暴涨、低端机 OOM | pickImage 时限制 maxWidth 与 imageQuality |
小结:平台能力按“选成熟插件、原生侧声明、运行时申请、失败时降级”四步走;权限要同时改 AndroidManifest.xml 与 Info.plist,被永久拒绝时引导到 openAppSettings;相机、通知、外链各有平台前提(Android 13 通知权限、<queries>、LSApplicationQueriesSchemes),最后凡是插件调用都兜住异常与不支持的平台。