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

如何用Flutter+Riverpod+JustAudio实现同步的音乐播放界面?

基于Riverpod + JustAudio 实现跨页面同步的音乐播放器方案

核心思路:用Riverpod管理全局播放状态

把AudioPlayer实例和播放相关状态(当前歌曲、播放进度、播放状态等)全部交给Riverpod的Provider统一管理,让所有UI组件(主播放页、迷你播放器)都从Provider读取状态、调用操作方法,彻底解耦UI与播放器实例。

具体实现步骤

1. 创建全局播放状态Provider

先定义包含播放器和核心状态的类,用StateNotifierProvider提供全局访问:

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

// 自定义歌曲模型,按需扩展字段
class Song {
  final String title;
  final String artist;
  final String audioUrl;

  Song({required this.title, required this.artist, required this.audioUrl});
}

// 封装播放核心状态
class PlayerState {
  final AudioPlayer player;
  final Song? currentSong;
  final Duration? position;
  final Duration? duration;
  final PlayerState playerStatus;

  PlayerState({
    required this.player,
    this.currentSong,
    this.position,
    this.duration,
    required this.playerStatus,
  });

  // 状态复制方法,用于局部更新
  PlayerState copyWith({
    AudioPlayer? player,
    Song? currentSong,
    Duration? position,
    Duration? duration,
    PlayerState? playerStatus,
  }) {
    return PlayerState(
      player: player ?? this.player,
      currentSong: currentSong ?? this.currentSong,
      position: position ?? this.position,
      duration: duration ?? this.duration,
      playerStatus: playerStatus ?? this.playerStatus,
    );
  }
}

// 状态管理类,处理播放器逻辑与状态同步
class PlayerStateNotifier extends StateNotifier<PlayerState> {
  PlayerStateNotifier() : super(PlayerState(player: AudioPlayer(), playerStatus: PlayerState.stopped)) {
    _initPlayerListeners();
  }

  // 监听播放器的各类状态流,同步到StateNotifier
  void _initPlayerListeners() {
    state.player.playerStateStream.listen((status) {
      state = state.copyWith(playerStatus: status);
    });

    state.player.positionStream.listen((pos) {
      state = state.copyWith(position: pos);
    });

    state.player.durationStream.listen((dur) {
      state = state.copyWith(duration: dur);
    });
  }

  // 加载并播放指定歌曲
  Future<void> playSong(Song song) async {
    await state.player.setUrl(song.audioUrl);
    state = state.copyWith(currentSong: song);
    await state.player.play();
  }

  // 暂停播放
  void pause() => state.player.pause();

  // 恢复播放
  void resume() => state.player.play();

  // 切换播放/暂停状态
  void togglePlayPause() {
    state.playerStatus.playing ? pause() : resume();
  }

  @override
  void dispose() {
    state.player.dispose();
    super.dispose();
  }
}

// 全局Provider,所有UI组件通过它获取播放状态与操作能力
final playerStateProvider = StateNotifierProvider<PlayerStateNotifier, PlayerState>((ref) {
  return PlayerStateNotifier();
});

2. 实现迷你播放器组件

迷你播放器仅从Provider读取状态,无需持有播放器实例:

class MiniPlayer extends ConsumerWidget {
  const MiniPlayer({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final playerState = ref.watch(playerStateProvider);
    final currentSong = playerState.currentSong;

    // 无播放歌曲时隐藏迷你播放器
    if (currentSong == null) return const SizedBox.shrink();

    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      color: Colors.grey[900],
      child: Row(
        children: [
          const SizedBox(width: 48, height: 48, child: ColoredBox(color: Colors.blue)),
          const SizedBox(width: 12),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(currentSong.title, style: const TextStyle(color: Colors.white, fontSize: 14)),
                Text(currentSong.artist, style: TextStyle(color: Colors.grey[400], fontSize: 12)),
              ],
            ),
          ),
          IconButton(
            icon: Icon(
              playerState.playerStatus.playing ? Icons.pause : Icons.play_arrow,
              color: Colors.white,
            ),
            onPressed: () => ref.read(playerStateProvider.notifier).togglePlayPause(),
          ),
        ],
      ),
    );
  }
}

3. 实现主播放页面

主播放页同样通过Provider获取状态,所有操作调用Notifier方法:

class PlayerScreen extends ConsumerWidget {
  const PlayerScreen({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final playerState = ref.watch(playerStateProvider);
    final currentSong = playerState.currentSong;

    if (currentSong == null) {
      return const Center(child: Text('暂无播放歌曲'));
    }

    return Scaffold(
      appBar: AppBar(title: const Text('正在播放')),
      body: Column(
        children: [
          Expanded(
            child: Center(
              child: ColoredBox(color: Colors.blue, width: 200, height: 200),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(16),
            child: Column(
              children: [
                Text(currentSong.title, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
                const SizedBox(height: 8),
                Text(currentSong.artist, style: TextStyle(color: Colors.grey[600])),
              ],
            ),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: Slider(
              value: playerState.position?.inMilliseconds.toDouble() ?? 0,
              max: playerState.duration?.inMilliseconds.toDouble() ?? 100,
              onChanged: (value) {
                ref.read(playerStateProvider.notifier).state.player.seek(Duration(milliseconds: value.toInt()));
              },
            ),
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              IconButton(
                icon: const Icon(Icons.skip_previous, size: 40),
                onPressed: () { /* 实现上一曲逻辑 */ },
              ),
              IconButton(
                icon: Icon(
                  playerState.playerStatus.playing ? Icons.pause_circle : Icons.play_circle,
                  size: 60,
                ),
                onPressed: () => ref.read(playerStateProvider.notifier).togglePlayPause(),
              ),
              IconButton(
                icon: const Icon(Icons.skip_next, size: 40),
                onPressed: () { /* 实现下一曲逻辑 */ },
              ),
            ],
          ),
          const SizedBox(height: 32),
        ],
      ),
    );
  }
}

4. 全局嵌入迷你播放器

将迷你播放器放在全局布局的底部(比如Scaffold的bottomNavigationBar),确保所有页面都能显示并同步状态:

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: const Navigator(/* 你的页面路由逻辑 */),
        bottomNavigationBar: const MiniPlayer(),
      ),
    );
  }
}

关键注意事项

  • 状态同步:确保播放器的所有流(状态、进度、时长)都正确监听并同步到PlayerState,保证UI实时响应变化。
  • 资源释放:在PlayerStateNotifier的dispose方法中必须调用AudioPlayer.dispose(),避免内存泄漏。
  • 单一实例:通过StateNotifierProvider确保整个应用只有一个AudioPlayer实例,避免多实例冲突。
  • 错误处理:在playSong等方法中加入try-catch,处理音频加载失败的情况,可新增状态字段提示用户。

内容的提问来源于stack exchange,提问作者Proud boy

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 22:22:01