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

Flutter问题:使用just_audio实现多页面音频持续播放

解决just_audio页面切换时音频停止的问题

核心思路

把音频播放器实例从页面组件中抽离,放到全局状态管理容器或者单例类中,让它独立于页面生命周期存在。这样切换页面时,播放器实例不会被销毁,音频就能持续播放;同时通过全局状态来控制播放列表的切换逻辑。

具体实现步骤

1. 创建全局播放器实例(单例模式)

创建一个单例类统一管理播放器,确保整个APP中只有一个播放器实例,避免重复创建和销毁:

import 'package:just_audio/just_audio.dart';
import 'package:just_audio_background/just_audio_background.dart';

class AudioPlayerManager {
  // 单例实例
  static final AudioPlayerManager _instance = AudioPlayerManager._internal();
  factory AudioPlayerManager() => _instance;
  AudioPlayerManager._internal() {
    // 初始化播放器,复用你已有的后台播放配置
    _player = AudioPlayer();
  }

  late AudioPlayer _player;
  AudioPlayer get player => _player;

  // 标记当前播放列表ID,用于判断是否需要切换列表
  String? _currentPlaylistId;

  // 加载播放列表:仅当新列表ID与当前不同时,才停止并切换
  Future<void> loadPlaylist(String playlistId, List<AudioSource> sources) async {
    if (_currentPlaylistId == playlistId) {
      // 同一列表,直接恢复播放(如果当前未播放)
      if (!_player.playing) {
        await _player.play();
      }
      return;
    }
    // 新列表:停止当前播放,加载新资源
    await _player.stop();
    await _player.setAudioSource(
      ConcatenatingAudioSource(children: sources),
      initialIndex: 0,
      initialPosition: Duration.zero,
    );
    _currentPlaylistId = playlistId;
    await _player.play();
  }

  // APP退出时调用,释放播放器资源
  Future<void> dispose() async {
    await _player.dispose();
  }
}

2. 确认后台播放初始化

确保main函数中已正确初始化just_audio_background(你已实现,这里再做一次校验):

void main() async {
  await JustAudioBackground.init(
    androidNotificationChannelId: 'com.yourapp.channel.audio',
    androidNotificationChannelName: '音频播放',
    androidNotificationOngoing: true,
  );
  runApp(const MyApp());
}

3. 在播放器页面使用全局实例

页面中不再创建本地播放器,直接调用全局的AudioPlayerManager:

class PlayerPage extends StatefulWidget {
  final String playlistId;
  final List<AudioSource> playlistSources;

  const PlayerPage({super.key, required this.playlistId, required this.playlistSources});

  @override
  State<PlayerPage> createState() => _PlayerPageState();
}

class _PlayerPageState extends State<PlayerPage> {
  late AudioPlayerManager _playerManager;

  @override
  void initState() {
    super.initState();
    _playerManager = AudioPlayerManager();
    // 进入页面时自动加载对应播放列表
    _playerManager.loadPlaylist(widget.playlistId, widget.playlistSources);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('音频播放器')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 播放状态控制按钮
            StreamBuilder<PlayerState>(
              stream: _playerManager.player.playerStateStream,
              builder: (context, snapshot) {
                final playerState = snapshot.data;
                final processingState = playerState?.processingState;
                final playing = playerState?.playing;

                if (processingState == ProcessingState.loading || processingState == ProcessingState.buffering) {
                  return const CircularProgressIndicator();
                } else if (playing != true) {
                  return IconButton(
                    icon: const Icon(Icons.play_arrow),
                    iconSize: 64.0,
                    onPressed: _playerManager.player.play,
                  );
                } else if (processingState != ProcessingState.completed) {
                  return IconButton(
                    icon: const Icon(Icons.pause),
                    iconSize: 64.0,
                    onPressed: _playerManager.player.pause,
                  );
                } else {
                  return IconButton(
                    icon: const Icon(Icons.replay),
                    iconSize: 64.0,
                    onPressed: () => _playerManager.player.seek(Duration.zero),
                  );
                }
              },
            ),
            // 可添加进度条、上/下一曲等其他控制组件
          ],
        ),
      ),
    );
  }

  // 页面销毁时不要释放播放器,全局实例需保留至APP退出
  @override
  void dispose() {
    super.dispose();
  }
}

4. 切换播放列表的逻辑

当用户选择新播放列表时,调用全局管理器的loadPlaylist方法,自动完成切换判断:

// 示例:在播放列表选择页面的点击事件中
void onSelectPlaylist(String newPlaylistId, List<AudioSource> newSources) async {
  await AudioPlayerManager().loadPlaylist(newPlaylistId, newSources);
  // 可选:跳转到播放器页面
  Navigator.push(context, MaterialPageRoute(
    builder: (context) => PlayerPage(playlistId: newPlaylistId, playlistSources: newSources)
  ));
}

关键注意事项

  • 全局播放器实例仅在APP完全退出时(可通过WidgetsBindingObserver监听APP生命周期)调用dispose释放资源,避免内存泄漏。
  • 单例模式是最简单的全局实例实现方式,也可使用Provider、Riverpod等状态管理库托管实例,效果一致。
  • 已配置的just_audio_background后台播放功能不受影响,全局实例的方式会延续其后台播放能力。

内容的提问来源于stack exchange,提问作者Rafael Schaffer Gimenes

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 16:27:07