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

Flutter 3.7.3下从Firebase Storage下载音频并实现播放的求助

Flutter 3.7.3 音频下载、列表展示与底部播放实现

前置准备

  1. 完成Firebase项目配置,在Flutter应用中集成Firebase Core和Storage(按Firebase官方指引完成Android/iOS平台配置)
  2. 在pubspec.yaml中添加依赖:
dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^2.10.0
  firebase_storage: ^11.2.0
  just_audio: ^0.9.34
  path_provider: ^2.0.15
  permission_handler: ^10.2.0
  1. Android端在AndroidManifest.xml添加存储权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  1. iOS端在Info.plist添加权限描述:
<key>NSAppleMusicUsageDescription</key>
<string>需要访问音频文件进行播放</string>
<key>NSFileProviderDomainUsageDescription</key>
<string>需要存储下载的音频文件</string>

完整可运行代码

1. 音频数据模型

class AudioItem {
  final String id;
  final String name;
  final String storagePath;
  String? localFilePath;
  bool isDownloaded = false;

  AudioItem({
    required this.id,
    required this.name,
    required this.storagePath,
    this.localFilePath,
    this.isDownloaded = false,
  });
}

2. 音频播放与下载管理器

import 'dart:io';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/foundation.dart';
import 'package:just_audio/just_audio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';

class AudioPlayerManager extends ChangeNotifier {
  final AudioPlayer _audioPlayer = AudioPlayer();
  AudioItem? _currentPlayingAudio;
  bool _isPlaying = false;
  Duration _currentPosition = Duration.zero;
  Duration _totalDuration = Duration.zero;

  AudioItem? get currentPlayingAudio => _currentPlayingAudio;
  bool get isPlaying => _isPlaying;
  Duration get currentPosition => _currentPosition;
  Duration get totalDuration => _totalDuration;

  AudioPlayerManager() {
    _initAudioPlayerListeners();
  }

  void _initAudioPlayerListeners() {
    _audioPlayer.playerStateStream.listen((state) {
      _isPlaying = state.playing;
      notifyListeners();
    });

    _audioPlayer.positionStream.listen((position) {
      _currentPosition = position;
      notifyListeners();
    });

    _audioPlayer.durationStream.listen((duration) {
      if (duration != null) {
        _totalDuration = duration;
        notifyListeners();
      }
    });
  }

  // 请求存储权限
  Future<bool> requestStoragePermission() async {
    if (kIsWeb) return true;
    PermissionStatus status = await Permission.storage.request();
    return status.isGranted;
  }

  // 下载音频到本地
  Future<void> downloadAudio(AudioItem audio) async {
    bool hasPermission = await requestStoragePermission();
    if (!hasPermission) return;

    try {
      Reference ref = FirebaseStorage.instance.ref(audio.storagePath);
      Directory appDir = await getApplicationDocumentsDirectory();
      File localFile = File('${appDir.path}/${audio.id}.mp3');

      await ref.writeToFile(localFile);

      audio.localFilePath = localFile.path;
      audio.isDownloaded = true;
      notifyListeners();
    } catch (e) {
      debugPrint('下载失败: $e');
    }
  }

  // 播放音频
  Future<void> playAudio(AudioItem audio) async {
    try {
      if (!audio.isDownloaded) {
        await downloadAudio(audio);
      }

      if (audio.localFilePath == null) return;

      if (_currentPlayingAudio?.id != audio.id) {
        await _audioPlayer.setFilePath(audio.localFilePath!);
        _currentPlayingAudio = audio;
      }

      if (_isPlaying) {
        await _audioPlayer.pause();
      } else {
        await _audioPlayer.play();
      }
    } catch (e) {
      debugPrint('播放失败: $e');
    }
  }

  // 暂停播放
  Future<void> pauseAudio() async {
    await _audioPlayer.pause();
  }

  // 进度跳转
  Future<void> seekTo(Duration position) async {
    await _audioPlayer.seek(position);
  }

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

3. 主页面实现

import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:provider/provider.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => AudioPlayerManager(),
      child: MaterialApp(
        title: '音频播放器',
        theme: ThemeData(primarySwatch: Colors.blue),
        home: const AudioListPage(),
      ),
    );
  }
}

class AudioListPage extends StatefulWidget {
  const AudioListPage({super.key});

  @override
  State<AudioListPage> createState() => _AudioListPageState();
}

class _AudioListPageState extends State<AudioListPage> {
  List<AudioItem> _audioList = [];
  bool _isLoading = true;
  String? _nextPageToken;

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

  // 从Firebase Storage分页加载音频列表
  Future<void> _loadAudioList() async {
    try {
      ListResult result = await FirebaseStorage.instance
          .ref('audios/') // 替换为你的音频存储根路径
          .list(ListOptions(
            maxResults: 50, // 每页加载50条,避免大列表卡顿
            pageToken: _nextPageToken,
          ));

      List<AudioItem> newAudios = result.items.map((ref) {
        return AudioItem(
          id: ref.name.split('.')[0],
          name: ref.name,
          storagePath: ref.fullPath,
        );
      }).toList();

      setState(() {
        _audioList.addAll(newAudios);
        _nextPageToken = result.nextPageToken;
        _isLoading = false;
      });
    } catch (e) {
      debugPrint('加载列表失败: $e');
      setState(() => _isLoading = false);
    }
  }

  // 加载更多音频
  Future<void> _loadMore() async {
    if (_nextPageToken == null) return;
    setState(() => _isLoading = true);
    await _loadAudioList();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('音频列表')),
      body: _isLoading && _audioList.isEmpty
          ? const Center(child: CircularProgressIndicator())
          : ListView.builder(
              itemCount: _audioList.length + (_nextPageToken != null ? 1 : 0),
              itemBuilder: (context, index) {
                if (index == _audioList.length) {
                  return Center(
                    child: TextButton(
                      onPressed: _loadMore,
                      child: const Text('加载更多'),
                    ),
                  );
                }

                AudioItem audio = _audioList[index];
                return ListTile(
                  title: Text(audio.name),
                  subtitle: audio.isDownloaded
                      ? const Text('已下载')
                      : const Text('未下载'),
                  trailing: IconButton(
                    icon: Icon(
                      context.watch<AudioPlayerManager>().currentPlayingAudio?.id == audio.id &&
                              context.watch<AudioPlayerManager>().isPlaying
                          ? Icons.pause
                          : Icons.play_arrow,
                    ),
                    onPressed: () {
                      context.read<AudioPlayerManager>().playAudio(audio);
                    },
                  ),
                  onTap: () {
                    context.read<AudioPlayerManager>().playAudio(audio);
                  },
                );
              },
            ),
      bottomNavigationBar: const AudioPlayerBottomBar(),
    );
  }
}

class AudioPlayerBottomBar extends StatelessWidget {
  const AudioPlayerBottomBar({super.key});

  @override
  Widget build(BuildContext context) {
    AudioPlayerManager manager = context.watch<AudioPlayerManager>();
    if (manager.currentPlayingAudio == null) {
      return const SizedBox.shrink();
    }

    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      color: Colors.grey[200],
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(manager.currentPlayingAudio!.name),
          const SizedBox(height: 8),
          Row(
            children: [
              Text(_formatDuration(manager.currentPosition)),
              Expanded(
                child: Slider(
                  value: manager.currentPosition.inSeconds.toDouble(),
                  max: manager.totalDuration.inSeconds.toDouble(),
                  onChanged: (value) {
                    manager.seekTo(Duration(seconds: value.toInt()));
                  },
                ),
              ),
              Text(_formatDuration(manager.totalDuration)),
            ],
          ),
          IconButton(
            icon: Icon(manager.isPlaying ? Icons.pause : Icons.play_arrow),
            iconSize: 32,
            onPressed: () {
              if (manager.isPlaying) {
                manager.pauseAudio();
              } else {
                manager.playAudio(manager.currentPlayingAudio!);
              }
            },
          ),
        ],
      ),
    );
  }

  String _formatDuration(Duration duration) {
    String twoDigits(int n) => n.toString().padLeft(2, '0');
    String minutes = twoDigits(duration.inMinutes.remainder(60));
    String seconds = twoDigits(duration.inSeconds.remainder(60));
    return '$minutes:$seconds';
  }
}

关键说明

  1. 分页加载:通过Firebase Storage的list方法分页获取音频,每次加载50条,避免一次性加载10000条导致内存溢出和UI卡顿
  2. 本地缓存:下载后的音频存储在应用专属文档目录,标记为已下载,后续播放无需重复下载
  3. 状态管理:使用ChangeNotifier统一管理播放状态、当前音频和进度,通过Provider实现页面间状态共享
  4. 权限处理:针对Android/iOS平台请求存储权限,确保音频能正常写入本地

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 09:02:57