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

Flutter技术问询:实现内部存储(含指定文件夹)视频列表展示与播放

Flutter 视频文件管理与播放实现方案

前置准备

  1. 添加依赖到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
  1. 配置权限:
    • 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>
      

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 04:01:18