封装一个插件,统一处理权限+保存

import 'dart:io';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:demo/common/index.dart';
import 'package:image_gallery_saver_plus/image_gallery_saver_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:get/get.dart';

/// 图片保存工具类
/// 支持保存网络图片到本地相册
/// 自动处理iOS和Android的权限适配
class ImageSaverHelper {
  static final Dio _dio = Dio();

  /// 保存网络图片到相册
  /// [imageUrl] 图片链接
  /// [fileName] 文件名(可选,默认使用时间戳)
  /// [quality] 图片质量(0-100,默认100)
  /// [showLoading] 是否显示加载弹窗(默认true)
  /// [showResult] 是否显示结果提示(默认true)
  /// Returns: 是否保存成功
  /// 注意:iOS端不需要手动检查权限,image_gallery_saver_plus插件会自动处理
  static Future<bool> saveNetworkImage(
    String imageUrl, {
    String? fileName,
    int quality = 100,
    bool showLoading = true,
    bool showResult = true,
  }) async {
    if (imageUrl.isEmpty) {
      if (showResult) Loading.error('图片地址不能为空'.tr);
      return false;
    }

    if (showLoading) Loading.show();

    try {
      // iOS: 不需要手动检查权限,插件会自动弹出权限申请
      // Android: 需要手动检查权限
      if (Platform.isAndroid && !await _checkAndRequestPermission()) {
        return false;
      }

      // 下载图片
      final imageData = await _downloadImage(imageUrl);
      if (imageData == null) {
        if (showResult) Loading.error('下载图片失败'.tr);
        return false;
      }

      // 生成文件名
      final name = fileName ?? "image_${DateTime.now().millisecondsSinceEpoch}";

      // 保存到相册(iOS插件会自动处理权限申请)
      final result = await ImageGallerySaverPlus.saveImage(
        imageData,
        quality: quality,
        name: name,
      );

      if (result != null && result['isSuccess']) {
        if (showResult) Loading.success('保存成功'.tr);
        return true;
      } else {
        print('保存失败:${result}');
        // 权限被拒绝时,显示系统设置对话框(iOS和Android通用)
        if (_isPermissionDeniedError(result)) {
          if (showResult) {
            await _showPermissionDialog();
          }
          return false;
        }
        if (showResult) Loading.error('保存失败,请确保已授予存储权限'.tr);
        return false;
      }
    } catch (e) {
      // 权限被拒绝时,显示系统设置对话框(iOS和Android通用)
      if (_isPermissionError(e.toString())) {
        if (showResult) {
          await _showPermissionDialog();
        }
        return false;
      }
      if (showResult) Loading.error('保存失败:${e.toString()}'.tr);
      return false;
    } finally {
      if (showLoading) Loading.dismiss();
    }
  }

  /// 保存本地图片文件到相册
  /// [imagePath] 本地图片路径
  /// [fileName] 文件名(可选,默认使用时间戳)
  /// [quality] 图片质量(0-100,默认100)
  /// [showLoading] 是否显示加载弹窗(默认true)
  /// [showResult] 是否显示结果提示(默认true)
  /// Returns: 是否保存成功
  /// 注意:iOS端不需要手动检查权限,image_gallery_saver_plus插件会自动处理
  static Future<bool> saveLocalImage(
    String imagePath, {
    String? fileName,
    int quality = 100,
    bool showLoading = true,
    bool showResult = true,
  }) async {
    if (imagePath.isEmpty) {
      if (showResult) Loading.error('图片路径不能为空'.tr);
      return false;
    }

    if (showLoading) Loading.show();

    try {
      // iOS: 不需要手动检查权限,插件会自动弹出权限申请
      // Android: 需要手动检查权限
      if (Platform.isAndroid && !await _checkAndRequestPermission()) {
        return false;
      }

      // 读取本地文件
      final file = File(imagePath);
      if (!await file.exists()) {
        if (showResult) Loading.error('图片文件不存在'.tr);
        return false;
      }

      final imageData = await file.readAsBytes();

      // 生成文件名
      final name = fileName ?? "image_${DateTime.now().millisecondsSinceEpoch}";

      // 保存到相册(iOS插件会自动处理权限申请)
      final result = await ImageGallerySaverPlus.saveImage(
        imageData,
        quality: quality,
        name: name,
      );

      if (result != null && result['isSuccess']) {
        if (showResult) Loading.success('保存成功'.tr);
        return true;
      } else {
        print('保存失败:${result}');
        // 权限被拒绝时,显示系统设置对话框(iOS和Android通用)
        if (_isPermissionDeniedError(result)) {
          if (showResult) {
            await _showPermissionDialog();
          }
          return false;
        }
        if (showResult) Loading.error('保存失败,请确保已授予存储权限'.tr);
        return false;
      }
    } catch (e) {
      // 权限被拒绝时,显示系统设置对话框(iOS和Android通用)
      if (_isPermissionError(e.toString())) {
        if (showResult) {
          await _showPermissionDialog();
        }
        return false;
      }
      if (showResult) Loading.error('保存失败:${e.toString()}'.tr);
      return false;
    } finally {
      if (showLoading) Loading.dismiss();
    }
  }

  /// 保存内存图片到相册(Uint8List)
  /// 注意:iOS端不需要手动检查权限,image_gallery_saver_plus插件会自动处理
  static Future<bool> saveUint8ListImage(
    Uint8List imageData, {
    String? fileName,
    int quality = 100,
    bool showLoading = true,
    bool showResult = true,
  }) async {
    if (imageData.isEmpty) {
      if (showResult) Loading.error('图片数据为空'.tr);
      return false;
    }

    if (showLoading) Loading.show();

    try {
      // iOS: 不需要手动检查权限,插件会自动弹出权限申请
      // Android: 需要手动检查权限
      if (Platform.isAndroid && !await _checkAndRequestPermission()) {
        return false;
      }

      // 生成文件名
      final name = fileName ?? "image_${DateTime.now().millisecondsSinceEpoch}";

      // 保存到相册(iOS插件会自动处理权限申请)
      final result = await ImageGallerySaverPlus.saveImage(
        imageData,
        quality: quality,
        name: name,
      );

      if (result != null && result['isSuccess']) {
        if (showResult) Loading.success('保存成功'.tr);
        return true;
      } else {
        print('保存失败:${result}');
        // 权限被拒绝时,显示系统设置对话框(iOS和Android通用)
        if (_isPermissionDeniedError(result)) {
          if (showResult) {
            await _showPermissionDialog();
          }
          return false;
        }
        if (showResult) Loading.error('保存失败,请确保已授予存储权限'.tr);
        return false;
      }
    } catch (e) {
      // 权限被拒绝时,显示系统设置对话框(iOS和Android通用)
      if (_isPermissionError(e.toString())) {
        if (showResult) {
          await _showPermissionDialog();
        }
        return false;
      }
      if (showResult) Loading.error('保存失败:${e.toString()}'.tr);
      return false;
    } finally {
      if (showLoading) Loading.dismiss();
    }
  }

  /// 下载网络图片
  /// [imageUrl] 图片链接
  /// Returns: 图片字节数据
  static Future<Uint8List?> _downloadImage(String imageUrl) async {
    try {
      final response = await _dio.get(
        imageUrl,
        options: Options(
          responseType: ResponseType.bytes,
          // 设置超时时间
          sendTimeout: const Duration(seconds: 30),
          receiveTimeout: const Duration(seconds: 30),
        ),
      );

      if (response.statusCode == 200) {
        return Uint8List.fromList(response.data);
      }
      return null;
    } catch (e) {
      print('下载图片失败: $e');
      return null;
    }
  }

  /// 检查并请求权限
  /// Returns: 是否获得权限
  static Future<bool> _checkAndRequestPermission() async {
    try {
      if (Platform.isIOS) {
        // iOS权限处理
        var status = await Permission.photos.status;
        if (status.isDenied) {
          status = await Permission.photos.request();
        }

        if (status.isPermanentlyDenied) {
          // 权限被永久拒绝,显示对话框提示用户前往设置
          await _showPermissionDialog();
          return false;
        }

        return status.isGranted;
      } else if (Platform.isAndroid) {
        // Android权限处理
        final androidInfo = await DeviceInfoPlugin().androidInfo;
        final sdkInt = androidInfo.version.sdkInt;

        PermissionStatus status;
        if (sdkInt >= 30) {
          // Android 11 (API 30) 及以上,使用 manageExternalStorage
          status = await Permission.manageExternalStorage.status;
          if (status.isDenied) {
            status = await Permission.manageExternalStorage.request();
          }
        } else {
          // Android 10 (API 29) 及以下,使用存储权限
          status = await Permission.storage.status;
          if (status.isDenied) {
            status = await Permission.storage.request();
          }
        }

        if (status.isPermanentlyDenied) {
          // 权限被永久拒绝,显示对话框提示用户前往设置
          await _showPermissionDialog();
          return false;
        }

        if (!status.isGranted) {
          // 权限被拒绝,显示对话框提示用户前往设置
          await _showPermissionDialog();
          return false;
        }
        return true;
      }

      return true;
    } catch (e) {
      Loading.error('权限检查失败:${e.toString()}'.tr);
      return false;
    }
  }

  /// 检查是否有权限(不请求)
  /// Returns: 是否有权限
  static Future<bool> hasPermission() async {
    try {
      if (Platform.isIOS) {
        var status = await Permission.photos.status;
        return status.isGranted;
      } else if (Platform.isAndroid) {
        final androidInfo = await DeviceInfoPlugin().androidInfo;
        final sdkInt = androidInfo.version.sdkInt;

        PermissionStatus status;
        if (sdkInt >= 30) {
          status = await Permission.manageExternalStorage.status;
        } else {
          status = await Permission.storage.status;
        }

        return status.isGranted;
      }
      return true;
    } catch (e) {
      return false;
    }
  }

  /// 判断是否为权限被拒绝的错误(检查保存结果)
  /// [result] 保存结果
  /// Returns: 是否为权限被拒绝的错误
  static bool _isPermissionDeniedError(Map? result) {
    if (result == null) return false;

    final errorMessage = result['errorMessage']?.toString() ?? '';
    // 检查是否包含权限相关的错误码
    // iOS: PHPhotosErrorDomain Code=3311 表示权限被拒绝
    // iOS: ALAssetsLibraryErrorDomain Code=-1 也可能是权限问题
    // Android: 可能包含 permission 相关的错误信息
    return errorMessage.contains('PHPhotosErrorDomain') ||
        errorMessage.contains('ALAssetsLibraryErrorDomain') ||
        errorMessage.contains('Code=3311') ||
        errorMessage.contains('permission') ||
        errorMessage.contains('Permission') ||
        errorMessage.contains('PERMISSION_DENIED');
  }

  /// 判断是否为权限被拒绝的错误(检查异常信息)
  /// [errorMessage] 错误信息
  /// Returns: 是否为权限被拒绝的错误
  static bool _isPermissionError(String errorMessage) {
    return errorMessage.contains('PHPhotosErrorDomain') ||
        errorMessage.contains('ALAssetsLibraryErrorDomain') ||
        errorMessage.contains('Code=3311') ||
        errorMessage.contains('PermissionState.denied') ||
        errorMessage.contains('permission') ||
        errorMessage.contains('Permission') ||
        errorMessage.contains('PERMISSION_DENIED');
  }

  /// 显示权限被拒绝时的对话框(iOS和Android通用)
  /// 当用户拒绝相册权限后,提示用户前往系统设置
  static Future<void> _showPermissionDialog() async {
    final context = Get.context;
    if (context == null) {
      Loading.error('无法访问相册中照片,请在设置中开启照片访问权限'.tr);
      return;
    }
    Loading.dismiss();

    return showCupertinoDialog<void>(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext dialogContext) {
        return CupertinoAlertDialog(
          title: Text('无法访问相册中照片'.tr),
          content: Text('当前无照片访问权限,建议前往系统设置,允许应用访问[照片]中的[所有照片]'.tr),
          actions: <Widget>[
            CupertinoDialogAction(
              child: Text(
                '取消'.tr,
                style: const TextStyle(fontSize: 16, color: AppTheme.color999),
              ),
              onPressed: () {
                Navigator.of(dialogContext).pop();
              },
            ),
            CupertinoDialogAction(
              isDefaultAction: true,
              child: Text(
                '前往系统设置'.tr,
                style: const TextStyle(fontSize: 16, color: Colors.black),
              ),
              onPressed: () async {
                Navigator.of(dialogContext).pop();
                // 跳转到系统设置
                await openAppSettings();
              },
            ),
          ],
        );
      },
    );
  }
}


该工具类使用说明

/// ImageSaverHelper 工具类使用说明
///
/// 这个工具类封装了图片保存到相册的功能,包含完整的权限处理
/// 支持保存网络图片和本地图片到设备相册
///
/// ## 功能特性
///
/// ### 权限自动适配
/// - **iOS**: 自动处理照片权限 (NSPhotoLibraryUsageDescription)
/// - **Android**: 根据系统版本自动适配不同的存储权限
///   - Android 11 (API 30) 及以上: 使用 manageExternalStorage
///   - Android 10 (API 29): 使用 photos 权限
///   - Android 9 及以下: 使用 storage 权限
///
/// ### 错误处理
/// - 权限被拒绝时自动引导用户去设置页面
/// - 网络图片下载失败自动提示
/// - 本地文件不存在自动检测
///
/// ### UI交互
/// - 可选择是否显示加载弹窗
/// - 可选择是否显示成功/失败提示
/// - 支持自定义文件名和图片质量
///
/// ## 基础用法
///
/// ```dart
/// // 1. 最简单的用法 - 保存网络图片
/// await ImageSaverHelper.saveNetworkImage('https://example.com/image.jpg');
///
/// // 2. 自定义参数
/// await ImageSaverHelper.saveNetworkImage(
///   'https://example.com/image.jpg',
///   fileName: 'my_image',
///   quality: 85,
///   showLoading: true,
///   showResult: true,
/// );

/// // 3. 保存本地图片
/// await ImageSaverHelper.saveLocalImage('/path/to/local/image.jpg');


/// // 4. 保存二维码图片
///  ImageSaverHelper.saveUint8ListImage(byteData.buffer.asUint8List());

///
/// // 4. 检查权限状态
/// bool hasPermission = await ImageSaverHelper.hasPermission();
/// ```
///
/// ## 高级用法
///
/// ### 静默保存(不显示UI)
/// ```dart
/// bool success = await ImageSaverHelper.saveNetworkImage(
///   imageUrl,
///   showLoading: false,
///   showResult: false,
/// );
/// // 根据返回值进行自定义处理
/// ```
///
/// ### 批量保存
/// ```dart
/// for (String url in imageUrls) {
///   await ImageSaverHelper.saveNetworkImage(
///     url,
///     showLoading: false, // 避免多个加载弹窗
///     showResult: false,  // 最后统一显示结果
///   );
/// }
/// ```
///
/// ## 参数说明
///
/// ### saveNetworkImage 方法
/// - `imageUrl` (String): 必需,网络图片链接
/// - `fileName` (String?): 可选,自定义文件名,默认使用时间戳
/// - `quality` (int): 可选,图片质量 0-100,默认100
/// - `showLoading` (bool): 可选,是否显示加载弹窗,默认true
/// - `showResult` (bool): 可选,是否显示结果提示,默认true
/// - 返回值: `Future<bool>` 是否保存成功
///
/// ### saveLocalImage 方法
/// - `imagePath` (String): 必需,本地图片路径
/// - 其他参数与 saveNetworkImage 相同
///
/// ### hasPermission 方法
/// - 返回值: `Future<bool>` 是否已有权限(不会主动请求权限)
///
/// ## 权限配置
///
/// ### iOS 配置
/// 确保 `ios/Runner/Info.plist` 中包含:
/// ```xml
/// <key>NSPhotoLibraryUsageDescription</key>
/// <string>需要访问相册选择图片进行身份认证</string>
/// ```
///
/// ### Android 配置
/// 确保 `android/app/src/main/AndroidManifest.xml` 中包含:
/// ```xml
/// <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
/// <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
/// <!-- Android 11+ -->
/// <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
/// ```
///
/// ## 依赖包
///
/// 确保 `pubspec.yaml` 中包含以下依赖:
/// ```yaml
/// dependencies:
///   permission_handler: ^11.0.0
///   image_gallery_saver_plus: ^3.0.0
///   device_info_plus: ^9.0.0
///   dio: ^5.0.0
/// ```
///
/// ## 错误处理
///
/// 工具类会自动处理以下错误情况:
/// 1. 权限被拒绝 - 自动显示错误提示并引导用户设置
/// 2. 网络错误 - 自动提示下载失败
/// 3. 本地文件不存在 - 自动检测并提示
/// 4. 保存失败 - 显示具体错误信息
///
/// ## 注意事项
///
/// 1. **权限申请**: 第一次调用时会自动申请权限,用户拒绝后会引导去设置页面
/// 2. **网络超时**: 网络图片下载超时时间为30秒
/// 3. **文件名**: 如果不指定文件名,会使用 "image_时间戳" 格式
/// 4. **图片质量**: quality参数影响文件大小,100为最高质量
/// 5. **并发下载**: 批量保存时建议控制并发数量,避免内存压力
///
/// ## 示例场景
///
/// ### 1. 用户分享页面保存二维码
/// ```dart
/// void onSaveQrCode() async {
///   await ImageSaverHelper.saveNetworkImage(qrCodeUrl);
/// }
/// ```
///
/// ### 2. 图片浏览器保存图片
/// ```dart
/// void onLongPress(String imageUrl) async {
///   bool success = await ImageSaverHelper.saveNetworkImage(
///     imageUrl,
///     fileName: 'gallery_image_${DateTime.now().millisecondsSinceEpoch}',
///   );
/// }
/// ```
///
/// ### 3. 批量下载保存
/// ```dart
/// void downloadAllImages(List<String> urls) async {
///   Loading.show('正在下载图片...');
///   int count = 0;
///   for (String url in urls) {
///     bool success = await ImageSaverHelper.saveNetworkImage(
///       url,
///       showLoading: false,
///       showResult: false,
///     );
///     if (success) count++;
///   }
///   Loading.dismiss();
///   Loading.success('成功保存 $count/${urls.length} 张图片');
/// }
/// ```
class ImageSaverHelperReadme {
  // 这个类仅用于文档说明,无需实例化
}

生成二维码参考链接生成二维码,并保存相册
替换该文章中的保存功能

  // 保存图片
  void saveImage() async {
    // 获取 RenderRepaintBoundary
    final boundary = qrKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
    print('boundary: $boundary');

    // 等待一下确保UI已经完全渲染
    await Future.delayed(const Duration(milliseconds: 20));

    // 将 Widget 转换成图片
    final image = await boundary.toImage(pixelRatio: 3.0);
    print('image: $image');

    // 将图片转换成字节数据
    final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
    print('byteData: $byteData');

    if (byteData != null) {
        final success = await ImageSaverHelper.saveUint8ListImage(byteData.buffer.asUint8List());
        print('success: $success');
    }
  }
Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐