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:获取应用版本、名称等信息
相关推荐
雨白1 小时前
压缩、序列化与哈希
android
安卓开发者2 小时前
RxJava 核心概念解析:构建响应式Android应用的基石
android·echarts·rxjava
Monkey-旭3 小时前
Android ADB 常用指令全解析
android·adb
来来走走3 小时前
Flutter 顶部导航标签组件Tab + TabBar + TabController
android·flutter
丐中丐9994 小时前
Android NFC框架的NfcService与hal层代码概览
android
程序员老刘4 小时前
2025 Google 开发者大会 客户端要点速览
flutter·ai编程·客户端
用户2018792831674 小时前
<include>标签时设置ltr无效?
android
用户2018792831674 小时前
Android多语言与RTL/LTR适配
android
minos.cpp6 小时前
第一章 OkHttp 是怎么发出一个请求的?——整体流程概览
android·okhttp·面试