Flutter 3.7.3下从Firebase Storage下载音频并实现播放的求助
Flutter 3.7.3 音频下载、列表展示与底部播放实现
前置准备
- 完成Firebase项目配置,在Flutter应用中集成Firebase Core和Storage(按Firebase官方指引完成Android/iOS平台配置)
- 在
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
- Android端在
AndroidManifest.xml添加存储权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
- 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'; } }
关键说明
- 分页加载:通过Firebase Storage的
list方法分页获取音频,每次加载50条,避免一次性加载10000条导致内存溢出和UI卡顿 - 本地缓存:下载后的音频存储在应用专属文档目录,标记为已下载,后续播放无需重复下载
- 状态管理:使用
ChangeNotifier统一管理播放状态、当前音频和进度,通过Provider实现页面间状态共享 - 权限处理:针对Android/iOS平台请求存储权限,确保音频能正常写入本地
内容的提问来源于stack exchange,提问作者osama nabil
相关产品推荐
相关产品推荐

