Flutter音乐播放器:实现直接返回音乐列表而非逐个回退
问题描述
我用Flutter和audio player开发音乐播放器,想实现上一曲/下一曲功能。现在每首音乐单独创建StatefulWidget类,通过Navigator.push跳转切换曲目,但路由栈会变得混乱——比如从音乐列表进入B音乐页面,多次切换后栈变成「音乐列表→B→C→B→A→B」,点击AppBar的返回按钮要逐个回退所有访问过的页面,无法直接回到音乐列表。纠结是把所有音乐整合到同一个类,还是修改导航逻辑,求解决方案。
附其中一个音乐页面代码:
class Music1 extends StatefulWidget { const Music1({Key? key}) : super(key: key); @override State<Music1> createState() => _Music1State(); } class _Music1State extends State<Music1> { //setting the project url String img_cover_url = "https://i.pinimg.com/736x/a7/a9/cb/a7a9cbcefc58f5b677d8c480cf4ddc5d.jpg"; late AudioPlayer advancedPlayer; Duration _duration = new Duration(); Duration _position = new Duration(); bool isPlaying = false; bool isPaused = false; bool isLoop = false; void initPlayer() async { await advancedPlayer.setSource(AssetSource("forest.mp3")); advancedPlayer.onDurationChanged.listen((d) {setState(() { _duration = d; }); }); advancedPlayer.onPositionChanged.listen((p) {setState(() { _position = p; }); }); } //init the player @override void initState() { // TODO: implement initState super.initState(); advancedPlayer = AudioPlayer(); initPlayer(); } @override void changeToSecond(int second) { Duration newDuration = Duration(seconds: second); advancedPlayer.seek(newDuration); } Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Musics for Kids", style: TextStyle(fontFamily: "Itim",fontSize: 25)),), body: Stack( children: [ Container( constraints: const BoxConstraints.expand(), height: 300.0, width: 300.0, decoration: const BoxDecoration( image: DecorationImage( image: AssetImage("assets/forest.jpg"), fit: BoxFit.cover, ), ), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 28, sigmaY: 28), child: Container( color: Colors.black.withOpacity(0.6), ), ), ), Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ //setting the music cover ClipRRect( borderRadius: BorderRadius.circular(30.0), child: Image.asset( "assets/forest.jpg", width: 250.0, ), ), const SizedBox( height: 10.0, ), const Text( "Forest", style: TextStyle( color: Colors.white, fontSize: 36, letterSpacing: 6, fontFamily: "Itim"), ), //Setting the seekbar const SizedBox( height: 30.0, //50 ), Row( //problem part - previous and next buttons mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( onTap: () { Navigator.push(context, MaterialPageRoute<void>(builder: (BuildContext context) { return const Music0(); }) ); }, onTap: () => Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (context)=> MusicList(), ), (route) => true, ), child: Text("Previous", style: TextStyle(color: Colors.white)), ), SizedBox(width: 200), GestureDetector( onTap: () { Navigator.push(context, MaterialPageRoute<void>(builder: (BuildContext context) { return const Music2(); }) ); }, child: Text("Next", style: TextStyle(color: Colors.white)), ), ], ), const SizedBox( height: 20.0, //50 ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( _position.toString().split(".")[0], style: const TextStyle( color: Colors.white, ), ), Slider.adaptive( onChanged: (value){}, min: 0.0, max: _duration.inSeconds.toDouble(), value:_position.inSeconds.toDouble(), onChangeEnd: (double value){ setState((){ changeToSecond(value.toInt()); value = value; }); advancedPlayer.pause(); advancedPlayer.seek(Duration(seconds: value.toInt())); advancedPlayer.resume(); }, activeColor: Colors.white, ), Text( _duration.toString().split(".")[0], style: const TextStyle( color: Colors.white, ), ), ], ), const SizedBox( height: 60.0, ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(60.0), color: Colors.black87, border: Border.all(color: Colors.white38), ), width: 50.0, height: 50.0, child: InkWell( onTapDown: (details) { advancedPlayer.setPlaybackRate(0.5); }, onTapUp: (details) { advancedPlayer.setPlaybackRate(1); }, child: const Center( child: Icon( Icons.fast_rewind_rounded, color: Colors.white, ), ), ), ), const SizedBox( width: 60.0, ), Container( width: 50.0, height: 50.0, decoration: BoxDecoration( borderRadius: BorderRadius.circular(60.0), color: Colors.black87, border: Border.all(color: Colors.pink), ), child: InkWell( onTap: () async{ if (isPlaying) { await advancedPlayer.pause(); setState((){ isPlaying = false; }); } else { await advancedPlayer.resume(); setState((){ isPlaying = true; }); } }, child: Icon( isPlaying ? Icons.pause: Icons.play_arrow, color: Colors.white, ), ) ), const SizedBox( width: 60.0, ), Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(60.0), color: Colors.black87, border: Border.all(color: Colors.white38), ), width: 50.0, height: 50.0, child: InkWell( onTapDown: (details) { advancedPlayer.setPlaybackRate(2); }, onTapUp: (details) { advancedPlayer.setPlaybackRate(1); }, child: const Center( child: Icon( Icons.fast_forward_rounded, color: Colors.white, ), ), ), ), ], ) ], ), ], ), ); } }
解决方案
方案一:整合到同一播放页面(推荐)
核心思路是把音乐数据抽成独立模型,用单个播放页面接收不同音乐数据,切换曲目时只更新页面状态,不跳转新页面。这样路由栈始终保持「音乐列表→播放页面」的结构,点击返回直接回到列表。
步骤1:定义音乐数据模型
class MusicModel { final String title; final String coverAsset; final String audioAsset; MusicModel({required this.title, required this.coverAsset, required this.audioAsset}); }
步骤2:创建全局音乐列表(或从列表页面传入)
final List<MusicModel> musicList = [ MusicModel(title: "Forest", coverAsset: "assets/forest.jpg", audioAsset: "forest.mp3"), MusicModel(title: "Ocean", coverAsset: "assets/ocean.jpg", audioAsset: "ocean.mp3"), MusicModel(title: "Rain", coverAsset: "assets/rain.jpg", audioAsset: "rain.mp3"), ];
步骤3:实现单个播放页面
class MusicPlayerPage extends StatefulWidget { final int initialIndex; final List<MusicModel> musics; const MusicPlayerPage({Key? key, required this.initialIndex, required this.musics}) : super(key: key); @override State<MusicPlayerPage> createState() => _MusicPlayerPageState(); } class _MusicPlayerPageState extends State<MusicPlayerPage> { late AudioPlayer advancedPlayer; Duration _duration = Duration(); Duration _position = Duration(); bool isPlaying = false; late int currentIndex; late MusicModel currentMusic; @override void initState() { super.initState(); currentIndex = widget.initialIndex; currentMusic = widget.musics[currentIndex]; advancedPlayer = AudioPlayer(); initPlayer(); } void initPlayer() async { await advancedPlayer.setSource(AssetSource(currentMusic.audioAsset)); advancedPlayer.onDurationChanged.listen((d) => setState(() => _duration = d)); advancedPlayer.onPositionChanged.listen((p) => setState(() => _position = p)); } void changeMusic(int newIndex) async { if (newIndex >= 0 && newIndex < widget.musics.length) { await advancedPlayer.stop(); setState(() { currentIndex = newIndex; currentMusic = widget.musics[currentIndex]; isPlaying = false; }); initPlayer(); await advancedPlayer.play(); setState(() => isPlaying = true); } } void changeToSecond(int second) { advancedPlayer.seek(Duration(seconds: second)); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Musics for Kids", style: TextStyle(fontFamily: "Itim", fontSize: 25))), body: Stack( children: [ Container( constraints: const BoxConstraints.expand(), decoration: BoxDecoration( image: DecorationImage( image: AssetImage(currentMusic.coverAsset), fit: BoxFit.cover, ), ), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 28, sigmaY: 28), child: Container(color: Colors.black.withOpacity(0.6)), ), ), Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ ClipRRect( borderRadius: BorderRadius.circular(30.0), child: Image.asset(currentMusic.coverAsset, width: 250.0), ), const SizedBox(height: 10.0), Text( currentMusic.title, style: const TextStyle(color: Colors.white, fontSize: 36, letterSpacing: 6, fontFamily: "Itim"), ), const SizedBox(height: 30.0), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( onTap: () => changeMusic(currentIndex - 1), child: const Text("Previous", style: TextStyle(color: Colors.white)), ), const SizedBox(width: 200), GestureDetector( onTap: () => changeMusic(currentIndex + 1), child: const Text("Next", style: TextStyle(color: Colors.white)), ), ], ), const SizedBox(height: 20.0), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( _position.toString().split(".")[0], style: const TextStyle(color: Colors.white), ), Slider.adaptive( onChanged: (value) {}, min: 0.0, max: _duration.inSeconds.toDouble(), value: _position.inSeconds.toDouble(), onChangeEnd: (double value) { changeToSecond(value.toInt()); if (isPlaying) { advancedPlayer.pause(); advancedPlayer.seek(Duration(seconds: value.toInt())); advancedPlayer.resume(); } }, activeColor: Colors.white, ), Text( _duration.toString().split(".")[0], style: const TextStyle(color: Colors.white), ), ], ), const SizedBox(height: 60.0), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(60.0), color: Colors.black87, border: Border.all(color: Colors.white38), ), width: 50.0, height: 50.0, child: InkWell( onTapDown: (details) => advancedPlayer.setPlaybackRate(0.5), onTapUp: (details) => advancedPlayer.setPlaybackRate(1), child: const Center(child: Icon(Icons.fast_rewind_rounded, color: Colors.white)), ), ), const SizedBox(width: 60.0), Container( width: 50.0, height: 50.0, decoration: BoxDecoration( borderRadius: BorderRadius.circular(60.0), color: Colors.black87, border: Border.all(color: Colors.pink), ), child: InkWell( onTap: () async { if (isPlaying) { await advancedPlayer.pause(); setState(() => isPlaying = false); } else { await advancedPlayer.resume(); setState(() => isPlaying = true); } }, child: Icon(isPlaying ? Icons.pause : Icons.play_arrow, color: Colors.white), ), ), const SizedBox(width: 60.0), Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(60.0), color: Colors.black87, border: Border.all(color: Colors.white38), ), width: 50.0, height: 50.0, child: InkWell( onTapDown: (details) => advancedPlayer.setPlaybackRate(2), onTapUp: (details) => advancedPlayer.setPlaybackRate(1), child: const Center(child: Icon(Icons.fast_forward_rounded, color: Colors.white)), ), ), ], ) ], ), ], ), ); } }
步骤4:从音乐列表跳转播放页面
// 列表页面的跳转逻辑示例 GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => MusicPlayerPage(initialIndex: 0, musics: musicList), ), ); }, child: Text(musicList[0].title), )
方案二:修改导航逻辑(临时解决,不推荐)
如果不想重构页面,可以在切换曲目时用pushReplacement替换当前路由,避免路由栈无限叠加:
// 上一曲跳转逻辑 Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => Music0()), ); // 下一曲跳转逻辑 Navigator.pushReplacement( context, MaterialPageRoute(builder: (context) => Music2()), );
但这种方式存在明显缺陷:切换曲目时无法保留之前的播放状态,且每首歌仍需单独维护页面,代码冗余,长期维护成本高。
总结
优先选择方案一,将音乐数据与播放页面分离,用状态切换替代页面跳转,既解决路由栈混乱问题,又减少代码冗余,方便后续扩展循环播放、随机播放等功能。
内容的提问来源于stack exchange,提问作者Bella Lee
相关产品推荐
相关产品推荐

