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 模型设计要点

  1. 不可变性优先:使用 final 修饰可选字段,保证数据的稳定性
  2. 工厂构造函数:使用 factory 解析 JSON 数据,便于网络数据转换
  3. 默认值处理:为每个字段提供合理的默认值,避免空指针异常

四、网络服务层实现

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 网络服务设计要点

  1. 单例模式:确保全局只有一个服务实例,节省资源
  2. 错误处理:每个方法都包含 try-catch,保证应用不会因网络错误崩溃
  3. 降级策略:网络请求失败时返回模拟数据,保证功能可用性
  4. 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 状态管理设计要点

  1. ChangeNotifier:使用 Flutter 官方推荐的状态管理模式
  2. Getter 封装:私有化状态字段,通过 Getter 方法访问
  3. 自动管理:播放进度自动更新,播放完成自动切换
  4. 播放模式:支持列表循环、单曲循环、随机播放三种模式

六、页面实现

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 运行说明

  1. 环境准备:确保已安装 Flutter for OpenHarmony SDK
  2. 项目配置:在 pubspec.yaml 中添加依赖
  3. 运行命令
    flutter run -d <设备ID>
    
  4. 截图保存:运行后可通过设备截图功能保存运行界面

九、代码托管

本文涉及的完整代码已托管至 AtomGit 平台:

仓库地址:https://atomgit.com/maaath/flutter_music_player

仓库包含以下内容:

  • 完整的 Flutter 项目源码
  • 项目配置文件
  • README 使用说明
  • 截图资源文件夹

十、总结

本文通过实战项目,详细讲解了如何使用 Flutter for OpenHarmony 构建音乐播放器应用。主要知识点包括:

  1. 数据模型设计:使用 Dart 类封装数据,支持 JSON 序列化
  2. 网络请求:使用适配后的 dio 库进行网络请求
  3. 状态管理:使用 ChangeNotifier 管理播放器状态
  4. UI 组件:自定义底部选项卡、迷你播放器、全屏播放器等组件
  5. 交互实现:下拉刷新、上拉加载、进度拖拽等功能

通过本文的学习,读者可以掌握 Flutter 在鸿蒙设备上的开发流程,为后续更复杂的应用开发打下坚实基础。


十一、参考资料

  • Flutter 官方文档:https://docs.flutter.dev
  • OpenHarmony 开发者文档:https://developer.harmonyos.com
  • Flutter for OpenHarmony 适配指南

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐