You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Dio/Flutter将视频保存至手机相册(仅用Dio和path provider)

问题原因

你通过getExternalStorageDirectory()获取的是APP私有存储目录,系统相册不会自动扫描该目录下的文件,所以即便视频下载成功,也无法在相册中显示。

解决方案

要让视频出现在相册,需完成两个核心操作:

  1. 将文件保存到系统公共媒体目录(如DCIM、Movies文件夹)
  2. 通知系统媒体扫描器识别新文件,触发相册更新

步骤1:配置权限

  • Android:在AndroidManifest.xml中添加权限适配
    <!-- Android 9及以下需写入权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <!-- Android 10+ 读取媒体文件权限 -->
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
    <!-- 如需直接写入公共目录(可选,推荐用MediaStore合规方案) -->
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
    
  • iOS:在Info.plist中添加相册权限描述
    <key>NSPhotoLibraryAddUsageDescription</key>
    <string>需要访问相册以保存视频</string>
    

步骤2:修改下载代码

完整实现代码

import 'dart:io';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:flutter/services.dart';

void downloadVideo() async {
  // 1. 获取公共媒体目录
  Directory? publicDir;
  if (Platform.isAndroid) {
    // Android:DCIM文件夹,相册默认扫描此目录
    publicDir = await getExternalStoragePublicDirectory(DirectoryType.dcim);
  } else if (Platform.isIOS) {
    // iOS:先获取APP文档目录,后续通过原生代码同步到相册
    publicDir = await getApplicationDocumentsDirectory();
  }

  if (publicDir == null) return;

  final savePath = '${publicDir.path}/downloaded_video.mp4';
  final dio = Dio();

  try {
    // 2. 下载视频到公共目录
    await dio.download(
      'video_url',
      savePath,
      onReceiveProgress: (actualBytes, totalBytes) {
        if (totalBytes != -1) {
          final percentage = (actualBytes / totalBytes * 100).toStringAsFixed(2);
          print('下载进度:$percentage%');
        }
      },
    );

    // 3. 通知系统扫描文件,更新相册
    await _notifyMediaScanner(savePath);
    print('视频已保存到相册');
  } catch (e) {
    print('下载失败:$e');
  }
}

// 跨平台通知系统媒体扫描器
Future<void> _notifyMediaScanner(String filePath) async {
  if (Platform.isAndroid) {
    try {
      await MethodChannel('com.example.media_scanner').invokeMethod(
        'scanFile',
        {'path': filePath, 'mime': 'video/mp4'},
      );
    } on PlatformException catch (e) {
      print('扫描失败:${e.message}');
    }
  } else if (Platform.isIOS) {
    try {
      await MethodChannel('com.example.media_scanner').invokeMethod(
        'saveToGallery',
        {'path': filePath},
      );
    } on PlatformException catch (e) {
      print('保存到相册失败:${e.message}');
    }
  }
}

步骤3:补充原生代码(必须)

Flutter无直接调用系统媒体扫描的API,需通过平台通道实现:

Android 原生代码(MainActivity.kt)

import android.media.MediaScannerConnection
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.example.media_scanner"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            if (call.method == "scanFile") {
                val path = call.argument<String>("path")
                val mime = call.argument<String>("mime")
                if (path != null && mime != null) {
                    MediaScannerConnection.scanFile(
                        this,
                        arrayOf(path),
                        arrayOf(mime)
                    ) { _, uri ->
                        result.success(if (uri != null) "扫描成功" else "扫描失败")
                    }
                } else {
                    result.error("ERROR", "参数错误", null)
                }
            } else {
                result.notImplemented()
            }
        }
    }
}

iOS 原生代码(AppDelegate.swift)

import UIKit
import Flutter
import Photos

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    private let CHANNEL = "com.example.media_scanner"

    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        let controller = window?.rootViewController as! FlutterViewController
        let channel = FlutterMethodChannel(name: CHANNEL, binaryMessenger: controller.binaryMessenger)
        
        channel.setMethodCallHandler { [weak self] call, result in
            guard let self = self else { return }
            if call.method == "saveToGallery" {
                guard let args = call.arguments as? [String: String], let filePath = args["path"] else {
                    result(FlutterError(code: "ERROR", message: "参数错误", details: nil))
                    return
                }
                let fileURL = URL(fileURLWithPath: filePath)
                
                PHPhotoLibrary.requestAuthorization { status in
                    if status == .authorized {
                        PHPhotoLibrary.shared().performChanges({
                            PHAssetCreationRequest.forAsset().addResource(with: .video, fileURL: fileURL, options: nil)
                        }) { success, error in
                            if success {
                                result("保存成功")
                            } else {
                                result(FlutterError(code: "ERROR", message: error?.localizedDescription, details: nil))
                            }
                        }
                    } else {
                        result(FlutterError(code: "PERMISSION_DENIED", message: "相册权限被拒绝", details: nil))
                    }
                }
            } else {
                result(FlutterMethodNotImplemented)
            }
        }

        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}
关键说明
  • Android:MediaScannerConnection扫描后,系统相册会自动加载视频;Android 10+ 推荐使用MediaStore API写入文件,规避Scoped Storage限制(可自行扩展实现)。
  • iOS:必须申请相册权限,通过PHPhotoLibrary将文件添加到相册,否则无法在系统相册中查看。

内容的提问来源于stack exchange,提问作者shakti goyal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 23:27:31