【maaath】Flutter for OpenHarmony 音乐播放器应用实战开发
随着 OpenHarmony 生态的蓬勃发展,Flutter 作为跨平台开发框架也在积极拥抱鸿蒙生态。本文将带领读者使用 Flutter for OpenHarmony 构建一个功能完善的音乐播放器应用,涵盖网络请求、列表展示、下拉刷新、底部选项卡以及播放进度动画等核心功能。通过本文的学习,读者将掌握 Flutter 在鸿蒙设备上的实战开发技巧。网络请求获取音乐列表数据音乐列表的展示与交互下拉刷新
Flutter for OpenHarmony 音乐播放器应用实战开发
社区引导
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
作者:maaath
前言
随着 OpenHarmony 生态的蓬勃发展,Flutter 作为跨平台开发框架也在积极拥抱鸿蒙生态。本文将带领读者使用 Flutter for OpenHarmony 构建一个功能完善的音乐播放器应用,涵盖网络请求、列表展示、下拉刷新、底部选项卡以及播放进度动画等核心功能。通过本文的学习,读者将掌握 Flutter 在鸿蒙设备上的实战开发技巧。
一、项目概述
1.1 项目目标
本次实战项目旨在使用 Flutter for OpenHarmony 构建一个完整的音乐播放器应用,主要实现以下功能:
- 网络请求获取音乐列表数据
- 音乐列表的展示与交互
- 下拉刷新与上拉加载更多
- 底部选项卡切换(推荐/歌单/排行榜/我的)
- 音乐播放进度动画与控制
1.2 技术栈
- 框架:Flutter for OpenHarmony
- 语言:Dart
- 网络库:dio(需进行鸿蒙化适配)
- 状态管理:Riverpod / Provider(可选)
- 目标平台:OpenHarmony
二、项目结构
良好的项目结构是保证代码可维护性的基础。本项目的目录结构如下:
lib/
├── main.dart # 应用入口
├── model/
│ └── music_model.dart # 音乐数据模型
├── service/
│ └── music_service.dart # 音乐网络服务
├── provider/
│ └── player_provider.dart # 播放器状态管理
├── pages/
│ ├── home_page.dart # 主页(底部选项卡)
│ ├── recommend_page.dart # 推荐页面
│ ├── playlist_page.dart # 歌单页面
│ ├── rank_page.dart # 排行榜页面
│ ├── mine_page.dart # 我的页面
│ └── player_page.dart # 播放器页面
└── widgets/
├── music_list_item.dart # 音乐列表项组件
├── mini_player.dart # 迷你播放器组件
└── progress_slider.dart # 进度滑块组件
三、数据模型定义
首先,我们需要定义音乐相关的数据模型,包括音乐信息、歌单信息和排行榜信息等。
// lib/model/music_model.dart
/// 音乐数据模型
class MusicModel {
final String id;
final String title;
final String artist;
final String album;
final int duration;
final String coverUrl;
final String audioUrl;
final String category;
final int playCount;
bool isFavorite;
bool isPlaying;
MusicModel({
required this.id,
required this.title,
required this.artist,
required this.album,
required this.duration,
required this.coverUrl,
required this.audioUrl,
required this.category,
required this.playCount,
this.isFavorite = false,
this.isPlaying = false,
});
factory MusicModel.fromJson(Map<String, dynamic> json) {
return MusicModel(
id: json['id']?.toString() ?? '',
title: json['title'] ?? '未知歌曲',
artist: json['artist'] ?? '未知歌手',
album: json['album'] ?? '',
duration: json['duration'] ?? 0,
coverUrl: json['coverUrl'] ?? '',
audioUrl: json['audioUrl'] ?? '',
category: json['category'] ?? '推荐',
playCount: json['playCount'] ?? 0,
isFavorite: json['isFavorite'] ?? false,
isPlaying: json['isPlaying'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'artist': artist,
'album': album,
'duration': duration,
'coverUrl': coverUrl,
'audioUrl': audioUrl,
'category': category,
'playCount': playCount,
'isFavorite': isFavorite,
'isPlaying': isPlaying,
};
}
}
/// 歌单数据模型
class PlaylistModel {
final String id;
final String name;
final String coverUrl;
final String description;
final int musicCount;
final int playCount;
final String creator;
final List<MusicModel> musicList;
PlaylistModel({
required this.id,
required this.name,
required this.coverUrl,
required this.description,
required this.musicCount,
required this.playCount,
required this.creator,
required this.musicList,
});
factory PlaylistModel.fromJson(Map<String, dynamic> json) {
final musicListJson = json['musicList'] as List<dynamic>? ?? [];
return PlaylistModel(
id: json['id']?.toString() ?? '',
name: json['name'] ?? '未知歌单',
coverUrl: json['coverUrl'] ?? '',
description: json['description'] ?? '',
musicCount: json['musicCount'] ?? 0,
playCount: json['playCount'] ?? 0,
creator: json['creator'] ?? '未知',
musicList: musicListJson.map((e) => MusicModel.fromJson(e)).toList(),
);
}
}
/// 排行榜数据模型
class RankModel {
final String id;
final String name;
final String coverUrl;
final String updateTime;
final List<MusicModel> musicList;
RankModel({
required this.id,
required this.name,
required this.coverUrl,
required this.updateTime,
required this.musicList,
});
factory RankModel.fromJson(Map<String, dynamic> json) {
final musicListJson = json['musicList'] as List<dynamic>? ?? [];
return RankModel(
id: json['id']?.toString() ?? '',
name: json['name'] ?? '未知榜单',
coverUrl: json['coverUrl'] ?? '',
updateTime: json['updateTime'] ?? '',
musicList: musicListJson.map((e) => MusicModel.fromJson(e)).toList(),
);
}
}
3.1 模型设计要点
- 不可变性优先:使用
final修饰可选字段,保证数据的稳定性 - 工厂构造函数:使用
factory解析 JSON 数据,便于网络数据转换 - 默认值处理:为每个字段提供合理的默认值,避免空指针异常
四、网络服务层实现
4.1 dio 鸿蒙化适配
Flutter for OpenHarmony 中的网络请求需要使用适配后的 dio 库。以下是网络服务的实现:
// lib/service/music_service.dart
import 'package:dio/dio.dart';
import '../model/music_model.dart';
/// 音乐网络服务
class MusicService {
static final MusicService _instance = MusicService._internal();
factory MusicService() => _instance;
MusicService._internal();
late final Dio _dio;
final int _pageSize = 20;
void init() {
_dio = Dio(BaseOptions(
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
},
));
// 添加日志拦截器(生产环境可移除)
_dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
));
}
/// 获取推荐歌曲列表
Future<List<MusicModel>> getRecommendList(int page) async {
try {
final response = await _dio.get(
'https://api.example.com/music/recommend',
queryParameters: {'page': page, 'size': _pageSize},
);
if (response.statusCode == 200) {
final data = response.data['data'] as List<dynamic>? ?? [];
return data.map((e) => MusicModel.fromJson(e)).toList();
}
} catch (e) {
print('获取推荐列表失败: $e');
}
return _getMockRecommendList(page);
}
/// 获取歌单列表
Future<List<PlaylistModel>> getPlaylistList(int page) async {
try {
final response = await _dio.get(
'https://api.example.com/playlist/list',
queryParameters: {'page': page, 'size': _pageSize},
);
if (response.statusCode == 200) {
final data = response.data['data'] as List<dynamic>? ?? [];
return data.map((e) => PlaylistModel.fromJson(e)).toList();
}
} catch (e) {
print('获取歌单列表失败: $e');
}
return _getMockPlaylistList();
}
/// 获取排行榜列表
Future<List<RankModel>> getRankList() async {
try {
final response = await _dio.get(
'https://api.example.com/rank/list',
);
if (response.statusCode == 200) {
final data = response.data['data'] as List<dynamic>? ?? [];
return data.map((e) => RankModel.fromJson(e)).toList();
}
} catch (e) {
print('获取排行榜列表失败: $e');
}
return _getMockRankList();
}
/// 搜索音乐
Future<List<MusicModel>> searchMusic(String keyword) async {
try {
final response = await _dio.get(
'https://api.example.com/music/search',
queryParameters: {'keyword': keyword},
);
if (response.statusCode == 200) {
final data = response.data['data'] as List<dynamic>? ?? [];
return data.map((e) => MusicModel.fromJson(e)).toList();
}
} catch (e) {
print('搜索失败: $e');
}
return _getMockSearchResult(keyword);
}
/// 格式化播放量
String formatPlayCount(int count) {
if (count >= 100000000) {
return '${(count / 100000000).toStringAsFixed(1)}亿';
} else if (count >= 10000) {
return '${(count / 10000).toStringAsFixed(1)}万';
}
return count.toString();
}
/// 格式化时长
String formatDuration(int seconds) {
final minutes = seconds ~/ 60;
final secs = seconds % 60;
return '$minutes:${secs.toString().padLeft(2, '0')}';
}
// ==================== 模拟数据 ====================
List<MusicModel> _getMockRecommendList(int page) {
final baseCount = (page - 1) * _pageSize;
final titles = ['晴天', '稻香', '七里香', '夜曲', '青花瓷', '告白气球', '等你下课', '说好不哭'];
final artists = ['周杰伦', '周杰伦', '周杰伦', '周杰伦', '周杰伦', '周杰伦', '周杰伦', '周杰伦/阿信'];
final albums = ['叶惠美', '魔杰座', '七里香', '十一月的肖邦', '我很忙', '周杰伦的床边故事', '等你下课', '说好不哭'];
final durations = [269, 218, 299, 240, 219, 225, 231, 225];
return List.generate(_pageSize, (i) {
final index = (baseCount + i) % titles.length;
return MusicModel(
id: 'recommend_${page}_$i',
title: titles[index],
artist: artists[index],
album: albums[index],
duration: durations[index],
coverUrl: 'https://picsum.photos/seed/${titles[index]}$i/400/400',
audioUrl: '',
category: '推荐',
playCount: (1000000 + page * 10000 + i * 1000),
);
});
}
List<PlaylistModel> _getMockPlaylistList() {
final names = ['华语经典', '欧美热歌', '日韩潮流', '深夜电台', '运动健身', '轻音乐集'];
final descriptions = [
'回味那些年我们一起追过的华语金曲',
'全球最流行的英文歌曲合集',
'日韩最新流行音乐一网打尽',
'在深夜里给你最温柔的陪伴',
'燃烧你的卡路里,跟着节奏动起来',
'放松身心,享受宁静时光',
];
return List.generate(names.length, (i) {
return PlaylistModel(
id: 'playlist_$i',
name: names[i],
coverUrl: 'https://picsum.photos/seed/playlist$i/400/400',
description: descriptions[i],
musicCount: 30 + i * 10,
playCount: 500000 + i * 100000,
creator: '音乐官方',
musicList: _getMockRecommendList(1).take(5).toList(),
);
});
}
List<RankModel> _getMockRankList() {
final names = ['热歌榜', '新歌榜', '飙升榜', '原创榜', '歌单推荐榜', 'MV榜'];
final updateTimes = ['每周四更新', '每日更新', '实时更新', '每周五更新', '每周一更新', '每周三更新'];
return List.generate(names.length, (i) {
return RankModel(
id: 'rank_$i',
name: names[i],
coverUrl: 'https://picsum.photos/seed/rank$i/400/400',
updateTime: updateTimes[i],
musicList: _getMockRecommendList(1).take(10).toList(),
);
});
}
List<MusicModel> _getMockSearchResult(String keyword) {
return _getMockRecommendList(1)
.where((m) => m.title.contains(keyword) || m.artist.contains(keyword))
.toList();
}
}
// 全局服务实例
final musicService = MusicService();
4.2 网络服务设计要点
- 单例模式:确保全局只有一个服务实例,节省资源
- 错误处理:每个方法都包含 try-catch,保证应用不会因网络错误崩溃
- 降级策略:网络请求失败时返回模拟数据,保证功能可用性
- Dio 配置:统一配置超时时间和请求头,便于维护
五、播放器状态管理
播放器是音乐应用的核心组件,需要管理播放状态、进度、播放列表等数据。
// lib/provider/player_provider.dart
import 'package:flutter/foundation.dart';
import '../model/music_model.dart';
/// 播放状态枚举
enum PlayState { playing, paused, loading, error }
/// 播放模式枚举
enum PlayMode { listLoop, singleLoop, shuffle }
/// 播放器状态管理
class PlayerProvider extends ChangeNotifier {
MusicModel? _currentMusic;
PlayState _playState = PlayState.paused;
PlayMode _playMode = PlayMode.listLoop;
int _currentTime = 0;
int _totalTime = 0;
double _progress = 0.0;
List<MusicModel> _playlist = [];
int _currentIndex = 0;
// Getter 方法
MusicModel? get currentMusic => _currentMusic;
PlayState get playState => _playState;
PlayMode get playMode => _playMode;
int get currentTime => _currentTime;
int get totalTime => _totalTime;
double get progress => _progress;
List<MusicModel> get playlist => _playlist;
int get currentIndex => _currentIndex;
/// 播放音乐
Future<void> play(MusicModel music) async {
_currentMusic = music;
_totalTime = music.duration;
_playState = PlayState.loading;
notifyListeners();
// 模拟播放(实际项目中需要调用原生播放器)
await Future.delayed(const Duration(milliseconds: 500));
_playState = PlayState.playing;
_startProgressTimer();
notifyListeners();
}
/// 播放列表
Future<void> playList(List<MusicModel> list, {int startIndex = 0}) async {
_playlist = list;
_currentIndex = startIndex;
if (list.isNotEmpty) {
await play(list[startIndex]);
}
}
/// 暂停
void pause() {
_playState = PlayState.paused;
notifyListeners();
}
/// 继续播放
void resume() {
_playState = PlayState.playing;
notifyListeners();
}
/// 切换播放状态
void togglePlay() {
if (_playState == PlayState.playing) {
pause();
} else {
resume();
}
}
/// 上一曲
Future<void> previous() async {
if (_playlist.isEmpty) return;
switch (_playMode) {
case PlayMode.singleLoop:
await play(_playlist[_currentIndex]);
break;
case PlayMode.shuffle:
_currentIndex = DateTime.now().millisecond % _playlist.length;
await play(_playlist[_currentIndex]);
break;
default:
_currentIndex = _currentIndex > 0
? _currentIndex - 1
: _playlist.length - 1;
await play(_playlist[_currentIndex]);
}
}
/// 下一曲
Future<void> next() async {
if (_playlist.isEmpty) return;
switch (_playMode) {
case PlayMode.singleLoop:
await play(_playlist[_currentIndex]);
break;
case PlayMode.shuffle:
_currentIndex = DateTime.now().millisecond % _playlist.length;
await play(_playlist[_currentIndex]);
break;
default:
_currentIndex = (_currentIndex + 1) % _playlist.length;
await play(_playlist[_currentIndex]);
}
}
/// 跳转播放位置
void seekTo(int position) {
_currentTime = position;
if (_totalTime > 0) {
_progress = _currentTime / _totalTime;
}
notifyListeners();
}
/// 切换播放模式
void togglePlayMode() {
switch (_playMode) {
case PlayMode.listLoop:
_playMode = PlayMode.singleLoop;
break;
case PlayMode.singleLoop:
_playMode = PlayMode.shuffle;
break;
case PlayMode.shuffle:
_playMode = PlayMode.listLoop;
break;
}
notifyListeners();
}
/// 获取播放模式图标
String getPlayModeIcon() {
switch (_playMode) {
case PlayMode.singleLoop:
return '🔂';
case PlayMode.shuffle:
return '🔀';
default:
return '🔁';
}
}
/// 获取播放模式名称
String getPlayModeName() {
switch (_playMode) {
case PlayMode.singleLoop:
return '单曲循环';
case PlayMode.shuffle:
return '随机播放';
default:
return '列表循环';
}
}
/// 格式化时间
String formatTime(int seconds) {
final mins = seconds ~/ 60;
final secs = seconds % 60;
return '$mins:${secs.toString().padLeft(2, '0')}';
}
// 进度定时器
void _startProgressTimer() {
Future.doWhile(() async {
await Future.delayed(const Duration(seconds: 1));
if (_playState == PlayState.playing) {
_currentTime++;
if (_totalTime > 0) {
_progress = _currentTime / _totalTime;
}
notifyListeners();
// 自动播放下一首
if (_currentTime >= _totalTime && _totalTime > 0) {
await next();
return false;
}
return true;
}
return _playState == PlayState.playing;
});
}
}
5.1 状态管理设计要点
- ChangeNotifier:使用 Flutter 官方推荐的状态管理模式
- Getter 封装:私有化状态字段,通过 Getter 方法访问
- 自动管理:播放进度自动更新,播放完成自动切换
- 播放模式:支持列表循环、单曲循环、随机播放三种模式
六、页面实现
6.1 主页(底部选项卡)
// lib/pages/home_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../provider/player_provider.dart';
import 'recommend_page.dart';
import 'playlist_page.dart';
import 'rank_page.dart';
import 'mine_page.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _currentIndex = 0;
final List<Widget> _pages = const [
RecommendPage(),
PlaylistPage(),
RankPage(),
MinePage(),
];
final List<String> _tabTitles = ['推荐', '歌单', '排行榜', '我的'];
final List<String> _tabIcons = ['📻', '📋', '🏆', '👤'];
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF1A1A2E),
body: Stack(
children: [
// 页面内容
IndexedStack(
index: _currentIndex,
children: _pages,
),
// 迷你播放器
Consumer<PlayerProvider>(
builder: (context, player, child) {
if (player.currentMusic == null) {
return const SizedBox.shrink();
}
return Positioned(
left: 0,
right: 0,
bottom: 60,
child: _buildMiniPlayer(player),
);
},
),
// 底部导航栏
Positioned(
left: 0,
right: 0,
bottom: 0,
child: _buildBottomNavBar(),
),
],
),
);
}
Widget _buildMiniPlayer(PlayerProvider player) {
return GestureDetector(
onTap: () => _showFullPlayer(context),
child: Container(
height: 60,
decoration: const BoxDecoration(
color: Color(0xFF252538),
border: Border(
top: BorderSide(color: Colors.white10, width: 0.5),
),
),
child: Row(
children: [
// 封面
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
player.currentMusic?.coverUrl ?? '',
width: 46,
height: 46,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
width: 46,
height: 46,
color: Colors.grey[800],
child: const Icon(Icons.music_note, color: Colors.white54),
),
),
),
// 歌曲信息
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
player.currentMusic?.title ?? '未知歌曲',
style: const TextStyle(
color: Colors.white,
fontSize: 15,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
Text(
player.currentMusic?.artist ?? '未知歌手',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 12,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
// 播放控制按钮
IconButton(
icon: Icon(
player.playState == PlayState.playing
? Icons.pause
: Icons.play_arrow,
color: Colors.white,
size: 36,
),
onPressed: () => player.togglePlay(),
),
IconButton(
icon: const Icon(Icons.skip_next, color: Colors.white, size: 32),
onPressed: () => player.next(),
),
],
),
),
);
}
Widget _buildBottomNavBar() {
return Container(
height: 60,
decoration: const BoxDecoration(
color: Color(0xFF1E1E32),
border: Border(
top: BorderSide(color: Colors.white10, width: 0.5),
),
),
child: Row(
children: List.generate(_tabTitles.length, (index) {
final isSelected = _currentIndex == index;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _currentIndex = index),
behavior: HitTestBehavior.opaque,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_tabIcons[index],
style: const TextStyle(fontSize: 22),
),
const SizedBox(height: 4),
Text(
_tabTitles[index],
style: TextStyle(
fontSize: isSelected ? 13 : 12,
color: isSelected
? const Color(0xFFFF6B6B)
: Colors.white.withOpacity(0.5),
fontWeight:
isSelected ? FontWeight.bold : FontWeight.normal,
),
),
],
),
),
);
}),
),
);
}
void _showFullPlayer(BuildContext context) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const PlayerPage(),
),
);
}
}
6.2 推荐页面
// lib/pages/recommend_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../model/music_model.dart';
import '../service/music_service.dart';
import '../provider/player_provider.dart';
class RecommendPage extends StatefulWidget {
const RecommendPage({super.key});
State<RecommendPage> createState() => _RecommendPageState();
}
class _RecommendPageState extends State<RecommendPage> {
final List<MusicModel> _musicList = [];
bool _isLoading = false;
bool _isRefreshing = false;
bool _hasMore = true;
int _currentPage = 1;
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
if (_isLoading) return;
setState(() => _isLoading = true);
try {
final newMusic = await musicService.getRecommendList(_currentPage);
setState(() {
if (_currentPage == 1) {
_musicList.clear();
}
_musicList.addAll(newMusic);
_hasMore = newMusic.length >= 20;
});
} finally {
setState(() {
_isLoading = false;
_isRefreshing = false;
});
}
}
Future<void> _refresh() async {
_isRefreshing = true;
_currentPage = 1;
await _loadData();
}
void _loadMore() {
if (!_hasMore || _isLoading) return;
_currentPage++;
_loadData();
}
Widget build(BuildContext context) {
return Column(
children: [
// 标题栏
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Row(
children: [
const Text(
'推荐',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const Spacer(),
IconButton(
icon: const Icon(Icons.search, color: Colors.white, size: 26),
onPressed: () {
// TODO: 跳转到搜索页面
},
),
],
),
),
// 刷新指示器
if (_isRefreshing)
const Padding(
padding: EdgeInsets.all(8.0),
child: LinearProgressIndicator(
backgroundColor: Colors.white10,
valueColor: AlwaysStoppedAnimation(Color(0xFFFF6B6B)),
),
),
// 音乐列表
Expanded(
child: _buildMusicList(),
),
],
);
}
Widget _buildMusicList() {
if (_isLoading && _musicList.isEmpty) {
return const Center(
child: CircularProgressIndicator(
color: Color(0xFFFF6B6B),
),
);
}
return RefreshIndicator(
onRefresh: _refresh,
color: const Color(0xFFFF6B6B),
backgroundColor: const Color(0xFF252538),
child: ListView.builder(
itemCount: _musicList.length + 1,
itemBuilder: (context, index) {
if (index == _musicList.length) {
return _buildLoadMoreIndicator();
}
return _buildMusicItem(_musicList[index], index);
},
onReachEnd: _loadMore,
),
);
}
Widget _buildMusicItem(MusicModel music, int index) {
final player = context.read<PlayerProvider>();
return InkWell(
onTap: () => player.playList(_musicList, startIndex: index),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
// 序号
SizedBox(
width: 30,
child: Text(
'${index + 1}',
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.4),
),
textAlign: TextAlign.center,
),
),
// 封面
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
music.coverUrl,
width: 52,
height: 52,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
width: 52,
height: 52,
color: Colors.grey[800],
child: const Icon(Icons.music_note, color: Colors.white54),
),
),
),
// 歌曲信息
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
music.title,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
'${music.artist} · ${music.album}',
style: TextStyle(
color: Colors.white.withOpacity(0.5),
fontSize: 12,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
),
// 播放量
Text(
'▶ ${musicService.formatPlayCount(music.playCount)}',
style: TextStyle(
fontSize: 11,
color: Colors.white.withOpacity(0.4),
),
),
// 更多按钮
IconButton(
icon: Icon(
Icons.more_vert,
color: Colors.white.withOpacity(0.5),
size: 20,
),
onPressed: () {
// TODO: 显示更多操作菜单
},
),
],
),
),
);
}
Widget _buildLoadMoreIndicator() {
return Container(
height: 44,
alignment: Alignment.center,
child: _isLoading && _currentPage > 1
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFFFF6B6B),
),
)
: _hasMore
? Text(
'点击加载更多',
style: TextStyle(
fontSize: 13,
color: Colors.white.withOpacity(0.4),
),
)
: Text(
'— 已加载全部 —',
style: TextStyle(
fontSize: 13,
color: Colors.white.withOpacity(0.3),
),
),
);
}
}
6.3 播放器页面
// lib/pages/player_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../provider/player_provider.dart';
class PlayerPage extends StatelessWidget {
const PlayerPage({super.key});
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF1A1A2E),
body: Consumer<PlayerProvider>(
builder: (context, player, child) {
return Column(
children: [
// 顶部栏
_buildTopBar(context),
const Spacer(),
// 封面
_buildCover(player),
const Spacer(),
// 歌曲信息
_buildSongInfo(player),
// 进度条
_buildProgressBar(player),
// 播放控制
_buildControls(player),
const SizedBox(height: 40),
],
);
},
),
);
}
Widget _buildTopBar(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.keyboard_arrow_down,
color: Colors.white, size: 28),
onPressed: () => Navigator.of(context).pop(),
),
const Expanded(
child: Text(
'正在播放',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
IconButton(
icon: const Icon(Icons.more_vert, color: Colors.white, size: 24),
onPressed: () {},
),
],
),
);
}
Widget _buildCover(PlayerProvider player) {
return Stack(
alignment: Alignment.center,
children: [
// 封面
Container(
width: 280,
height: 280,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(140),
boxShadow: [
BoxShadow(
color: const Color(0xFFFF6B6B).withOpacity(0.3),
blurRadius: 30,
offset: const Offset(0, 10),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(140),
child: Image.network(
player.currentMusic?.coverUrl ?? '',
width: 280,
height: 280,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
color: Colors.grey[800],
child: const Icon(Icons.music_note,
size: 100, color: Colors.white54),
),
),
),
),
// 磁盘外圈
Container(
width: 300,
height: 300,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFF3A3A4E),
width: 8,
),
),
),
],
);
}
Widget _buildSongInfo(PlayerProvider player) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: Column(
children: [
Text(
player.currentMusic?.title ?? '未知歌曲',
style: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Text(
player.currentMusic?.artist ?? '未知歌手',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 15,
),
),
],
),
);
}
Widget _buildProgressBar(PlayerProvider player) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 30),
child: Column(
children: [
SliderTheme(
data: SliderThemeData(
activeTrackColor: const Color(0xFFFF6B6B),
inactiveTrackColor: Colors.white.withOpacity(0.2),
thumbColor: const Color(0xFFFF6B6B),
overlayColor: const Color(0xFFFF6B6B).withOpacity(0.2),
trackHeight: 4,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
),
child: Slider(
value: player.progress.clamp(0.0, 1.0),
onChanged: (value) {
final position = (value * player.totalTime).toInt();
player.seekTo(position);
},
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
player.formatTime(player.currentTime),
style: TextStyle(
color: Colors.white.withOpacity(0.5),
fontSize: 12,
),
),
Text(
player.formatTime(player.totalTime),
style: TextStyle(
color: Colors.white.withOpacity(0.5),
fontSize: 12,
),
),
],
),
),
],
),
);
}
Widget _buildControls(PlayerProvider player) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// 播放模式
GestureDetector(
onTap: () => player.togglePlayMode(),
child: Text(
player.getPlayModeIcon(),
style: const TextStyle(fontSize: 24),
),
),
// 上一曲
IconButton(
icon: const Icon(Icons.skip_previous, color: Colors.white, size: 40),
onPressed: () => player.previous(),
),
// 播放/暂停按钮
GestureDetector(
onTap: () => player.togglePlay(),
child: Container(
width: 70,
height: 70,
decoration: const BoxDecoration(
color: Color(0xFFFF6B6B),
shape: BoxShape.circle,
),
child: player.playState == PlayState.loading
? const Center(
child: SizedBox(
width: 30,
height: 30,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
),
)
: Icon(
player.playState == PlayState.playing
? Icons.pause
: Icons.play_arrow,
color: Colors.white,
size: 40,
),
),
),
// 下一曲
IconButton(
icon: const Icon(Icons.skip_next, color: Colors.white, size: 40),
onPressed: () => player.next(),
),
// 收藏
GestureDetector(
onTap: () {},
child: Text(
'❤',
style: TextStyle(
fontSize: 24,
color: player.currentMusic?.isFavorite == true
? const Color(0xFFFF6B6B)
: Colors.white,
),
),
),
],
),
);
}
}
七、应用入口
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'service/music_service.dart';
import 'provider/player_provider.dart';
import 'pages/home_page.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
// 初始化服务
musicService.init();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => PlayerProvider()),
],
child: MaterialApp(
title: '音乐播放器',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
primaryColor: const Color(0xFFFF6B6B),
scaffoldBackgroundColor: const Color(0xFF1A1A2E),
colorScheme: const ColorScheme.dark(
primary: Color(0xFFFF6B6B),
),
),
home: const HomePage(),
),
);
}
}
八、截图运行板块
8.1 应用启动页面
应用启动后,首先显示启动页面,包含旋转唱片动画和加载指示器:
8.2 推荐页面
推荐页面展示热门歌曲列表,支持下拉刷新和上拉加载:

8.3 歌单页面
歌单页面展示各类精选歌单:

8.4 排行榜页面
排行榜页面展示各榜单的热门歌曲:
8.6 运行说明
- 环境准备:确保已安装 Flutter for OpenHarmony SDK
- 项目配置:在
pubspec.yaml中添加依赖 - 运行命令:
flutter run -d <设备ID> - 截图保存:运行后可通过设备截图功能保存运行界面
九、代码托管
本文涉及的完整代码已托管至 AtomGit 平台:
仓库地址:https://atomgit.com/maaath/flutter_music_player
仓库包含以下内容:
- 完整的 Flutter 项目源码
- 项目配置文件
- README 使用说明
- 截图资源文件夹
十、总结
本文通过实战项目,详细讲解了如何使用 Flutter for OpenHarmony 构建音乐播放器应用。主要知识点包括:
- 数据模型设计:使用 Dart 类封装数据,支持 JSON 序列化
- 网络请求:使用适配后的 dio 库进行网络请求
- 状态管理:使用 ChangeNotifier 管理播放器状态
- UI 组件:自定义底部选项卡、迷你播放器、全屏播放器等组件
- 交互实现:下拉刷新、上拉加载、进度拖拽等功能
通过本文的学习,读者可以掌握 Flutter 在鸿蒙设备上的开发流程,为后续更复杂的应用开发打下坚实基础。
十一、参考资料
- Flutter 官方文档:https://docs.flutter.dev
- OpenHarmony 开发者文档:https://developer.harmonyos.com
- Flutter for OpenHarmony 适配指南
更多推荐


所有评论(0)