Flutter音频播放页面Provider状态不更新问题求助
Flutter音频应用状态更新异常修复方案
问题背景
开发的Flutter音频应用从Firebase获取音频播放正常,但退出播放页面再返回时,进度条、播放/暂停按钮、音频时长等状态无法正确更新。此前使用本地状态时功能正常,改用Provider后状态同步出现异常。
核心问题分析
- Provider作用域错误:在
PlayingStory页面的build方法内创建ChangeNotifierProvider,导致每次页面重建都会生成全新的PlayerStateModel实例,之前的状态完全丢失。 - 本地状态与Provider状态割裂:播放按钮依赖本地
isPlaying变量,未与Provider中的状态绑定,页面返回时本地状态重置为初始值,导致按钮显示与实际播放状态不符。 - 音频播放器生命周期未全局管理:
AudioPlayer是页面局部变量,退出页面时播放器实例可能被销毁或状态无法保留,返回页面重新初始化后,状态无法同步到Provider。 - 页面重新进入未同步状态:页面从后台返回或重新进入时,没有主动从播放器同步当前播放状态、进度、时长到Provider。
修复步骤
- 全局共享PlayerStateModel:在应用根节点(如
MaterialApp上层)提供PlayerStateModel,确保所有页面使用同一个实例。 - 将AudioPlayer移入Provider:在
PlayerStateModel中维护播放器实例,统一管理播放状态、进度、时长的监听与更新。 - 移除本地状态依赖:所有UI组件直接通过
Consumer或Provider.of获取Provider中的状态,不再使用本地变量。 - 页面生命周期同步状态:在页面
resumed时,主动从播放器同步最新状态到Provider。
修改后的代码
1. 状态类(PlayerStateModel)
import 'package:flutter/widgets.dart'; import 'package:just_audio/just_audio.dart'; class PlayerStateModel extends ChangeNotifier { final AudioPlayer audioPlayer = AudioPlayer(); bool _isPlaying = false; Duration _currentPosition = Duration.zero; Duration _duration = Duration.zero; bool get isPlaying => _isPlaying; Duration get duration => _duration; Duration get currentPosition => _currentPosition; PlayerStateModel() { // 初始化播放器流监听 _setupStreams(); } void _setupStreams() { // 监听播放位置变化 audioPlayer.positionStream.listen((position) { _currentPosition = position; notifyListeners(); }); // 监听音频时长加载完成 audioPlayer.durationStream.listen((duration) { if (duration != null) { _duration = duration; notifyListeners(); } }); // 监听播放状态变化 audioPlayer.playerStateStream.listen((state) { _isPlaying = state.playing; notifyListeners(); }); } // 切换播放/暂停状态 void togglePlayPause() { _isPlaying = !_isPlaying; if (_isPlaying) { audioPlayer.play(); } else { audioPlayer.pause(); } notifyListeners(); } // 设置播放位置 void seekToPosition(Duration value) { audioPlayer.seek(value); _currentPosition = value; notifyListeners(); } // 释放资源 @override void dispose() { audioPlayer.dispose(); super.dispose(); } }
2. 播放页面(PlayingStory)
import 'dart:ui'; import 'package:app/model/PlayerState.dart'; import 'package:audio_service/audio_service.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:app/globals.dart' as globals; import 'package:provider/provider.dart'; import '../widgets/error_alert.dart'; import 'package:audio_session/audio_session.dart'; class PlayingStory extends StatefulWidget { final String story; final String episode; final String playLink; final String poster; const PlayingStory({ Key? key, required this.story, required this.episode, required this.playLink, required this.poster, }) : super(key: key); @override State<StatefulWidget> createState() => PlayingStoryState(); } class PlayingStoryState extends State<PlayingStory> with WidgetsBindingObserver { late String episodeTitle = widget.episode.split("=")[0]; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _initAudioSessionAndSource(); } Future<void> _initAudioSessionAndSource() async { final playerState = Provider.of<PlayerStateModel>(context, listen: false); // 配置音频会话 final session = await AudioSession.instance; await session.configure(const AudioSessionConfiguration.speech()); try { // 设置音频源 await playerState.audioPlayer.setAudioSource(AudioSource.uri( Uri.parse(widget.playLink), tag: MediaItem( id: 'StoryFy', album: widget.story, title: episodeTitle, artUri: Uri.parse(widget.poster), ), )); } catch (e) { if (kDebugMode) { print("加载音频源失败: $e"); } } } // 格式化时长显示 String formatDuration(Duration duration) { final hours = duration.inHours.remainder(60).toString().padLeft(2, '0'); final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0'); final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); return '$hours:$minutes:$seconds'; } // 页面生命周期变化时同步状态 @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); if (state == AppLifecycleState.resumed) { final playerState = Provider.of<PlayerStateModel>(context, listen: false); // 强制同步播放器当前状态 playerState.seekToPosition(playerState.audioPlayer.position); } } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; final width = screenSize.width; final height = screenSize.height; return Scaffold( appBar: AppBar(), body: Padding( padding: EdgeInsets.symmetric(horizontal: width * 0.03), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 海报区域 Center( child: Card( elevation: 10, child: ClipRRect( borderRadius: BorderRadius.circular(10), child: Image.network( widget.poster, height: height * 0.5, width: width, fit: BoxFit.fill, ), ), ), ), SizedBox(height: height * 0.05), // 剧集标题 Center( child: Text( episodeTitle, style: TextStyle(fontSize: height * 0.03), ), ), SizedBox(height: height * 0.01), // 故事名称 Center( child: Text( widget.story, style: TextStyle(fontSize: height * 0.02), ), ), SizedBox(height: height * 0.02), // 进度条 Consumer<PlayerStateModel>( builder: (context, playerState, child) { return Slider( value: playerState.currentPosition.inSeconds.toDouble(), min: 0.0, max: playerState.duration.inSeconds.toDouble(), inactiveColor: globals.shadowColor, onChanged: (newValue) { final clampedValue = newValue.clamp( 0.0, playerState.duration.inSeconds.toDouble() ); playerState.seekToPosition(Duration(seconds: clampedValue.toInt())); }, ); }, ), // 时长显示 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Consumer<PlayerStateModel>( builder: (context, playerState, child) { return Text(formatDuration(playerState.currentPosition)); }, ), Consumer<PlayerStateModel>( builder: (context, playerState, child) { return Text(formatDuration(playerState.duration)); }, ), ], ), SizedBox(height: height * 0.02), // 控制按钮组 Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( onPressed: () { showAlert(context, "data.values.toList()[index]"); Provider.of<PlayerStateModel>(context, listen: false) .audioPlayer .seekToPrevious(); }, iconSize: height * 0.05, color: Colors.storyfytheme, icon: const Icon(Icons.skip_previous_rounded), ), IconButton( onPressed: () async { final playerState = Provider.of<PlayerStateModel>(context, listen: false); final currentPosition = playerState.audioPlayer.position; final duration = playerState.audioPlayer.duration; if (currentPosition != null && duration != null) { final newPosition = currentPosition - const Duration(seconds: 10); final clampedPosition = newPosition.isNegative ? Duration.zero : newPosition; playerState.seekToPosition(clampedPosition); } }, iconSize: height * 0.05, color: Colors.storyfytheme, icon: const Icon(Icons.replay_10_rounded), ), // 播放/暂停按钮 Consumer<PlayerStateModel>( builder: (context, playerState, child) { return IconButton( onPressed: playerState.togglePlayPause, iconSize: height * 0.05, color: Colors.storyfytheme, icon: playerState.isPlaying ? const Icon(Icons.pause) : const Icon(Icons.play_arrow_rounded), ); }, ), IconButton( onPressed: () async { final playerState = Provider.of<PlayerStateModel>(context, listen: false); final currentPosition = playerState.audioPlayer.position; final duration = playerState.audioPlayer.duration; if (currentPosition != null && duration != null) { final newPosition = currentPosition + const Duration(seconds: 10); final clampedPosition = newPosition.compareTo(duration) > 0 ? duration : newPosition; playerState.seekToPosition(clampedPosition); } }, iconSize: height * 0.05, color: Colors.storyfytheme, icon: const Icon(Icons.forward_10_rounded), ), IconButton( onPressed: () { showAlert(context, "data.values.toList()[index]"); Provider.of<PlayerStateModel>(context, listen: false) .audioPlayer .seekToNext(); }, iconSize: height * 0.05, color: Colors.storyfytheme, icon: const Icon(Icons.skip_next_rounded), ) ], ) ], ), ), ); } }
3. 全局提供Provider
在应用入口处配置全局Provider,确保状态全局共享:
void main() { runApp( ChangeNotifierProvider( create: (context) => PlayerStateModel(), child: const MyApp(), ), ); }
内容的提问来源于stack exchange,提问作者Dhanrajsinh Parmar
相关产品推荐
相关产品推荐

