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

Flutter ListView.Builder音频播放实现:求支持空安全的插件及示例代码

Flutter 网络音频播放适配方案

推荐插件:audioplayers

该插件是目前Flutter生态中活跃度最高的音频播放组件之一,完全满足需求:

  • 支持空安全
  • 适配全平台(Android、iOS、Web、桌面端)
  • 支持网络音频、本地音频、Assets音频播放
  • 自带播放状态、进度、完成、错误等全场景监听回调
  • 支持后台播放、音量调节、播放速度控制等扩展能力

集成步骤

1. 添加依赖

在项目pubspec.yaml文件的依赖区块添加配置:

dependencies:
  flutter:
    sdk: flutter
  audioplayers: ^4.0.1 # 可替换为当前最新稳定版本

添加完成后执行flutter pub get完成依赖安装。

2. 平台权限配置

如果仅播放HTTPS协议的网络音频,可跳过iOS配置,Android必须配置网络权限:

  • Android端:在android/app/src/main/AndroidManifest.xml的<manifest>标签下添加权限声明:
    <uses-permission android:name="android.permission.INTERNET" />
    
  • iOS端:如果需要播放HTTP协议的音频,在ios/Runner/Info.plist的<dict>标签下添加配置:
    <key>NSAppTransportSecurity</key>
    <dict>
      <key>NSAllowsArbitraryLoads</key>
      <true/>
    </dict>
    

适配歌曲列表场景的完整示例代码

以下代码已经和ListView.Builder的歌曲列表逻辑打通,自带播放控制、进度展示、状态切换能力:

import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/material.dart';

// 自定义歌曲数据模型,可根据业务调整
class Song {
  final String name;
  final String networkUrl;
  final Duration totalDuration;

  Song({
    required this.name,
    required this.networkUrl,
    required this.totalDuration,
  });
}

class SongPlayPage extends StatefulWidget {
  const SongPlayPage({super.key});

  @override
  State<SongPlayPage> createState() => _SongPlayPageState();
}

class _SongPlayPageState extends State<SongPlayPage> {
  final AudioPlayer _audioPlayer = AudioPlayer();
  // 记录当前正在播放的歌曲下标,为null表示没有正在播放的音频
  int? _currentPlayingIndex;
  // 记录当前播放进度
  Duration _currentPlayPosition = Duration.zero;

  // 示例歌曲列表,替换为实际业务数据即可
  final List<Song> _songList = [
    Song(
      name: "测试歌曲1",
      networkUrl: "替换为你的音频网络地址",
      totalDuration: const Duration(minutes: 3, seconds: 18),
    ),
    Song(
      name: "测试歌曲2",
      networkUrl: "替换为你的音频网络地址",
      totalDuration: const Duration(minutes: 4, seconds: 02),
    ),
  ];

  @override
  void initState() {
    super.initState();
    // 监听播放进度更新
    _audioPlayer.onPositionChanged.listen((position) {
      setState(() {
        _currentPlayPosition = position;
      });
    });
    // 监听播放完成事件,自动重置状态
    _audioPlayer.onPlayerComplete.listen((_) {
      setState(() {
        _currentPlayingIndex = null;
        _currentPlayPosition = Duration.zero;
      });
    });
  }

  // 播放/暂停控制逻辑
  Future<void> _togglePlay(int songIndex) async {
    if (_currentPlayingIndex == songIndex) {
      // 点击的是当前正在播放的歌曲,切换播放/暂停状态
      if (_audioPlayer.state == PlayerState.playing) {
        await _audioPlayer.pause();
      } else {
        await _audioPlayer.resume();
      }
    } else {
      // 点击的是新歌曲,停止之前的播放后加载新音频播放
      await _audioPlayer.stop();
      await _audioPlayer.play(UrlSource(_songList[songIndex].networkUrl));
      _currentPlayingIndex = songIndex;
      _currentPlayPosition = Duration.zero;
    }
    setState(() {});
  }

  // 格式化时长为mm:ss格式
  String _formatTime(Duration duration) {
    String twoDigits(int num) => num.toString().padLeft(2, '0');
    final minute = twoDigits(duration.inMinutes.remainder(60));
    final second = twoDigits(duration.inSeconds.remainder(60));
    return "$minute:$second";
  }

  @override
  void dispose() {
    // 页面销毁时释放音频资源
    _audioPlayer.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("歌曲列表")),
      body: ListView.builder(
        itemCount: _songList.length,
        itemBuilder: (context, index) {
          final currentSong = _songList[index];
          final isCurrentPlaying = _currentPlayingIndex == index;
          // 计算进度条进度
          final playProgress = isCurrentPlaying && currentSong.totalDuration.inSeconds > 0
              ? _currentPlayPosition.inSeconds / currentSong.totalDuration.inSeconds
              : 0.0;

          return ListTile(
            leading: IconButton(
              icon: Icon(
                isCurrentPlaying && _audioPlayer.state == PlayerState.playing
                    ? Icons.pause_circle
                    : Icons.play_circle,
                size: 32,
              ),
              onPressed: () => _togglePlay(index),
            ),
            title: Text(currentSong.name),
            subtitle: Padding(
              padding: const EdgeInsets.only(top: 8),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  LinearProgressIndicator(
                    value: playProgress,
                    minHeight: 4,
                  ),
                  const SizedBox(height: 4),
                  Text(
                    "${_formatTime(isCurrentPlaying ? _currentPlayPosition : Duration.zero)} / ${_formatTime(currentSong.totalDuration)}",
                    style: const TextStyle(fontSize: 12, color: Colors.grey),
                  ),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

注意事项

  • 示例中的音频网络地址需要替换为有效音频链接,优先使用HTTPS协议地址减少适配成本
  • 若出现播放失败的情况,可通过监听_audioPlayer.onPlayerError回调获取具体错误信息排查问题
  • 如果需要播放本地Assets目录下的音频,只需要将UrlSource替换为AssetSource,传入assets路径即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.28 10:15:05