flutter学习第 18 节:设备功能调用

在移动应用开发中,调用设备原生功能(如相机、相册、定位等)是提升用户体验的关键。Flutter 提供了丰富的第三方插件,让我们可以轻松实现这些功能。本节课将详细讲解设备功能调用的核心知识点,包括权限管理、相机 / 相册调用、定位获取,并通过综合实例演示实际应用。

一、权限管理:permission_handler 插件

调用设备功能前必须获得用户授权,permission_handler 是 Flutter 中最常用的权限管理插件,支持 Android 和 iOS 平台的几乎所有权限类型。

1. 安装与配置

步骤 1:添加依赖

pubspec.yaml 中添加插件:

yaml 复制代码
dependencies:
  permission_handler: ^12.0.1  # 建议使用最新版本

执行 flutter pub get 安装。

步骤 2:平台权限配置

权限需要在原生配置文件中声明,否则调用时会直接失败。

  • Android 配置
    编辑 android/app/src/main/AndroidManifest.xml,添加需要的权限(根据功能添加):
xml 复制代码
<!-- 相机权限 -->
<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"/>
<!-- 定位权限 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

iOS 配置

编辑 ios/Runner/Info.plist,添加权限描述(用户会看到这些说明):

xml 复制代码
<!-- 相机权限描述 -->
<key>NSCameraUsageDescription</key>
<string>需要相机权限用于拍照</string>
<!-- 相册权限描述 -->
<key>NSPhotoLibraryUsageDescription</key>
<string>需要相册权限用于选择图片</string>
<!-- 定位权限描述 -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>需要定位权限用于获取当前位置</string>

2. 权限操作核心方法

permission_handler 提供了简洁的 API 用于权限检查和请求:

dart 复制代码
import 'package:permission_handler/permission_handler.dart';

// 检查权限状态
Future<bool> checkPermission(Permission permission) async {
  PermissionStatus status = await permission.status;
  return status.isGranted; // 返回是否已授权
}

// 请求权限
Future<bool> requestPermission(Permission permission) async {
  PermissionStatus status = await permission.request();
  return status.isGranted;
}

// 打开应用权限设置页面(当用户拒绝且勾选"不再询问"时使用)
Future<void> openAppSet() async {
  await openAppSettings();
}

3. 常用权限常量

Permission 类包含所有支持的权限,常用的有:

  • Permission.camera:相机权限
  • Permission.photos / Permission.storage:相册 / 存储权限
  • Permission.locationWhenInUse:使用中定位权限
  • Permission.locationAlways:始终允许定位权限

二、调用相机 / 相册:image_picker 插件

image_picker 是 Flutter 官方推荐的媒体选择插件,支持从相机拍照或从相册选择图片 / 视频。

1. 安装与配置

步骤 1:添加依赖

yaml 复制代码
dependencies:
  image_picker: ^1.1.2  # 建议使用最新版本

执行 flutter pub get 安装。

步骤 2:平台额外配置(部分场景需要)

  • iOS 视频选择 :如需选择视频,需在 Info.plist 中添加 NSCameraUsageDescription(同相机权限)
  • Android 10+ 存储 :如需保存图片到公共目录,需在 AndroidManifest.xml 中添加:
xml 复制代码
<application ...>
  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  <!-- Android 10+ 需添加 -->
  <application ... android:requestLegacyExternalStorage="true">
</application>

2. 核心功能实现

(1)从相机拍照

dart 复制代码
import 'package:image_picker/image_picker.dart';

// 从相机获取图片
Future<XFile?> takePhoto() async {
  final ImagePicker picker = ImagePicker();
  // 调用相机,返回XFile(包含图片路径等信息)
  final XFile? photo = await picker.pickImage(
    source: ImageSource.camera, // 来源为相机
    imageQuality: 80, // 图片质量(0-100)
    maxWidth: 1080, // 最大宽度
  );
  return photo;
}

(2)从相册选择图片

dart 复制代码
// 从相册选择图片
Future<XFile?> pickImageFromGallery() async {
  final ImagePicker picker = ImagePicker();
  final XFile? image = await picker.pickImage(
    source: ImageSource.gallery, // 来源为相册
    imageQuality: 80,
  );
  return image;
}

(3)显示选中的图片

获取 XFile 后,可通过 Image.file 显示图片:

dart 复制代码
import 'dart:io';

XFile? selectedImage; // 存储选中的图片

// 拍照后更新图片
void _onTakePhoto() async {
  XFile? photo = await takePhoto();
  if (photo != null) {
    setState(() {
      selectedImage = photo;
    });
  }
}

// 界面中显示图片
Widget buildImagePreview() {
  if (selectedImage == null) {
    return Text("未选择图片");
  }
  return Image.file(
    File(selectedImage!.path),
    width: 300,
    height: 300,
    fit: BoxFit.cover,
  );
}

三、定位功能:geolocator 插件

geolocator 提供了跨平台的定位服务,支持获取经纬度、海拔、速度等信息,还能监听位置变化。

1. 安装与配置

步骤 1:添加依赖

yaml 复制代码
dependencies:
  geolocator: ^14.0.2  # 建议使用最新版本

执行 flutter pub get 安装。

步骤 2:平台权限配置

定位功能需要额外的权限配置(已在 permission_handler 部分添加,这里补充细节):

  • Android
    除了 ACCESS_FINE_LOCATION(精确定位)和 ACCESS_COARSE_LOCATION(粗略定位),如需后台定位,需添加:
xml 复制代码
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>

iOS

如需后台定位,需在 Info.plist 中添加:

xml 复制代码
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>需要始终允许定位权限用于后台定位</string>
<key>UIBackgroundModes</key>
<array>
  <string>location</string>
</array>

2. 核心功能实现

(1)获取当前位置

dart 复制代码
import 'package:geolocator/geolocator.dart';

// 获取当前经纬度
Future<Position?> getCurrentLocation() async {
  // 检查定位服务是否开启
  bool isLocationEnabled = await Geolocator.isLocationServiceEnabled();
  if (!isLocationEnabled) {
    // 提示用户开启定位服务
    return null;
  }

  // 检查定位权限
  LocationPermission permission = await Geolocator.checkPermission();
  if (permission == LocationPermission.denied) {
    // 请求权限
    permission = await Geolocator.requestPermission();
    if (permission != LocationPermission.whileInUse && 
        permission != LocationPermission.always) {
      // 权限被拒绝
      return null;
    }
  }

  // 获取当前位置(最多等待10秒)
  try {
    Position position = await Geolocator.getCurrentPosition(
      desiredAccuracy: LocationAccuracy.high, // 高精度
      timeLimit: Duration(seconds: 10),
    );
    return position; // 包含latitude(纬度)和longitude(经度)
  } catch (e) {
    print("获取位置失败:$e");
    return null;
  }
}

(2)监听位置变化

dart 复制代码
// 监听位置变化(每移动10米或30秒更新一次)
StreamSubscription<Position>? positionStream;

void startListeningLocation() {
  positionStream = Geolocator.getPositionStream(
    locationSettings: LocationSettings(
      accuracy: LocationAccuracy.high,
      distanceFilter: 10, // 移动10米以上才更新
      intervalDuration: Duration(seconds: 30), // 至少30秒更新一次
    ),
  ).listen((Position position) {
    print("当前位置:${position.latitude}, ${position.longitude}");
  });
}

// 停止监听
void stopListeningLocation() {
  if (positionStream != null) {
    positionStream!.cancel();
    positionStream = null;
  }
}

四、综合实例:拍照上传与获取位置

下面实现一个完整页面,包含以下功能:

  1. 检查并请求相机、定位权限
  2. 拍照或从相册选择图片
  3. 获取当前位置经纬度
  4. 模拟图片上传(显示上传状态)

完整代码实现

dart 复制代码
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart';

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

  @override
  State<DeviceDemoPage> createState() => _DeviceDemoPageState();
}

class _DeviceDemoPageState extends State<DeviceDemoPage> {
  XFile? _selectedImage; // 选中的图片
  Position? _currentPosition; // 当前位置
  bool _isUploading = false; // 是否正在上传

  // 检查并请求权限
  Future<bool> _checkAndRequestPermission(Permission permission) async {
    bool isGranted = await checkPermission(permission);
    if (!isGranted) {
      isGranted = await requestPermission(permission);
    }
    return isGranted;
  }

  // 拍照
  void _takePhoto() async {
    bool hasCameraPermission = await _checkAndRequestPermission(Permission.camera);
    if (!hasCameraPermission) {
      _showSnackBar("请授予相机权限");
      return;
    }

    XFile? photo = await ImagePicker().pickImage(
      source: ImageSource.camera,
      imageQuality: 80,
    );
    if (photo != null) {
      setState(() => _selectedImage = photo);
    }
  }

  // 从相册选择
  void _pickFromGallery() async {
    bool hasStoragePermission = await _checkAndRequestPermission(Permission.photos);
    if (!hasStoragePermission) {
      _showSnackBar("请授予相册权限");
      return;
    }

    XFile? image = await ImagePicker().pickImage(
      source: ImageSource.gallery,
      imageQuality: 80,
    );
    if (image != null) {
      setState(() => _selectedImage = image);
    }
  }

  // 获取当前位置
  void _getCurrentLocation() async {
    bool hasLocationPermission = await _checkAndRequestPermission(Permission.locationWhenInUse);
    if (!hasLocationPermission) {
      _showSnackBar("请授予定位权限");
      return;
    }

    Position? position = await getCurrentLocation();
    if (position != null) {
      setState(() => _currentPosition = position);
      _showSnackBar("已获取位置信息");
    } else {
      _showSnackBar("获取位置失败,请检查定位服务");
    }
  }

  // 模拟上传图片
  void _uploadImage() async {
    if (_selectedImage == null) {
      _showSnackBar("请先选择图片");
      return;
    }

    setState(() => _isUploading = true);
    // 模拟网络请求(2秒后完成)
    await Future.delayed(Duration(seconds: 2));
    setState(() => _isUploading = false);
    _showSnackBar("图片上传成功");
  }

  // 显示提示
  void _showSnackBar(String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(message)),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("设备功能演示")),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            // 图片预览
            _selectedImage == null
                ? const Text("未选择图片", style: TextStyle(fontSize: 16))
                : Image.file(
                    File(_selectedImage!.path),
                    width: 300,
                    height: 300,
                    fit: BoxFit.cover,
                  ),
            const SizedBox(height: 20),

            // 操作按钮
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton.icon(
                  onPressed: _takePhoto,
                  icon: const Icon(Icons.camera_alt),
                  label: const Text("拍照"),
                ),
                const SizedBox(width: 16),
                ElevatedButton.icon(
                  onPressed: _pickFromGallery,
                  icon: const Icon(Icons.photo_library),
                  label: const Text("相册"),
                ),
              ],
            ),
            const SizedBox(height: 16),

            // 定位信息
            ElevatedButton.icon(
              onPressed: _getCurrentLocation,
              icon: const Icon(Icons.location_on),
              label: const Text("获取当前位置"),
            ),
            if (_currentPosition != null)
              Padding(
                padding: const EdgeInsets.all(16),
                child: Text(
                  "当前位置:\n纬度:${_currentPosition!.latitude}\n经度:${_currentPosition!.longitude}",
                  textAlign: TextAlign.center,
                  style: const TextStyle(fontSize: 16),
                ),
              ),
            const SizedBox(height: 16),

            // 上传按钮
            _isUploading
                ? const CircularProgressIndicator()
                : ElevatedButton.icon(
                    onPressed: _uploadImage,
                    icon: const Icon(Icons.upload),
                    label: const Text("上传图片"),
                  ),
          ],
        ),
      ),
    );
  }
}

// 权限检查与请求的工具方法(可抽离到工具类)
Future<bool> checkPermission(Permission permission) async {
  return (await permission.status).isGranted;
}

Future<bool> requestPermission(Permission permission) async {
  return (await permission.request()).isGranted;
}

// 定位工具方法(可抽离到工具类)
Future<Position?> getCurrentLocation() async {
  // 检查定位服务是否开启
  if (!await Geolocator.isLocationServiceEnabled()) {
    return null;
  }

  // 检查权限
  LocationPermission permission = await Geolocator.checkPermission();
  if (permission == LocationPermission.denied) {
    permission = await Geolocator.requestPermission();
    if (permission != LocationPermission.whileInUse &&
        permission != LocationPermission.always) {
      return null;
    }
  }

  // 获取位置
  try {
    return await Geolocator.getCurrentPosition(
      desiredAccuracy: LocationAccuracy.high,
      timeLimit: const Duration(seconds: 10),
    );
  } catch (e) {
    return null;
  }
}

代码说明

  1. 权限管理 :通过封装的 checkPermissionrequestPermission 方法,统一处理权限逻辑,确保调用设备功能前已获得授权。
  2. 图片处理 :使用 image_picker 分别实现拍照和相册选择功能,并通过 Image.file 显示选中的图片。
  3. 定位功能 :通过 geolocator 获取经纬度,包含定位服务检查和权限处理,确保定位功能可靠。
  4. 用户体验:添加加载状态(上传时显示进度条)和提示信息(SnackBar),提升交互友好度。

五、扩展知识:其他常用设备功能插件

除了本节课讲解的功能,以下插件也常用于设备功能调用:

  • url_launcher:调用系统浏览器、拨打电话、发送邮件等
  • share_plus:分享文本、图片到其他应用
  • flutter_local_notifications:本地通知功能
  • connectivity_plus:网络连接状态监听
  • package_info_plus:获取应用版本、名称等信息
相关推荐
91刘仁德14 小时前
MYSQL 事务原理及使用
android·mysql·adb
一笑的小酒馆16 小时前
AndroidKMP之瀑布流实现
android
RainyJiang16 小时前
AI时代下,Android的边界正在消失
android·openai·ai编程
Android-Flutter16 小时前
android 内存抖动详解
android
代码的小搬运工18 小时前
KVO学习
学习·ios·cocoa
方白羽18 小时前
为什么 Android 非要用 Intent 传值?
android·ios·harmonyos
dora19 小时前
iOS开发新手的第一行代码
ios
方白羽20 小时前
Android17 重写 MessageQueue,解决 Handler 隐性卡顿
android·app
蜡台21 小时前
Kotlin 零基础完整版实战教程|从语法入门到Android工程实战
android·开发语言·kotlin
律宏阔21 小时前
Android 车载 USB 开发笔记:USB Host、USB 串口、USB-CAN、HID 与系统 API
android