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

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,
        ),
      ),
    );
  }
}

关键说明

  1. 路径问题解决:通过photo_manager库获取系统媒体,不需要直接操作文件路径,完全规避Scoped Storage限制。
  2. 原始顺序保持:getAssetListPaged获取的媒体列表默认按系统相册的原始排序(与系统相册展示顺序一致)。
  3. 视频标识与跳转:通过AssetEntity.type判断是否为视频,叠加播放按钮,点击后获取视频文件路径跳转至播放页。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 18:42:47