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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 03:15:41