Flutter网格视图展示相册媒体文件及路径异常问题求助
问题原因与解决办法
核心问题分析
你代码里访问的路径 /storage/emulated/0/Android/data/com.example.syn_ios_android/files/DCIM/ 是APP私有外部存储目录,系统相册的照片/视频根本不在这个位置。另外从Android 10开始引入的Scoped Storage机制,禁止APP直接访问公共存储目录的文件路径,这双重原因导致了路径不存在的异常。
解决步骤
要实现系统相册风格的页面,不能直接硬编码路径读取文件,必须借助专门的媒体库来绕过Scoped Storage限制,同时正确获取系统相册的媒体资源。
1. 引入依赖
在pubspec.yaml中添加两个核心依赖:
dependencies: flutter: sdk: flutter photo_manager: ^2.8.0 # 用于访问系统媒体库,保持原始排序 video_player: ^2.8.1 # 用于视频播放
2. 配置权限
- Android:在
android/app/src/main/AndroidManifest.xml中添加权限声明:
<!-- Android 13+ 权限 --> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <!-- Android 12及以下兼容权限 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
- iOS:在
ios/Runner/Info.plist中添加权限描述:
<key>NSPhotoLibraryUsageDescription</key> <string>需要访问您的相册以展示照片和视频</string>
3. 修正实现代码
以下是完整的示例代码,包含权限请求、媒体加载、网格展示、视频播放按钮及跳转逻辑:
import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:video_player/video_player.dart'; class SystemAlbumPage extends StatefulWidget { const SystemAlbumPage({super.key}); @override State<SystemAlbumPage> createState() => _SystemAlbumPageState(); } class _SystemAlbumPageState extends State<SystemAlbumPage> { List<AssetEntity> _mediaList = []; bool _isLoading = true; @override void initState() { super.initState(); _requestPermissionAndLoadMedia(); } // 请求权限并加载媒体文件 Future<void> _requestPermissionAndLoadMedia() async { final PermissionState permissionState = await PhotoManager.requestPermissionExtend(); if (permissionState.isAuth) { // 获取所有媒体文件,按系统相册原始顺序排序 final List<AssetPathEntity> paths = await PhotoManager.getAssetPathList( type: RequestType.all, hasAll: true, ); if (paths.isNotEmpty) { final List<AssetEntity> media = await paths[0].getAssetListPaged(page: 0, size: 1000); setState(() { _mediaList = media; _isLoading = false; }); } } else { // 权限被拒绝,提示用户 setState(() => _isLoading = false); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('需要相册权限才能展示内容')), ); } } } // 构建媒体项 Widget _buildMediaItem(AssetEntity entity) { return FutureBuilder<Uint8List?>( future: entity.thumbnailData, builder: (context, snapshot) { if (snapshot.hasData) { return ClipRRect( borderRadius: BorderRadius.circular(8), child: Stack( fit: StackFit.expand, children: [ Image.memory( snapshot.data!, fit: BoxFit.cover, ), // 视频添加播放按钮 if (entity.type == AssetType.video) Center( child: Container( width: 40, height: 40, decoration: const BoxDecoration( color: Colors.black54, shape: BoxShape.circle, ), child: IconButton( padding: EdgeInsets.zero, icon: const Icon(Icons.play_arrow, color: Colors.white), onPressed: () => _navigateToVideoPlayer(entity), ), ), ), ], ), ); } return const Center(child: CircularProgressIndicator()); }, ); } // 跳转视频播放页 void _navigateToVideoPlayer(AssetEntity entity) async { final String? videoPath = await entity.file?.path; if (videoPath != null && mounted) { Navigator.push( context, MaterialPageRoute( builder: (context) => VideoPlayerPage(videoPath: videoPath), ), ); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('系统相册')), body: _isLoading ? const Center(child: CircularProgressIndicator()) : GridView.builder( padding: const EdgeInsets.all(8), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, crossAxisSpacing: 8, mainAxisSpacing: 8, ), itemCount: _mediaList.length, itemBuilder: (context, index) => _buildMediaItem(_mediaList[index]), ), ); } } // 视频播放页 class VideoPlayerPage extends StatefulWidget { final String videoPath; const VideoPlayerPage({super.key, required this.videoPath}); @override State<VideoPlayerPage> createState() => _VideoPlayerPageState(); } class _VideoPlayerPageState extends State<VideoPlayerPage> { late VideoPlayerController _controller; @override void initState() { super.initState(); _controller = VideoPlayerController.file(File(widget.videoPath)) ..initialize().then((_) => setState(() {})) ..play(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('视频播放')), body: Center( child: _controller.value.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, ), ), ); } }
关键说明
- 路径问题解决:通过
photo_manager库获取系统媒体,不需要直接操作文件路径,完全规避Scoped Storage限制。 - 原始顺序保持:
getAssetListPaged获取的媒体列表默认按系统相册的原始排序(与系统相册展示顺序一致)。 - 视频标识与跳转:通过
AssetEntity.type判断是否为视频,叠加播放按钮,点击后获取视频文件路径跳转至播放页。
内容的提问来源于stack exchange,提问作者LearnFlutter
相关产品推荐
相关产品推荐

