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

如何在FlutterFlow中实现本地媒体相册自定义代码?

问题描述

我正尝试用FlutterFlow构建一款本地媒体相册应用,需要展示本地存储中的照片与视频。已调研获取了相关实现代码,但由于刚接触Flutter和FlutterFlow,无法完成集成,想了解将这些代码最佳集成到FlutterFlow的方法,或是是否有结合FF组件加少量自定义代码的替代实现方式?自行尝试操作过,但因不熟悉技术栈未能成功。

调研获取的实现代码
import 'dart:async';
import 'dart:io';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:photo_gallery/photo_gallery.dart';
import 'package:transparent_image/transparent_image.dart';
import 'package:appinio_video_player/appinio_video_player.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  List<Album>? _albums;
  bool _loading = false;

  @override
  void initState() {
    super.initState();
    _loading = true;
    initAsync();
  }

  Future<void> initAsync() async {
    if (await _promptPermissionSetting()) {
      List<Album> albums = await PhotoGallery.listAlbums();
      setState(() {
        _albums = albums;
        _loading = false;
      });
    }
    setState(() {
      _loading = false;
    });
  }

  Future<bool> _promptPermissionSetting() async {
    if (Platform.isIOS) {
      if (await Permission.photos.request().isGranted || await Permission.storage.request().isGranted) {
        return true;
      }
    }
    if (Platform.isAndroid) {
      if (await Permission.storage.request().isGranted ||
          await Permission.photos.request().isGranted &&
              await Permission.videos.request().isGranted) {
        return true;
      }
    }
    return false;
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Photo gallery example'),
        ),
        body: _loading
            ? Center(
                child: CircularProgressIndicator(),
              )
            : LayoutBuilder(
                builder: (context, constraints) {
                  double gridWidth = (constraints.maxWidth - 20) / 3;
                  double gridHeight = gridWidth + 33;
                  double ratio = gridWidth / gridHeight;
                  return Container(
                    padding: EdgeInsets.all(5),
                    child: GridView.count(
                      childAspectRatio: ratio,
                      crossAxisCount: 3,
                      mainAxisSpacing: 5.0,
                      crossAxisSpacing: 5.0,
                      children: <Widget>[
                        ...?_albums?.map(
                          (album) => GestureDetector(
                            onTap: () => Navigator.of(context).push(
                              MaterialPageRoute(builder: (context) => AlbumPage(album)),
                            ),
                            child: Column(
                              children: <Widget>[
                                ClipRRect(
                                  borderRadius: BorderRadius.circular(5.0),
                                  child: Container(
                                    color: Colors.grey[300],
                                    height: gridWidth,
                                    width: gridWidth,
                                    child: FadeInImage(
                                      fit: BoxFit.cover,
                                      placeholder: MemoryImage(kTransparentImage),
                                      image: AlbumThumbnailProvider(
                                        album: album,
                                        highQuality: true,
                                      ),
                                    ),
                                  ),  //Image thumbnail
                                ),
                                Container(
                                  alignment: Alignment.topLeft,
                                  padding: EdgeInsets.only(left: 2.0),
                                  child: Text(
                                    album.name ?? "Unnamed Album",
                                    maxLines: 1,
                                    textAlign: TextAlign.start,
                                    style: TextStyle(
                                      height: 1.2,
                                      fontSize: 16,
                                    ),
                                  ),
                                ),   //Album name
                                Container(
                                  alignment: Alignment.topLeft,
                                  padding: EdgeInsets.only(left: 2.0),
                                  child: Text(
                                    album.count.toString(),
                                    textAlign: TextAlign.start,
                                    style: TextStyle(
                                      height: 1.2,
                                      fontSize: 12,
                                    ),
                                  ),
                                ),  //Iterms count
                              ],
                            ),
                          ),
                        ),
                      ],
                    ),
                  );
                },
              ),
      ),
    );
  }
}

class AlbumPage extends StatefulWidget {
  final Album album;

  AlbumPage(Album album) : album = album;

  @override
  State<StatefulWidget> createState() => AlbumPageState();
}

class AlbumPageState extends State<AlbumPage> {
  List<Medium>? _media;

  @override
  void initState() {
    super.initState();
    initAsync();
  }

  void initAsync() async {
    MediaPage mediaPage = await widget.album.listMedia();
    setState(() {
      _media = mediaPage.items;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          leading: IconButton(
            icon: Icon(Icons.arrow_back_ios),
            onPressed: () => Navigator.of(context).pop(),
          ),
          title: Text(widget.album.name ?? "Unnamed Album"),
        ),
        body: GridView.count(
          crossAxisCount: 3,
          mainAxisSpacing: 1.0,
          crossAxisSpacing: 1.0,
          children: <Widget>[
            ...?_media?.map(
              (medium) => GestureDetector(
                onTap: () => Navigator.of(context).push(
                  MaterialPageRoute(builder: (context) => ViewerPage(medium)),
                ),
                child: Container(
                  color: Colors.grey[300],
                  child: FadeInImage(
                    fit: BoxFit.cover,
                    placeholder: MemoryImage(kTransparentImage),
                    image: ThumbnailProvider(
                      mediumId: medium.id,
                      mediumType: medium.mediumType,
                      highQuality: true,
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class ViewerPage extends StatelessWidget {
  final Medium medium;

  ViewerPage(Medium medium) : medium = medium;

  @override
  Widget build(BuildContext context) {
    DateTime? date = medium.creationDate ?? medium.modifiedDate;
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          leading: IconButton(
            onPressed: () => Navigator.of(context).pop(),
            icon: Icon(Icons.arrow_back_ios),
          ),
          title: date != null ? Text(date.toLocal().toString()) : null,
        ),
        body: Container(
          alignment: Alignment.center,
          child: medium.mediumType == MediumType.image
              ? GestureDetector(
                  onTap: () async {
                    PhotoGallery.deleteMedium(mediumId: medium.id);
                  },
                  child: FadeInImage(
                    fit: BoxFit.cover,
                    placeholder: MemoryImage(kTransparentImage),
                    image: PhotoProvider(mediumId: medium.id),
                  ),
                )
              : Center(
                child: VideoProvider(
                    mediumId: medium.id,
                  ),
              ),
        ),
      ),
    );
  }
}


class VideoProvider extends StatefulWidget {
  final String mediumId;

  const VideoProvider({
    required this.mediumId,
  });

  @override
  _VideoProviderState createState() => _VideoProviderState();
}

class _VideoProviderState extends State<VideoProvider> {
  late VideoPlayerController _videoPlayerController;
  late CustomVideoPlayerController _customVideoPlayerController;
  File? _file;
  bool initialized = false;

  final CustomVideoPlayerSettings _customVideoPlayerSettings =
  const CustomVideoPlayerSettings(showSeekButtons: false);

  @override
  void initState() {
    super.initState();
    initAsync(); // Initialize the video player in initState
  }

  Future<void> initAsync() async {
    try {
      _file = await PhotoGallery.getFile(mediumId: widget.mediumId);
      _videoPlayerController = VideoPlayerController.file(_file!);
      await _videoPlayerController.initialize();

      _customVideoPlayerController = CustomVideoPlayerController(
        context: context,
        videoPlayerController: _videoPlayerController,
        customVideoPlayerSettings: _customVideoPlayerSettings,
        additionalVideoSources: {
          "Max": _videoPlayerController,
        },
      );

      setState(() {
        initialized = true;
      });
    } catch (e) {
      print("Failed : $e");
    }
  }

  @override
  void dispose() {
    _customVideoPlayerController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (!initialized) {
      return CupertinoActivityIndicator(); // Show loading indicator until initialized
    }

    return CupertinoPageScaffold(
      child: SafeArea(
        child: Column(
          children: [
            CustomVideoPlayer(
              customVideoPlayerController: _customVideoPlayerController,
            ),
          ],
        ),
      ),
    );
  }
}
解决方案

方案一:直接集成现有代码到FlutterFlow

步骤1:添加依赖包

在FlutterFlow左侧菜单栏进入Settings -> Pubspec,在dependencies块中添加以下内容,版本可根据最新稳定版调整:

dependencies:
  permission_handler: ^10.2.0
  photo_gallery: ^1.2.2
  transparent_image: ^2.0.1
  appinio_video_player: ^1.2.0

添加完成后点击右上角Get Packages同步依赖。

步骤2:拆分代码为自定义Widget

FlutterFlow不支持直接导入完整Flutter项目,需将原代码拆分为适配FF页面结构的自定义Widget:

  1. 相册列表Widget:创建GalleryAlbumList状态自定义Widget,迁移原_MyAppState的权限请求、相册加载和GridView展示逻辑,移除MaterialApp和Scaffold,仅保留核心布局代码,使用FF提供的context。
  2. 相册详情Widget:创建AlbumMediaList自定义Widget,接收相册ID参数,实现原AlbumPage的媒体列表加载与展示逻辑。
  3. 媒体查看Widget:创建MediaViewer自定义Widget,接收媒体ID和类型参数,实现图片查看和视频播放逻辑,视频部分复用原VideoProvider代码。

步骤3:权限与导航适配

  • 权限处理:在FF首页初始化时,调用自定义Widget的权限请求方法,或直接使用FF自带的Permission Actions提前请求相册/存储权限,确保权限通过后再加载数据。
  • 页面导航:将原代码的Navigator.push替换为FF的Navigate to Page动作,传递相册ID、媒体ID等参数到目标页面,在目标页面的自定义Widget中接收参数并加载对应数据。

步骤4:配置平台权限声明

  • iOS:在Settings -> iOS -> Info.plist中添加NSPhotoLibraryUsageDescription,填写权限说明文本。
  • Android:在Settings -> Android -> Manifest中添加android.permission.READ_EXTERNAL_STORAGE、android.permission.READ_MEDIA_IMAGES、android.permission.READ_MEDIA_VIDEO(根据Android版本选择对应权限)。

方案二:FlutterFlow组件+少量自定义代码(更贴合FF生态)

步骤1:权限请求

使用FlutterFlow自带的Permission Actions,在页面初始化时请求对应权限:

  • iOS:请求Photos权限
  • Android:请求Storage/Photos/Videos权限

步骤2:封装数据获取逻辑

创建Custom Functions封装异步数据获取:

  1. 获取相册列表:写异步函数调用PhotoGallery.listAlbums(),返回相册列表并存储到FF的App State。
  2. 获取相册媒体:写接收相册ID的异步函数,调用Album.listMedia()获取媒体列表,同样存储到App State。

步骤3:用FF组件展示内容

  • 相册列表:使用FF的GridView组件绑定App State中的相册列表,缩略图用自定义Widget封装AlbumThumbnailProvider实现加载。
  • 媒体列表:同理用GridView展示媒体缩略图,缩略图通过自定义Widget封装ThumbnailProvider加载。
  • 图片查看:使用FF的Image Viewer组件,通过自定义函数获取图片本地路径或PhotoProvider传递给组件。
  • 视频播放:使用FF的Video Player组件,通过自定义函数获取视频本地文件路径作为播放源。

步骤4:状态管理

全程使用FlutterFlow的App State存储相册、媒体数据,确保组件间数据共享和状态同步。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 13:05:55