如何用Dio/Flutter将视频保存至手机相册(仅用Dio和path provider)
问题原因
你通过getExternalStorageDirectory()获取的是APP私有存储目录,系统相册不会自动扫描该目录下的文件,所以即便视频下载成功,也无法在相册中显示。
解决方案
要让视频出现在相册,需完成两个核心操作:
- 将文件保存到系统公共媒体目录(如DCIM、Movies文件夹)
- 通知系统媒体扫描器识别新文件,触发相册更新
步骤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+ 推荐使用MediaStoreAPI写入文件,规避Scoped Storage限制(可自行扩展实现)。 - iOS:必须申请相册权限,通过
PHPhotoLibrary将文件添加到相册,否则无法在系统相册中查看。
内容的提问来源于stack exchange,提问作者shakti goyal
相关产品推荐
相关产品推荐

