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

Flutter如何读取内部存储本地目录文件?图库模块开发求助

Flutter 图库模块实现方案

完全可以实现你需求的功能,以下是具体的分步实现思路和关键代码:

一、权限申请

首先必须获取存储访问权限,推荐使用permission_handler包:

  1. 配置权限:
    • Android:在android/app/src/main/AndroidManifest.xml中添加对应权限(Android 13+ 需用READ_MEDIA_IMAGES和READ_MEDIA_VIDEO,低版本用READ_EXTERNAL_STORAGE)
    • iOS:在Info.plist中添加NSPhotoLibraryUsageDescription权限描述
  2. 权限请求代码:
import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'dart:io';

Future<bool> requestStoragePermissions() async {
  var status = await Permission.storage.request();
  if (Platform.isAndroid && await DeviceInfoPlugin().androidInfo.version.sdkInt >= 33) {
    status = await Permission.photos.request();
    await Permission.videos.request();
  }
  return status.isGranted;
}

二、读取'abc'文件夹下的媒体文件

直接构造目标文件夹路径,遍历子目录筛选对应类型的媒体文件:

import 'dart:io';
import 'package:path/path.dart' as path;

// 定义媒体文件类型枚举
enum MediaType { image, video }

// 获取指定类型的媒体文件列表
Future<List<File>> getMediaFiles(MediaType type) async {
  // 构造目标文件夹路径(Android公共内部存储路径)
  final dirPath = '/storage/emulated/0/abc/${type == MediaType.image ? '图片' : '视频'}';
  final directory = Directory(dirPath);
  if (!await directory.exists()) return [];

  final files = await directory.list().where((entity) {
    final ext = path.extension(entity.path).toLowerCase();
    // 根据类型筛选后缀
    if (type == MediaType.image) {
      return ['.jpg', '.jpeg', '.png', '.gif'].contains(ext);
    } else {
      return ['.mp4', '.mov', '.avi', '.mkv'].contains(ext);
    }
  }).map((entity) => File(entity.path)).toList();

  // 按修改日期倒序排列
  files.sort((a, b) => b.statSync().modified.compareTo(a.statSync().modified));
  return files;
}

三、分标签页展示媒体内容

使用TabBar+TabBarView实现标签切换,配合GridView展示文件列表:

import 'package:flutter/material.dart';

class MediaGallery extends StatefulWidget {
  @override
  _MediaGalleryState createState() => _MediaGalleryState();
}

class _MediaGalleryState extends State<MediaGallery> with SingleTickerProviderStateMixin {
  late TabController _tabController;
  List<File> _imageFiles = [];
  List<File> _videoFiles = [];

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 2, vsync: this);
    _loadMediaFiles();
  }

  Future<void> _loadMediaFiles() async {
    final hasPermission = await requestStoragePermissions();
    if (!hasPermission) return;
    
    final images = await getMediaFiles(MediaType.image);
    final videos = await getMediaFiles(MediaType.video);
    setState(() {
      _imageFiles = images;
      _videoFiles = videos;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        bottom: TabBar(
          controller: _tabController,
          tabs: const [
            Tab(text: '图片'),
            Tab(text: '视频'),
          ],
        ),
      ),
      body: TabBarView(
        controller: _tabController,
        children: [
          _buildMediaGrid(_imageFiles, MediaType.image),
          _buildMediaGrid(_videoFiles, MediaType.video),
        ],
      ),
    );
  }

  Widget _buildMediaGrid(List<File> files, MediaType type) {
    if (files.isEmpty) return const Center(child: Text('暂无文件'));
    return GridView.builder(
      padding: const EdgeInsets.all(8),
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 3,
        crossAxisSpacing: 8,
        mainAxisSpacing: 8,
      ),
      itemCount: files.length,
      itemBuilder: (context, index) {
        final file = files[index];
        final stat = file.statSync();
        return MediaItemWidget(file: file, stat: stat, type: type);
      },
    );
  }
}

四、展示文件信息与缩略图

自定义MediaItemWidget组件,整合缩略图、文件大小和修改日期:

import 'package:video_thumbnail/video_thumbnail.dart';

class MediaItemWidget extends StatelessWidget {
  final File file;
  final FileStat stat;
  final MediaType type;

  const MediaItemWidget({required this.file, required this.stat, required this.type, Key? key}) : super(key: key);

  // 格式化文件大小
  String _formatFileSize(int bytes) {
    if (bytes >= 1024 * 1024) {
      return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
    } else if (bytes >= 1024) {
      return '${(bytes / 1024).toStringAsFixed(1)} KB';
    } else {
      return '$bytes B';
    }
  }

  // 格式化日期
  String _formatDateTime(DateTime dateTime) {
    return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')}';
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        // 加载缩略图
        FutureBuilder<String?>(
          future: type == MediaType.image 
              ? Future.value(file.path) 
              : VideoThumbnail.thumbnailFile(
                  video: file.path,
                  imageFormat: ImageFormat.JPEG,
                  maxWidth: 120,
                  quality: 75,
                ),
          builder: (context, snapshot) {
            if (snapshot.hasData && snapshot.data != null) {
              return Image.file(File(snapshot.data!), fit: BoxFit.cover, width: double.infinity, height: double.infinity);
            }
            return const Center(child: CircularProgressIndicator());
          },
        ),
        // 文件信息栏
        Positioned(
          bottom: 0,
          left: 0,
          right: 0,
          child: Container(
            color: Colors.black54,
            padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(_formatFileSize(stat.size), style: const TextStyle(color: Colors.white, fontSize: 12)),
                Text(_formatDateTime(stat.modified), style: const TextStyle(color: Colors.white, fontSize: 10)),
              ],
            ),
          ),
        ),
        // 视频标识
        if (type == MediaType.video)
          const Positioned(
            top: 4,
            right: 4,
            child: Icon(Icons.play_circle, color: Colors.white, size: 20),
          ),
      ],
    );
  }
}

五、视频裁剪功能

使用video_compress包实现视频裁剪,指定起止时间即可:

import 'package:video_compress/video_compress.dart';

Future<File?> cropVideo(File videoFile, int startSecond, int endSecond) async {
  final result = await VideoCompress.compressVideo(
    videoFile.path,
    startTime: startSecond,
    endTime: endSecond,
    quality: VideoQuality.DefaultQuality,
    includeAudio: true,
  );
  if (result != null && result.file != null) {
    return result.file;
  }
  return null;
}

最后需要在pubspec.yaml中添加依赖:

dependencies:
  permission_handler: ^10.2.0
  path_provider: ^2.0.15
  video_thumbnail: ^0.5.3
  video_compress: ^3.1.1
  device_info_plus: ^9.0.2

内容的提问来源于stack exchange,提问作者Dhruvil Patel

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 11:25:30