💬 社区连接https://openharmonycrossplatform.csdn.net


🎯 前言:为什么要关注图片选择器?

想象这样一个场景:用户要上传头像、分享生活瞬间、或者给商品拍照——这些都离不开图片采集功能。在移动应用开发中,图片/视频选择器是「刚需中的刚需」。好消息是,image_picker 插件已经完成了 OpenHarmony 平台的适配,API 12+ 完美支持。

这篇文章不打算做枯燥的 API 翻译,而是用「问题驱动」的方式,带你掌握 image_picker 在鸿蒙平台上的实战技巧。从最基本的单图选择,到复杂的多图预览、视频录制,甚至处理系统回收场景——我们都会通过实际代码一一攻克。


🚀 核心能力一览

先快速了解 image_picker 能做什么:

// 📦 8 种核心操作,一行代码搞定
await picker.pickImage(source: ImageSource.camera);           // 📸 拍照
await picker.pickImage(source: ImageSource.gallery);          // 🖼️ 选图
await picker.pickVideo(source: ImageSource.camera);           // 🎥 录像
await picker.pickVideo(source: ImageSource.gallery);          // 🎬 选视频
await picker.pickMultiImage();                                // 🖼️ 多图选择
await picker.pickMedia();                                      // 📦 单媒体(图/视频)
await picker.pickMultipleMedia();                              // 📱 多媒体选择
await picker.retrieveLostData();                               // 💾 恢复丢失数据

🔑 关键点

  • ✅ 支持相机和相册两种来源
  • ✅ 支持单选/多选图片
  • ✅ 支持视频选择和录制
  • ✅ 可选参数:最大宽高、图片质量
  • ✅ 鸿蒙 API 12+ 完整支持

⚙️ 环境准备:三步走

第一步:添加依赖

pubspec.yaml 中:

dependencies:
  image_picker:
    git:
      url: https://atomgit.com/openharmony-tpc/flutter_packages
      path: packages/image_picker/image_picker

第二步:配置权限

⚠️ 重要:鸿蒙的相机和媒体权限是 system_basic 级别,默认应用权限等级是 normal,需要修改签名模板才能使用。

2.1 配置 module.json5

📄 ohos/entry/src/main/module.json5

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:camera_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.READ_IMAGEVIDEO",
        "reason": "$string:media_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.WRITE_IMAGEVIDEO",
        "reason": "$string:media_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}
2.2 配置权限说明

📄 ohos/entry/src/main/resources/base/element/string.json

{
  "string": [
    {
      "name": "camera_reason",
      "value": "拍摄照片和视频"
    },
    {
      "name": "media_reason",
      "value": "访问和管理媒体文件"
    }
  ]
}
2.3 修改签名模板(开发阶段)

由于 READ_IMAGEVIDEOWRITE_IMAGEVIDEOsystem_basic 级别权限,需要修改 SDK 中的签名模板文件。

📄 签名模板路径:

{SDK路径}/openharmony/toolchains/lib/UnsgnedDebugProfileTemplate.json

修改内容如下:

{
  "bundle-info": {
    "apl": "system_basic"
  },
  "acls": {
    "allowed-acls": [
      "ohos.permission.CAMERA",
      "ohos.permission.READ_IMAGEVIDEO",
      "ohos.permission.WRITE_IMAGEVIDEO"
    ]
  },
  "permissions": {
    "restricted-permissions": [
      "ohos.permission.CAMERA",
      "ohos.permission.READ_IMAGEVIDEO",
      "ohos.permission.WRITE_IMAGEVIDEO"
    ]
  }
}

⚠️ 注意

  • 修改签名模板后,需要在 DevEco Studio 中重新签名才能生效
  • 这是开发阶段的解决方案,正式发布需要通过应用市场审核
2.4 处理安装错误

如果在安装应用时遇到 错误代码 9568289(install failed due to grant request permissions failed),这通常是因为:

  1. 签名模板没有正确修改
  2. 应用权限等级仍然是 normal

解决方法

  1. 确认签名模板文件中的 apl 字段已设置为 system_basic
  2. 在 DevEco Studio 中清理项目并重新签名
  3. 卸载设备上的旧版本应用后再安装

第三步:初始化平台实例

import 'package:image_picker_ohos/image_picker_ohos.dart';
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';

void main() {
  final ImagePickerPlatform impl = ImagePickerPlatform.instance;
  if (impl is ImagePickerOhos) {
    impl.useOhosPhotoPicker = true;  // ✨ 启用鸿蒙相册选择器
  }
  runApp(MyApp());
}

📸 场景一:最简单的单图选择

在这里插入图片描述

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';

void main() {
  runApp(const SimpleImagePickerApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '单图选择示例',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF6366F1)),
        useMaterial3: true,
      ),
      home: const SimpleImagePicker(),
    );
  }
}

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

  
  State<SimpleImagePicker> createState() => _SimpleImagePickerState();
}

class _SimpleImagePickerState extends State<SimpleImagePicker> {
  final ImagePicker _picker = ImagePicker();
  XFile? _image;

  Future<void> _pickFromGallery() async {
    try {
      final XFile? image = await _picker.pickImage(
        source: ImageSource.gallery,
      );
      if (image != null) {
        setState(() => _image = image);
      }
    } catch (e) {
      debugPrint('选图失败: $e');
    }
  }

  Future<void> _takePhoto() async {
    try {
      final XFile? photo = await _picker.pickImage(
        source: ImageSource.camera,
      );
      if (photo != null) {
        setState(() => _image = photo);
      }
    } catch (e) {
      debugPrint('拍照失败: $e');
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('单图选择')),
      body: Center(
        child: _image == null
            ? const Text('请选择或拍摄图片')
            : Image.file(File(_image!.path)),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            heroTag: 'gallery',
            onPressed: _pickFromGallery,
            child: const Icon(Icons.photo_library),
          ),
          const SizedBox(width: 16),
          FloatingActionButton(
            heroTag: 'camera',
            onPressed: _takePhoto,
            child: const Icon(Icons.camera_alt),
          ),
        ],
      ),
    );
  }
}

💡 实战提示

  • 🔸 返回值是 XFile?,一定要检查是否为 null(用户可能取消)
  • 🔸 使用 Image.file() 显示本地图片
  • 🔸 建议用 try-catch 包裹调用,处理权限拒绝等异常

🖼️ 场景二:多图选择 + 网格预览

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';

void main() {
  runApp(const MultiImagePickerApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '多图选择示例',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF10B981)),
        useMaterial3: true,
      ),
      home: const MultiImagePicker(),
    );
  }
}

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

  
  State<MultiImagePicker> createState() => _MultiImagePickerState();
}

class _MultiImagePickerState extends State<MultiImagePicker> {
  final ImagePicker _picker = ImagePicker();
  List<XFile> _images = [];

  Future<void> _pickMultipleImages() async {
    try {
      final List<XFile> images = await _picker.pickMultiImage();
      if (images.isNotEmpty) {
        setState(() => _images = images);
      }
    } catch (e) {
      debugPrint('多图选择失败: $e');
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('多图选择 (${_images.length})'),
        actions: [
          IconButton(
            icon: const Icon(Icons.clear),
            onPressed: () => setState(() => _images.clear()),
          ),
        ],
      ),
      body: _images.isEmpty
          ? const Center(child: Text('请选择多张图片'))
          : GridView.builder(
              padding: const EdgeInsets.all(8),
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 3,
                crossAxisSpacing: 8,
                mainAxisSpacing: 8,
              ),
              itemCount: _images.length,
              itemBuilder: (context, index) {
                return Stack(
                  fit: StackFit.expand,
                  children: [
                    Image.file(
                      File(_images[index].path),
                      fit: BoxFit.cover,
                    ),
                    Positioned(
                      top: 4,
                      right: 4,
                      child: GestureDetector(
                        onTap: () {
                          setState(() => _images.removeAt(index));
                        },
                        child: Container(
                          padding: const EdgeInsets.all(4),
                          decoration: const BoxDecoration(
                            color: Colors.black54,
                            shape: BoxShape.circle,
                          ),
                          child: const Icon(
                            Icons.close,
                            color: Colors.white,
                            size: 16,
                          ),
                        ),
                      ),
                    ),
                  ],
                );
              },
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: _pickMultipleImages,
        child: const Icon(Icons.add_photo_alternate),
      ),
    );
  }
}

💡 实战提示

  • 🔸 pickMultiImage() 返回 List<XFile>,不会是 null
  • 🔸 用 GridView 实现网格预览,用户体验更佳
  • 🔸 添加删除按钮,允许用户移除选错的图片

🗜️ 场景三:图片压缩与质量控制

在这里插入图片描述

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';

void main() {
  runApp(const CompressedImagePickerApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '图片压缩示例',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFFF59E0B)),
        useMaterial3: true,
      ),
      home: const CompressedImagePicker(),
    );
  }
}

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

  
  State<CompressedImagePicker> createState() => _CompressedImagePickerState();
}

class _CompressedImagePickerState extends State<CompressedImagePicker> {
  final ImagePicker _picker = ImagePicker();
  XFile? _image;
  int _imageQuality = 80;
  double _maxWidth = 1080;

  Future<void> _pickWithCompression() async {
    try {
      final XFile? image = await _picker.pickImage(
        source: ImageSource.gallery,
        maxWidth: _maxWidth,
        maxHeight: _maxWidth,
        imageQuality: _imageQuality,
      );
      if (image != null) {
        final bytes = await image.readAsBytes();
        final size = bytes.length / 1024;
        debugPrint('压缩后大小: ${size.toStringAsFixed(2)} KB');
        setState(() => _image = image);
      }
    } catch (e) {
      debugPrint('选图失败: $e');
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('图片压缩')),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              children: [
                Text('图片质量: $_imageQuality'),
                Slider(
                  value: _imageQuality.toDouble(),
                  min: 10,
                  max: 100,
                  divisions: 9,
                  label: '$_imageQuality',
                  onChanged: (value) {
                    setState(() => _imageQuality = value.toInt());
                  },
                ),
                const SizedBox(height: 16),
                Text('最大宽度: ${_maxWidth.toInt()}'),
                Slider(
                  value: _maxWidth,
                  min: 480,
                  max: 1920,
                  divisions: 5,
                  label: '${_maxWidth.toInt()}',
                  onChanged: (value) {
                    setState(() => _maxWidth = value);
                  },
                ),
              ],
            ),
          ),
          Expanded(
            child: Center(
              child: _image == null
                  ? const Text('请选择图片')
                  : Image.file(File(_image!.path)),
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _pickWithCompression,
        child: const Icon(Icons.photo_library),
      ),
    );
  }
}

💡 实战提示

  • 🔸 maxWidth/maxHeight:限制图片尺寸,等比缩放
  • 🔸 imageQuality:0-100,值越小压缩率越高,默认 100(不压缩)
  • 🔸 对于头像等小图,建议设置 maxWidth: 480, imageQuality: 70
  • 🔸 对于高清展示,建议设置 maxWidth: 1080, imageQuality: 85

🎥 场景四:视频录制与选择

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:video_player/video_player.dart';

void main() {
  runApp(const VideoPickerApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '视频选择示例',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFFEF4444)),
        useMaterial3: true,
      ),
      home: const VideoPickerDemo(),
    );
  }
}

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

  
  State<VideoPickerDemo> createState() => _VideoPickerDemoState();
}

class _VideoPickerDemoState extends State<VideoPickerDemo> {
  final ImagePicker _picker = ImagePicker();
  XFile? _video;
  VideoPlayerController? _controller;
  bool _isPlaying = false;

  Future<void> _pickVideo() async {
    try {
      final XFile? video = await _picker.pickVideo(
        source: ImageSource.gallery,
        maxDuration: const Duration(minutes: 5),  // ⏱️ 限制视频时长
      );
      if (video != null) {
        await _initVideoPlayer(video);
      }
    } catch (e) {
      debugPrint('选择视频失败: $e');
    }
  }

  Future<void> _recordVideo() async {
    try {
      final XFile? video = await _picker.pickVideo(
        source: ImageSource.camera,
        maxDuration: const Duration(minutes: 5),
      );
      if (video != null) {
        await _initVideoPlayer(video);
      }
    } catch (e) {
      debugPrint('录制视频失败: $e');
    }
  }

  Future<void> _initVideoPlayer(XFile video) async {
    await _controller?.dispose();
    _controller = VideoPlayerController.file(File(video.path));
    await _controller!.initialize();
    await _controller!.setLooping(true);
    setState(() {
      _video = video;
    });
  }

  
  void dispose() {
    _controller?.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('视频选择')),
      body: Center(
        child: _controller == null || !_controller!.value.isInitialized
            ? const Text('请选择或录制视频')
            : AspectRatio(
                aspectRatio: _controller!.value.aspectRatio,
                child: Stack(
                  alignment: Alignment.center,
                  children: [
                    VideoPlayer(_controller!),
                    if (!_isPlaying)
                      Container(
                        color: Colors.black26,
                        child: const Icon(
                          Icons.play_arrow,
                          color: Colors.white,
                          size: 64,
                        ),
                      ),
                    Positioned(
                      bottom: 8,
                      left: 8,
                      right: 8,
                      child: VideoProgressIndicator(_controller!),
                    ),
                  ],
                ),
              ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            heroTag: 'gallery',
            backgroundColor: Colors.red,
            onPressed: _pickVideo,
            child: const Icon(Icons.video_library),
          ),
          const SizedBox(width: 16),
          FloatingActionButton(
            heroTag: 'camera',
            backgroundColor: Colors.red,
            onPressed: _recordVideo,
            child: const Icon(Icons.videocam),
          ),
        ],
      ),
    );
  }
}

💡 实战提示

  • 🔸 视频播放需要 video_player 插件配合
  • 🔸 maxDuration 参数可限制录制时长
  • 🔸 记得在 dispose 时释放 VideoPlayerController
  • 🔸 鸿蒙平台支持视频选择和录制

📱 场景五:多媒体混合选择(图片+视频)

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';

void main() {
  runApp(const MixedMediaPickerApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '多媒体选择示例',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF8B5CF6)),
        useMaterial3: true,
      ),
      home: const MixedMediaPicker(),
    );
  }
}

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

  
  State<MixedMediaPicker> createState() => _MixedMediaPickerState();
}

class _MixedMediaPickerState extends State<MixedMediaPicker> {
  final ImagePicker _picker = ImagePicker();
  List<XFile> _media = [];

  Future<void> _pickMixedMedia() async {
    try {
      final List<XFile> media = await _picker.pickMultipleMedia();
      if (media.isNotEmpty) {
        setState(() => _media = media);
      }
    } catch (e) {
      debugPrint('选择多媒体失败: $e');
    }
  }

  bool _isVideo(XFile file) {
    final path = file.path.toLowerCase();
    return path.endsWith('.mp4') ||
           path.endsWith('.mov') ||
           path.endsWith('.avi');
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('多媒体选择 (${_media.length})'),
      ),
      body: _media.isEmpty
          ? const Center(child: Text('请选择图片或视频'))
          : ListView.builder(
              padding: const EdgeInsets.all(8),
              itemCount: _media.length,
              itemBuilder: (context, index) {
                final file = _media[index];
                final isVideo = _isVideo(file);

                return Card(
                  margin: const EdgeInsets.symmetric(vertical: 4),
                  child: ListTile(
                    leading: Icon(
                      isVideo ? Icons.videocam : Icons.image,
                      color: isVideo ? Colors.red : Colors.blue,
                    ),
                    title: Text(file.name),
                    subtitle: Text(
                      isVideo ? '视频文件' : '图片文件',
                      style: const TextStyle(fontSize: 12),
                    ),
                    trailing: IconButton(
                      icon: const Icon(Icons.delete),
                      onPressed: () {
                        setState(() => _media.removeAt(index));
                      },
                    ),
                  ),
                );
              },
            ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: _pickMixedMedia,
        label: const Text('选择多媒体'),
        icon: const Icon(Icons.add),
      ),
    );
  }
}

💡 实战提示

  • 🔸 pickMultipleMedia() 可以同时选择图片和视频
  • 🔸 通过文件扩展名判断媒体类型
  • 🔸 用不同的图标区分图片和视频,提升识别度

🛡️ 场景六:处理系统回收场景(Android 风格)

虽然鸿蒙平台可能不会像 Android 那样频繁销毁 Activity,但保持良好的习惯总是没错的:

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';

void main() {
  runApp(const RobustImagePickerApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '健壮图片选择示例',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFFEC4899)),
        useMaterial3: true,
      ),
      home: const RobustImagePicker(),
    );
  }
}

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

  
  State<RobustImagePicker> createState() => _RobustImagePickerState();
}

class _RobustImagePickerState extends State<RobustImagePicker> {
  final ImagePicker _picker = ImagePicker();
  XFile? _image;
  bool _hasLostData = false;

  
  void initState() {
    super.initState();
    _checkLostData();
  }

  Future<void> _checkLostData() async {
    try {
      final LostDataResponse response = await _picker.retrieveLostData();
      if (!response.isEmpty && response.file != null) {
        setState(() {
          _image = response.file;
          _hasLostData = true;
        });
      }
    } catch (e) {
      debugPrint('检查丢失数据失败: $e');
    }
  }

  Future<void> _pickImage() async {
    try {
      final XFile? image = await _picker.pickImage(
        source: ImageSource.gallery,
      );
      if (image != null) {
        setState(() {
          _image = image;
          _hasLostData = false;
        });
      }
    } catch (e) {
      debugPrint('选图失败: $e');
    }
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('健壮的图片选择')),
      body: Center(
        child: _image == null
            ? const Text('请选择图片')
            : Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Image.file(File(_image!.path)),
                  if (_hasLostData)
                    const Padding(
                      padding: EdgeInsets.all(8),
                      child: Text(
                        '从丢失数据中恢复',
                        style: TextStyle(color: Colors.orange),
                      ),
                    ),
                ],
              ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _pickImage,
        child: const Icon(Icons.photo_library),
      ),
    );
  }
}

💡 实战提示

  • 🔸 在 initState() 中调用 retrieveLostData()
  • 🔸 即使在鸿蒙平台上不太可能发生,但代码移植到 Android 时能避免问题
  • 🔸 给用户提示"从丢失数据中恢复",增加透明度

⚡ 高级技巧:自定义相机设备

// 📷 前置/后置相机切换
Future<void> _takePhotoWithCamera(
  CameraDevice device,
) async {
  try {
    final XFile? photo = await _picker.pickImage(
      source: ImageSource.camera,
      preferredCameraDevice: device,  // CameraDevice.front 或 CameraDevice.rear
    );
    if (photo != null) {
      setState(() => _image = photo);
    }
  } catch (e) {
    debugPrint('拍照失败: $e');
  }
}

❓ 常见问题排查

Q1:权限被拒绝怎么办?

// 🔍 检查权限状态
Future<bool> _checkPermission() async {
  // 使用 permission_handler 插件检查权限
  final status = await Permission.camera.status;
  return status.isGranted;
}

// 🔧 引导用户去设置
void _openSettings() {
  openAppSettings();
}

Q2:图片太大怎么办?

// 🗜️ 设置压缩参数
final XFile? image = await _picker.pickImage(
  source: ImageSource.gallery,
  maxWidth: 1024,      // 限制最大宽度
  maxHeight: 1024,     // 限制最大高度
  imageQuality: 80,    // 压缩质量
);

Q3:如何判断用户是否取消选择?

final XFile? image = await _picker.pickImage(...);

if (image == null) {
  // ❌ 用户取消了选择
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('已取消选择')),
  );
  return;
}

Q4:多图选择时如何限制数量?

Future<void> _pickLimitedImages() async {
  try {
    final List<XFile> images = await _picker.pickMultiImage();
    
    // 🔢 限制最多选择 9 张
    if (images.length > 9) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('最多只能选择 9 张图片')),
      );
      return;
    }
    
    setState(() => _images = images);
  } catch (e) {
    debugPrint('选图失败: $e');
  }
}

🚀 性能优化建议

1. 图片缓存

// 💾 使用 cached_network_image 缓存网络图片
// 💾 使用 flutter_cache_manager 缓存本地图片

2. 异步加载

// ⚡ 不要在 build 方法中读取文件
// ⚡ 使用 FutureBuilder 或 setState 异步更新

3. 内存管理


void dispose() {
  _controller?.dispose();  // 🗑️ 释放视频控制器
  // 🗑️ 清理大文件引用
  super.dispose();
}

✅ 总结:快速检查清单

在完成图片选择功能后,对照以下清单检查:

  • 📦 已添加 image_picker 依赖
  • 🔐 已配置相机和相册权限
  • ⚙️ 已初始化平台实例
  • ❌ 已处理用户取消选择的情况
  • 🛡️ 已添加错误处理(try-catch)
  • 🗜️ 已限制图片大小和质量
  • ⏳ 已实现进度反馈(加载状态)
  • 🗑️ 已释放资源(dispose)
  • 📸 已测试相机和相册两种来源
  • 🖼️ 已测试单选和多选场景

📚 参考资料


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐