Flutter技术问询:实现内部存储(含指定文件夹)视频列表展示与播放
Flutter 视频文件管理与播放实现方案
前置准备
- 添加依赖到
pubspec.yaml:
dependencies: flutter: sdk: flutter permission_handler: ^10.2.0 path_provider: ^2.0.15 video_player: ^2.6.1 path: ^1.8.3
- 配置权限:
- Android:在
AndroidManifest.xml中添加<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32"/> <!-- Android 13+ 需添加 --> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/> - iOS:在
Info.plist中添加<key>NSPhotoLibraryUsageDescription</key> <string>需要访问您的相册以获取视频文件</string>
- Android:在
1. 展示设备内部存储全部视频文件列表
核心逻辑是遍历内部存储根目录,筛选常见视频格式文件。
实现代码
import 'dart:io'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:path/path.dart' as p; // 获取所有视频文件 Future<List<File>> getAllVideos() async { List<File> videos = []; // 请求存储权限 var status = await Permission.storage.request(); if (!status.isGranted) return videos; // 适配Android/iOS存储路径 Directory? rootDir; if (Platform.isAndroid) { rootDir = Directory('/storage/emulated/0'); } else if (Platform.isIOS) { rootDir = await getApplicationDocumentsDirectory(); } if (rootDir == null) return videos; // 递归遍历目录筛选视频 await for (var entity in rootDir.list(recursive: true, followLinks: false)) { if (entity is File) { String ext = p.extension(entity.path).toLowerCase(); if (['.mp4', '.mov', '.avi', '.mkv', '.flv'].contains(ext)) { videos.add(entity); } } } return videos; } // 列表展示Widget Widget buildVideoList(List<File> videos) { return ListView.builder( itemCount: videos.length, itemBuilder: (context, index) { return ListTile( title: Text(p.basename(videos[index].path)), subtitle: Text(videos[index].path), ); }, ); }
2. 列出指定文件夹内的所有视频文件
仅遍历指定目标目录,逻辑与全存储遍历一致,缩小范围即可。
实现代码
// 获取指定文件夹下的视频 Future<List<File>> getVideosFromFolder(String folderPath) async { List<File> videos = []; var status = await Permission.storage.request(); if (!status.isGranted) return videos; Directory folderDir = Directory(folderPath); if (!await folderDir.exists()) return videos; await for (var entity in folderDir.list(recursive: true, followLinks: false)) { if (entity is File) { String ext = p.extension(entity.path).toLowerCase(); if (['.mp4', '.mov', '.avi', '.mkv', '.flv'].contains(ext)) { videos.add(entity); } } } return videos; } // 使用示例(Android DCIM目录) // List<File> dcimVideos = await getVideosFromFolder('/storage/emulated/0/DCIM');
3. GridView展示所有视频并支持播放
结合视频获取逻辑,用GridView展示缩略图,点击跳转播放页面。
1. GridView视频列表页面
class VideoGridViewPage extends StatefulWidget { const VideoGridViewPage({super.key}); @override State<VideoGridViewPage> createState() => _VideoGridViewPageState(); } class _VideoGridViewPageState extends State<VideoGridViewPage> { List<File> _videos = []; bool _isLoading = true; @override void initState() { super.initState(); _loadAllVideos(); } Future<void> _loadAllVideos() async { // 后台线程执行遍历,避免UI阻塞 _videos = await getAllVideos(); setState(() => _isLoading = false); } @override Widget build(BuildContext context) { if (_isLoading) { return const Center(child: CircularProgressIndicator()); } return GridView.builder( padding: const EdgeInsets.all(8), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 8, mainAxisSpacing: 8, childAspectRatio: 0.75, ), itemCount: _videos.length, itemBuilder: (context, index) { return GestureDetector( onTap: () => Navigator.push( context, MaterialPageRoute( builder: (context) => VideoPlayerPage(videoFile: _videos[index]), ), ), child: Column( children: [ Expanded( child: Image.file( _videos[index], fit: BoxFit.cover, errorBuilder: (_, __, ___) => const Icon(Icons.video_library, size: 40), ), ), const SizedBox(height: 4), Text( p.basename(_videos[index].path), overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, ), ], ), ); }, ); } }
2. 视频播放页面
import 'package:video_player/video_player.dart'; class VideoPlayerPage extends StatefulWidget { final File videoFile; const VideoPlayerPage({super.key, required this.videoFile}); @override State<VideoPlayerPage> createState() => _VideoPlayerPageState(); } class _VideoPlayerPageState extends State<VideoPlayerPage> { late VideoPlayerController _controller; bool _isInitialized = false; @override void initState() { super.initState(); _controller = VideoPlayerController.file(widget.videoFile) ..initialize().then((_) { setState(() { _isInitialized = true; _controller.play(); }); }); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(p.basename(widget.videoFile.path))), body: Center( child: _isInitialized ? AspectRatio( aspectRatio: _controller.value.aspectRatio, child: VideoPlayer(_controller), ) : const CircularProgressIndicator(), ), floatingActionButton: FloatingActionButton( onPressed: () => setState(() { _controller.value.isPlaying ? _controller.pause() : _controller.play(); }), child: Icon(_controller.value.isPlaying ? Icons.pause : Icons.play_arrow), ), ); } }
注意事项
- 遍历大存储目录时,建议使用
compute函数将文件筛选逻辑放到后台线程,避免UI卡顿。 - iOS仅能访问应用沙盒内文件,若需访问系统相册,需额外集成
photo_manager库并配置对应权限。
内容的提问来源于stack exchange,提问作者Naveen
相关产品推荐
相关产品推荐

