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

Flutter音频播放页面Provider状态不更新问题求助

Flutter音频应用状态更新异常修复方案

问题背景

开发的Flutter音频应用从Firebase获取音频播放正常,但退出播放页面再返回时,进度条、播放/暂停按钮、音频时长等状态无法正确更新。此前使用本地状态时功能正常,改用Provider后状态同步出现异常。

核心问题分析

  1. Provider作用域错误:在PlayingStory页面的build方法内创建ChangeNotifierProvider,导致每次页面重建都会生成全新的PlayerStateModel实例,之前的状态完全丢失。
  2. 本地状态与Provider状态割裂:播放按钮依赖本地isPlaying变量,未与Provider中的状态绑定,页面返回时本地状态重置为初始值,导致按钮显示与实际播放状态不符。
  3. 音频播放器生命周期未全局管理:AudioPlayer是页面局部变量,退出页面时播放器实例可能被销毁或状态无法保留,返回页面重新初始化后,状态无法同步到Provider。
  4. 页面重新进入未同步状态:页面从后台返回或重新进入时,没有主动从播放器同步当前播放状态、进度、时长到Provider。

修复步骤

  1. 全局共享PlayerStateModel:在应用根节点(如MaterialApp上层)提供PlayerStateModel,确保所有页面使用同一个实例。
  2. 将AudioPlayer移入Provider:在PlayerStateModel中维护播放器实例,统一管理播放状态、进度、时长的监听与更新。
  3. 移除本地状态依赖:所有UI组件直接通过Consumer或Provider.of获取Provider中的状态,不再使用本地变量。
  4. 页面生命周期同步状态:在页面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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 02:39:53