欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

一款功能完整的本地音乐播放器应用,基于 Flutter 框架开发,支持 HarmonyOS 平台。

创建项目

1. 创建 Flutter 项目

# 创建新项目
flutter create -t app text3
cd text3

2. 添加 HarmonyOS 支持

# 在项目中添加 HarmonyOS 平台
flutter build app --release

—## 项目结构

创建以下目录结构:

text3/
├── lib/
│   ├── main.dart
│   ├── models/
│   │   └── song_model.dart
│   ├── providers/
│   │   ├── audio_provider.dart
│   │   └── playlist_provider.dart
│   ├── screens/
│   │   ├── home_screen.dart
│   │   ├── player_screen.dart
│   │   └── playlist_screen.dart
│   ├── services/
│   │   ├── audio_service.dart
│   │   ├── database_helper.dart
│   │   └── media_scanner_service.dart
│   └── widgets/
│       ├── mini_player_bar.dart
│       ├── seek_slider.dart
│       └── circular_album_art.dart
├── assets/
│   ├── icons/
│   └── animations/
├── ohos/
│   └── entry/
│       └── src/
│           └── main/
│               ├── module.json5
│               └── ets/
└── pubspec.yaml

配置文件

1. pubspec.yaml

name: text3
description: Flutter HarmonyOS 音乐播放器
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: '>=3.5.0 <4.0.0'

dependencies:
  flutter:
    sdk: flutter
  
  # UI 组件
  cupertino_icons: ^1.0.8
  provider: ^6.1.2
  
  # 音频播放
  audioplayers: ^6.1.0
  
  # 本地音乐查询
  on_audio_query: ^2.9.0
  
  # 数据库
  sqflite: ^2.4.1
  path_provider: ^2.1.4
  path: ^1.9.0
  
  # 权限处理
  permission_handler: ^11.4.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^4.0.0

flutter:
  uses-material-design: true
  
  assets:
    - assets/
    - assets/icons/
    - assets/animations/

2. 安装依赖

flutter pub get

核心代码实现

1. 数据模型 - lib/models/song_model.dart

import 'package:on_audio_query/on_audio_query.dart';

/// 歌曲数据模型
class Song {
  final int id;
  final String title;
  final String artist;
  final String album;
  final String data;
  final int duration;
  final int albumId;
  bool isFavorite;

  Song({
    required this.id,
    required this.title,
    required this.artist,
    required this.album,
    required this.data,
    required this.duration,
    required this.albumId,
    this.isFavorite = false,
  });

  /// 从 AudioQuery 结果创建 Song 对象
  factory Song.fromAudioQuery(dynamic audio) {
    return Song(
      id: audio.id ?? 0,
      title: audio.displayName ?? '未知标题',
      artist: audio.artist ?? '未知艺术家',
      album: audio.album ?? '未知专辑',
      data: audio.data ?? '',
      duration: audio.duration ?? 0,
      albumId: audio.albumId ?? 0,
      isFavorite: false,
    );
  }

  /// 转换为 Map
  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'title': title,
      'artist': artist,
      'album': album,
      'data': data,
      'duration': duration,
      'albumId': albumId,
      'isFavorite': isFavorite ? 1 : 0,
    };
  }

  /// 从 Map 创建 Song
  factory Song.fromMap(Map<String, dynamic> map) {
    return Song(
      id: map['id'] ?? 0,
      title: map['title'] ?? '',
      artist: map['artist'] ?? '',
      album: map['album'] ?? '',
      data: map['data'] ?? '',
      duration: map['duration'] ?? 0,
      albumId: map['albumId'] ?? 0,
      isFavorite: (map['isFavorite'] ?? 0) == 1,
    );
  }
}

2. 音频播放服务 - lib/services/audio_service.dart

import 'package:audioplayers/audioplayers.dart';
import 'dart:async';
import '../models/song_model.dart';

/// 播放模式枚举
enum PlayMode {
  linear,    // 顺序播放
  loop,      // 循环播放
  shuffle,   // 随机播放
  single,    // 单曲循环
}

/// 音频播放服务(单例模式)
class AudioPlayerService {
  static final AudioPlayerService _instance = AudioPlayerService._internal();
  factory AudioPlayerService() => _instance;
  AudioPlayerService._internal();

  final AudioPlayer _audioPlayer = AudioPlayer();
  
  PlayMode _playMode = PlayMode.linear;
  List<Song> _playQueue = [];
  int _currentIndex = -1;
  bool _isPlaying = false;

  // 状态流
  final _stateController = StreamController<bool>.broadcast();
  Stream<bool> get stateStream => _stateController.stream;
  
  final _positionController = StreamController<Duration>.broadcast();
  Stream<Duration> get positionStream => _positionController.stream;
  
  final _durationController = StreamController<Duration>.broadcast();
  Stream<Duration> get durationStream => _durationController.stream;

  // Getter
  Song? get currentSong => _currentIndex >= 0 && _currentIndex < _playQueue.length 
      ? _playQueue[_currentIndex] 
      : null;
  bool get isPlaying => _isPlaying;
  List<Song> get playQueue => _playQueue;
  int get currentIndex => _currentIndex;

  /// 初始化播放器监听
  void init() {
    // 监听播放状态
    _audioPlayer.onPlayerStateChanged.listen((state) {
      _isPlaying = state == PlayerState.playing;
      _stateController.add(_isPlaying);
    });

    // 监听播放位置
    _audioPlayer.onPositionChanged.listen((position) {
      _positionController.add(position);
    });

    // 监听总时长
    _audioPlayer.onDurationChanged.listen((duration) {
      _durationController.add(duration);
    });

    // 监听播放完成
    _audioPlayer.onPlayerComplete.listen((event) {
      _handlePlayComplete();
    });
  }

  /// 播放完成处理
  void _handlePlayComplete() {
    switch (_playMode) {
      case PlayMode.linear:
        playNext();
        break;
      case PlayMode.loop:
        playNext();
        break;
      case PlayMode.shuffle:
        playNext();
        break;
      case PlayMode.single:
        playCurrent();
        break;
    }
  }

  /// 播放歌曲
  Future<void> playSong(Song song) async {
    try {
      final index = _playQueue.indexWhere((s) => s.id == song.id);
      if (index != -1) {
        _currentIndex = index;
      } else {
        _playQueue.add(song);
        _currentIndex = _playQueue.length - 1;
      }
      await _playCurrent();
    } catch (e) {
      print('播放失败:$e');
    }
  }

  /// 播放当前歌曲
  Future<void> _playCurrent() async {
    if (_currentIndex < 0 || _currentIndex >= _playQueue.length) return;
    
    final song = _playQueue[_currentIndex];
    try {
      await _audioPlayer.play(DeviceFileSource(song.data));
      _isPlaying = true;
      _stateController.add(_isPlaying);
    } catch (e) {
      print('播放当前歌曲失败:$e');
    }
  }

  /// 播放/暂停
  Future<void> playPause() async {
    if (_isPlaying) {
      await _audioPlayer.pause();
      _isPlaying = false;
    } else {
      await _audioPlayer.resume();
      _isPlaying = true;
    }
    _stateController.add(_isPlaying);
  }

  /// 播放上一首
  Future<void> playPrevious() async {
    if (_playQueue.isEmpty) return;

    if (_currentIndex > 0) {
      _currentIndex--;
      await _playCurrent();
    } else if (_playMode == PlayMode.loop) {
      _currentIndex = _playQueue.length - 1;
      await _playCurrent();
    }
  }

  /// 播放下一首
  Future<void> playNext() async {
    if (_playQueue.isEmpty) return;

    switch (_playMode) {
      case PlayMode.shuffle:
        _shuffleQueue();
        await _playCurrent();
        break;
      case PlayMode.linear:
      case PlayMode.loop:
        if (_currentIndex < _playQueue.length - 1) {
          _currentIndex++;
          await _playCurrent();
        } else if (_playMode == PlayMode.loop) {
          _currentIndex = 0;
          await _playCurrent();
        }
        break;
      case PlayMode.single:
        await _playCurrent();
        break;
    }
  }

  /// 随机打乱队列
  void _shuffleQueue() {
    if (_playQueue.isEmpty) return;
    _playQueue.shuffle();
  }

  /// 跳转到指定位置
  Future<void> seekTo(Duration position) async {
    await _audioPlayer.seek(position);
  }

  /// 设置播放进度(别名)
  Future<void> seek(Duration position) async {
    await _audioPlayer.seek(position);
  }

  /// 设置播放模式
  void setPlayMode(PlayMode mode) {
    _playMode = mode;

    switch (mode) {
      case PlayMode.loop:
        _audioPlayer.setReleaseMode(ReleaseMode.loop);
        break;
      default:
        _audioPlayer.setReleaseMode(ReleaseMode.release);
        break;
    }
  }

  /// 设置音量 (0.0 - 1.0)
  Future<void> setVolume(double volume) async {
    await _audioPlayer.setVolume(volume.clamp(0.0, 1.0));
  }

  /// 切换循环模式
  void toggleLoop() {
    if (_playMode == PlayMode.loop) {
      setPlayMode(PlayMode.linear);
    } else {
      setPlayMode(PlayMode.loop);
    }
  }

  /// 切换随机播放
  void toggleShuffle() {
    if (_playMode == PlayMode.shuffle) {
      setPlayMode(PlayMode.linear);
    } else {
      setPlayMode(PlayMode.shuffle);
    }
  }

  /// 停止播放
  Future<void> stop() async {
    await _audioPlayer.stop();
    _isPlaying = false;
    _stateController.add(_isPlaying);
  }

  /// 释放资源
  void dispose() {
    _audioPlayer.dispose();
    _stateController.close();
    _positionController.close();
    _durationController.close();
  }
}

3. 媒体扫描服务 - lib/services/media_scanner_service.dart

import 'package:on_audio_query/on_audio_query.dart';
import '../models/song_model.dart';

/// 媒体扫描服务
class MediaScannerService {
  final OnAudioQuery _audioQuery = OnAudioQuery();

  /// 扫描所有歌曲
  Future<List<Song>> scanAllSongs() async {
    try {
      final songs = await _audioQuery.querySongs(
        sortType: SongSortType.DATE_ADDED,
        orderType: OrderType.DESC_OR_GREATER,
        uriType: UriType.EXTERNAL,
      );
      
      return songs.map((song) => Song.fromAudioQuery(song)).toList();
    } catch (e) {
      print('扫描歌曲失败:$e');
      return [];
    }
  }

  /// 获取所有专辑
  Future<List<dynamic>> getAllAlbums() async {
    try {
      return await _audioQuery.queryAlbums(
        sortType: AlbumSortType.ALBUM,
        orderType: OrderType.ASC_OR_SMALLER,
      );
    } catch (e) {
      print('获取专辑失败:$e');
      return [];
    }
  }

  /// 获取专辑封面
  Future<List<int>?> getAlbumArt(int albumId) async {
    try {
      return await _audioQuery.queryArtwork(
        albumId,
        ArtworkType.ALBUM,
        size: 300,
      );
    } catch (e) {
      return null;
    }
  }
}

4. 数据库帮助类 - lib/services/database_helper.dart

import 'package:sqflite/sqflite.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart';

class DatabaseHelper {
  static final DatabaseHelper _instance = DatabaseHelper._internal();
  factory DatabaseHelper() => _instance;
  DatabaseHelper._internal();

  static Database? _database;

  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDatabase();
    return _database!;
  }

  Future<Database> _initDatabase() async {
    final directory = await getApplicationDocumentsDirectory();
    final path = join(directory.path, 'audio_player.db');
    
    return await openDatabase(
      path,
      version: 1,
      onCreate: _onCreate,
    );
  }

  Future<void> _onCreate(Database db, int version) async {
    // 收藏表
    await db.execute('''
      CREATE TABLE favorites (
        song_id INTEGER PRIMARY KEY,
        added_at TEXT NOT NULL
      )
    ''');
    
    // 播放列表表
    await db.execute('''
      CREATE TABLE playlists (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        cover_path TEXT,
        created_at TEXT NOT NULL
      )
    ''');
    
    // 播放列表 - 歌曲关联表
    await db.execute('''
      CREATE TABLE playlist_songs (
        playlist_id INTEGER NOT NULL,
        song_id INTEGER NOT NULL,
        position INTEGER NOT NULL,
        added_at TEXT NOT NULL,
        FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE,
        PRIMARY KEY (playlist_id, song_id)
      )
    ''');
    
    // 最近播放表
    await db.execute('''
      CREATE TABLE recent_plays (
        song_id INTEGER PRIMARY KEY,
        played_at TEXT NOT NULL
      )
    ''');
  }

  // ========== 收藏操作 ==========
  
  Future<void> addToFavorites(int songId) async {
    final db = await database;
    await db.insert(
      'favorites',
      {
        'song_id': songId,
        'added_at': DateTime.now().toIso8601String(),
      },
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }

  Future<void> removeFromFavorites(int songId) async {
    final db = await database;
    await db.delete(
      'favorites',
      where: 'song_id = ?',
      whereArgs: [songId],
    );
  }

  Future<bool> isFavorite(int songId) async {
    final db = await database;
    final result = await db.query(
      'favorites',
      where: 'song_id = ?',
      whereArgs: [songId],
    );
    return result.isNotEmpty;
  }

  Future<List<int>> getFavoriteSongIds() async {
    final db = await database;
    final result = await db.query('favorites');
    return result.map((row) => row['song_id'] as int).toList();
  }

  // ========== 播放列表操作 ==========
  
  Future<int> createPlaylist(String name, {String? coverPath}) async {
    final db = await database;
    return await db.insert('playlists', {
      'name': name,
      'cover_path': coverPath,
      'created_at': DateTime.now().toIso8601String(),
    });
  }

  Future<List<dynamic>> getPlaylists() async {
    final db = await database;
    final result = await db.query('playlists', orderBy: 'created_at DESC');
    return result;
  }

  Future<void> addSongToPlaylist(int playlistId, int songId, int position) async {
    final db = await database;
    await db.insert(
      'playlist_songs',
      {
        'playlist_id': playlistId,
        'song_id': songId,
        'position': position,
        'added_at': DateTime.now().toIso8601String(),
      },
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }

  Future<List<int>> getPlaylistSongs(int playlistId) async {
    final db = await database;
    final result = await db.query(
      'playlist_songs',
      where: 'playlist_id = ?',
      whereArgs: [playlistId],
      orderBy: 'position ASC',
    );
    return result.map((row) => row['song_id'] as int).toList();
  }

  // ========== 最近播放操作 ==========
  
  Future<void> addToRecent(int songId) async {
    final db = await database;
    await db.insert(
      'recent_plays',
      {
        'song_id': songId,
        'played_at': DateTime.now().toIso8601String(),
      },
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }

  Future<List<int>> getRecentPlayed({int limit = 20}) async {
    final db = await database;
    final result = await db.query(
      'recent_plays',
      orderBy: 'played_at DESC',
      limit: limit,
    );
    return result.map((row) => row['song_id'] as int).toList();
  }
}

5. 音频状态管理 - lib/providers/audio_provider.dart

import 'package:flutter/material.dart';
import '../services/media_scanner_service.dart';
import '../services/audio_service.dart';
import '../services/database_helper.dart';
import '../models/song_model.dart';

/// 音频播放状态管理
class AudioProvider extends ChangeNotifier {
  final MediaScannerService _scanner = MediaScannerService();
  final AudioPlayerService _player = AudioPlayerService();
  final DatabaseHelper _db = DatabaseHelper();
  
  List<Song> _songs = [];
  List<dynamic> _albums = [];
  List<Song> _favoriteSongs = [];
  List<Song> _recentSongs = [];
  
  bool _isLoading = false;
  bool _isPlaying = false;
  Song? _currentSong;
  Duration _currentPosition = Duration.zero;
  Duration _totalDuration = Duration.zero;
  
  // Getters
  List<Song> get songs => _songs;
  List<dynamic> get albums => _albums;
  List<Song> get favoriteSongs => _favoriteSongs;
  List<Song> get recentSongs => _recentSongs;
  bool get isLoading => _isLoading;
  bool get isPlaying => _isPlaying;
  Song? get currentSong => _currentSong;
  Duration get currentPosition => _currentPosition;
  Duration get totalDuration => _totalDuration;
  double get progress => _totalDuration.inMilliseconds > 0
      ? _currentPosition.inMilliseconds / _totalDuration.inMilliseconds
      : 0.0;
  
  AudioProvider() {
    _player.init();
    _initPlayerListeners();
  }
  
  void _initPlayerListeners() {
    _player.stateStream.listen((state) {
      _isPlaying = _player.isPlaying;
      notifyListeners();
    });
    
    _player.positionStream.listen((position) {
      _currentPosition = position;
      notifyListeners();
    });
    
    _player.durationStream.listen((duration) {
      _totalDuration = duration;
      notifyListeners();
    });
  }
  
  /// 加载所有歌曲
  Future<void> loadSongs() async {
    _isLoading = true;
    notifyListeners();
    
    try {
      _songs = await _scanner.scanAllSongs();
      
      // 加载收藏状态
      final favoriteIds = await _db.getFavoriteSongIds();
      for (var song in _songs) {
        song.isFavorite = favoriteIds.contains(song.id);
      }
      
      // 加载最近播放
      await _loadRecentSongs();
      
      // 加载收藏歌曲
      await _loadFavoriteSongs();
    } catch (e) {
      print('加载歌曲失败:$e');
    }
    
    _isLoading = false;
    notifyListeners();
  }
  
  /// 加载收藏歌曲
  Future<void> _loadFavoriteSongs() async {
    final favoriteIds = await _db.getFavoriteSongIds();
    _favoriteSongs = _songs.where((s) => favoriteIds.contains(s.id)).toList();
  }
  
  /// 加载最近播放
  Future<void> _loadRecentSongs() async {
    final recentIds = await _db.getRecentPlayed(limit: 20);
    _recentSongs = recentIds
        .map((id) => _songs.firstWhere((s) => s.id == id, orElse: () => _songs.first))
        .toList();
  }
  
  /// 播放歌曲
  Future<void> playSong(Song song) async {
    await _player.playSong(song);
    _currentSong = song;
    _isPlaying = true;
    
    // 添加到最近播放
    await _db.addToRecent(song.id);
    await _loadRecentSongs();
    
    notifyListeners();
  }
  
  /// 播放/暂停
  Future<void> playPause() async {
    await _player.playPause();
    _isPlaying = _player.isPlaying;
    notifyListeners();
  }
  
  /// 播放上一首
  Future<void> playPrevious() async {
    await _player.playPrevious();
    _currentSong = _player.currentSong;
    notifyListeners();
  }
  
  /// 播放下一首
  Future<void> playNext() async {
    await _player.playNext();
    _currentSong = _player.currentSong;
    notifyListeners();
  }
  
  /// 切换收藏状态
  Future<void> toggleFavorite(int songId) async {
    final song = _songs.firstWhere((s) => s.id == songId);
    song.isFavorite = !song.isFavorite;
    
    if (song.isFavorite) {
      await _db.addToFavorites(songId);
      _favoriteSongs.add(song);
    } else {
      await _db.removeFromFavorites(songId);
      _favoriteSongs.removeWhere((s) => s.id == songId);
    }
    
    notifyListeners();
  }
  
  /// 设置播放进度
  Future<void> seek(Duration position) async {
    await _player.seek(position);
  }
  
  /// 播放下一首(快捷方法)
  void next() {
    playNext();
  }
  
  /// 播放上一首(快捷方法)
  void previous() {
    playPrevious();
  }
  
  /// 切换循环
  void toggleLoop() {
    _player.toggleLoop();
    notifyListeners();
  }
  
  /// 切换随机播放
  void toggleShuffle() {
    _player.toggleShuffle();
    notifyListeners();
  }
  
  /// 跳转到指定位置
  void seekTo(Duration position) {
    seek(position);
  }
  
  /// 获取当前歌曲时长
  String get formattedPosition {
    final minutes = _currentPosition.inMinutes;
    final seconds = (_currentPosition.inSeconds % 60).toString().padLeft(2, '0');
    return '$minutes:$seconds';
  }
  
  /// 获取总时长
  String get formattedTotalDuration {
    final minutes = _totalDuration.inMinutes;
    final seconds = (_totalDuration.inSeconds % 60).toString().padLeft(2, '0');
    return '$minutes:$seconds';
  }
}

6. 播放列表管理 - lib/providers/playlist_provider.dart

import 'package:flutter/material.dart';
import '../services/database_helper.dart';
import '../models/song_model.dart';

/// 播放列表管理
class PlaylistProvider extends ChangeNotifier {
  final DatabaseHelper _db = DatabaseHelper();
  
  List<dynamic> _playlists = [];
  bool _isLoading = false;
  
  List<dynamic> get playlists => _playlists;
  bool get isLoading => _isLoading;
  
  /// 加载所有播放列表
  Future<void> loadPlaylists() async {
    _isLoading = true;
    notifyListeners();
    
    try {
      _playlists = await _db.getPlaylists();
    } catch (e) {
      print('加载播放列表失败:$e');
    }
    
    _isLoading = false;
    notifyListeners();
  }
  
  /// 创建播放列表
  Future<void> createPlaylist(String name) async {
    await _db.createPlaylist(name);
    await loadPlaylists();
  }
  
  /// 添加歌曲到播放列表
  Future<void> addSongToPlaylist(int playlistId, Song song) async {
    final songs = await _db.getPlaylistSongs(playlistId);
    await _db.addSongToPlaylist(playlistId, song.id, songs.length);
  }
  
  /// 删除播放列表
  Future<void> deletePlaylist(int playlistId) async {
    // TODO: 实现删除逻辑
  }
}

7. 主界面 - lib/screens/home_screen.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/audio_provider.dart';
import '../providers/playlist_provider.dart';
import '../widgets/mini_player_bar.dart';
import 'player_screen.dart';
import 'playlist_screen.dart';

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

  
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  int _selectedIndex = 0;

  
  void initState() {
    super.initState();
    // 初始化加载歌曲
    Future.microtask(() {
      context.read<AudioProvider>().loadSongs();
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _selectedIndex,
        children: [
          _buildMusicTab(),
          _buildPlaylistsTab(),
          _buildProfileTab(),
        ],
      ),
      bottomNavigationBar: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          // 迷你播放器
          Consumer<AudioProvider>(
            builder: (context, provider, child) {
              if (provider.currentSong == null) {
                return const SizedBox.shrink();
              }
              return MiniPlayerBar(
                onTap: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (_) => const PlayerScreen(),
                    ),
                  );
                },
              );
            },
          ),
          // 底部导航栏
          BottomNavigationBar(
            currentIndex: _selectedIndex,
            onTap: (index) => setState(() => _selectedIndex = index),
            type: BottomNavigationBarType.fixed,
            items: const [
              BottomNavigationBarItem(
                icon: Icon(Icons.music_note),
                label: '音乐',
              ),
              BottomNavigationBarItem(
                icon: Icon(Icons.playlist_play),
                label: '播放列表',
              ),
              BottomNavigationBarItem(
                icon: Icon(Icons.person),
                label: '我的',
              ),
            ],
          ),
        ],
      ),
    );
  }

  Widget _buildMusicTab() {
    return Consumer<AudioProvider>(
      builder: (context, provider, child) {
        if (provider.isLoading) {
          return const Center(child: CircularProgressIndicator());
        }

        if (provider.songs.isEmpty) {
          return const Center(
            child: Text('没有找到音乐文件'),
          );
        }

        return ListView.builder(
          itemCount: provider.songs.length,
          itemBuilder: (context, index) {
            final song = provider.songs[index];
            return ListTile(
              leading: const CircleAvatar(
                child: Icon(Icons.music_note),
              ),
              title: Text(song.title),
              subtitle: Text(song.artist),
              trailing: IconButton(
                icon: Icon(
                  song.isFavorite ? Icons.favorite : Icons.favorite_border,
                ),
                onPressed: () => provider.toggleFavorite(song.id),
              ),
              onTap: () => provider.playSong(song),
            );
          },
        );
      },
    );
  }

  Widget _buildPlaylistsTab() {
    return Consumer<PlaylistProvider>(
      builder: (context, provider, child) {
        if (provider.isLoading) {
          return const Center(child: CircularProgressIndicator());
        }

        if (provider.playlists.isEmpty) {
          return const Center(
            child: Text('暂无播放列表'),
          );
        }

        return ListView.builder(
          itemCount: provider.playlists.length,
          itemBuilder: (context, index) {
            final playlist = provider.playlists[index];
            return ListTile(
              leading: const CircleAvatar(
                child: Icon(Icons.playlist_play),
              ),
              title: Text(playlist['name'] ?? '未命名'),
              subtitle: Text('播放列表'),
              onTap: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (_) => PlaylistScreen(playlistId: playlist['id']),
                  ),
                );
              },
            );
          },
        );
      },
    );
  }

  Widget _buildProfileTab() {
    return const Center(
      child: Text('个人中心'),
    );
  }
}

8. 播放器界面 - lib/screens/player_screen.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/audio_provider.dart';
import '../widgets/seek_slider.dart';
import '../widgets/circular_album_art.dart';

class PlayerScreen extends StatelessWidget {
  const PlayerScreen({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: SafeArea(
        child: Consumer<AudioProvider>(
          builder: (context, provider, child) {
            final song = provider.currentSong;

            if (song == null) {
              return const Center(
                child: Text('没有正在播放的歌曲'),
              );
            }

            return Column(
              children: [
                // 顶部栏
                Padding(
                  padding: const EdgeInsets.all(16),
                  child: Row(
                    children: [
                      IconButton(
                        icon: const Icon(Icons.arrow_downward, color: Colors.white),
                        onPressed: () => Navigator.pop(context),
                      ),
                      const Spacer(),
                      IconButton(
                        icon: const Icon(Icons.more_vert, color: Colors.white),
                        onPressed: () => _showOptionsMenu(context, provider),
                      ),
                    ],
                  ),
                ),

                // 专辑封面
                Expanded(
                  flex: 3,
                  child: CircularAlbumArt(
                    song: song,
                    isPlaying: provider.isPlaying,
                  ),
                ),

                const SizedBox(height: 32),

                // 歌曲信息
                Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 32),
                  child: Column(
                    children: [
                      Text(
                        song.title,
                        style: const TextStyle(
                          color: Colors.white,
                          fontSize: 24,
                          fontWeight: FontWeight.bold,
                        ),
                        maxLines: 1,
                        overflow: TextOverflow.ellipsis,
                      ),
                      const SizedBox(height: 8),
                      Text(
                        song.artist,
                        style: TextStyle(
                          color: Colors.white.withOpacity(0.7),
                          fontSize: 16,
                        ),
                        maxLines: 1,
                        overflow: TextOverflow.ellipsis,
                      ),
                    ],
                  ),
                ),

                const SizedBox(height: 32),

                // 进度条
                Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 32),
                  child: SeekSlider(
                    progress: provider.progress,
                    position: provider.currentPosition,
                    duration: provider.totalDuration,
                    onChanged: (value) {
                      final position = Duration(
                        milliseconds: (value * provider.totalDuration.inMilliseconds).round(),
                      );
                      provider.seekTo(position);
                    },
                  ),
                ),

                const SizedBox(height: 24),

                // 控制按钮
                Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 32),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      IconButton(
                        icon: const Icon(Icons.skip_previous, size: 40),
                        color: Colors.white,
                        onPressed: provider.previous,
                      ),
                      const SizedBox(width: 24),
                      _buildPlayPauseButton(context, provider),
                      const SizedBox(width: 24),
                      IconButton(
                        icon: const Icon(Icons.skip_next, size: 40),
                        color: Colors.white,
                        onPressed: provider.next,
                      ),
                    ],
                  ),
                ),

                const SizedBox(height: 24),

                // 播放模式按钮
                Padding(
                  padding: const EdgeInsets.symmetric(horizontal: 32),
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                    children: [
                      IconButton(
                        icon: const Icon(Icons.shuffle, color: Colors.white),
                        onPressed: provider.toggleShuffle,
                      ),
                      IconButton(
                        icon: const Icon(Icons.repeat, color: Colors.white),
                        onPressed: provider.toggleLoop,
                      ),
                    ],
                  ),
                ),

                const SizedBox(height: 20),
              ],
            );
          },
        ),
      ),
    );
  }

  Widget _buildPlayPauseButton(BuildContext context, AudioProvider provider) {
    return GestureDetector(
      onTap: provider.playPause,
      child: Container(
        width: 70,
        height: 70,
        decoration: BoxDecoration(
          shape: BoxShape.circle,
          color: Theme.of(context).primaryColor,
          boxShadow: [
            BoxShadow(
              color: Colors.purple.withOpacity(0.3),
              blurRadius: 20,
              spreadRadius: 5,
            ),
          ],
        ),
        child: Icon(
          provider.isPlaying ? Icons.pause : Icons.play_arrow,
          size: 40,
          color: Colors.white,
        ),
      ),
    );
  }

  void _showOptionsMenu(BuildContext context, AudioProvider provider) {
    showModalBottomSheet(
      context: context,
      builder: (context) => Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          ListTile(
            leading: const Icon(Icons.favorite),
            title: const Text('收藏'),
            onTap: () {
              if (provider.currentSong != null) {
                provider.toggleFavorite(provider.currentSong!.id);
              }
              Navigator.pop(context);
            },
          ),
          ListTile(
            leading: const Icon(Icons.playlist_add),
            title: const Text('添加到播放列表'),
            onTap: () {
              Navigator.pop(context);
              // TODO: 显示播放列表选择对话框
            },
          ),
        ],
      ),
    );
  }
}

9. 播放列表界面 - lib/screens/playlist_screen.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/audio_provider.dart';
import '../providers/playlist_provider.dart';

class PlaylistScreen extends StatelessWidget {
  final int playlistId;

  const PlaylistScreen({super.key, required this.playlistId});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('播放列表'),
        backgroundColor: Colors.black,
        foregroundColor: Colors.white,
      ),
      backgroundColor: Colors.black,
      body: Consumer<PlaylistProvider>(
        builder: (context, provider, child) {
          // TODO: 实现播放列表歌曲列表
          return const Center(
            child: Text('播放列表详情'),
          );
        },
      ),
    );
  }
}

10. 迷你播放器 - lib/widgets/mini_player_bar.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/audio_provider.dart';

class MiniPlayerBar extends StatelessWidget {
  final VoidCallback onTap;

  const MiniPlayerBar({super.key, required this.onTap});

  
  Widget build(BuildContext context) {
    return Consumer<AudioProvider>(
      builder: (context, provider, child) {
        final song = provider.currentSong;
        
        if (song == null) {
          return const SizedBox.shrink();
        }

        return GestureDetector(
          onTap: onTap,
          child: Container(
            height: 64,
            decoration: BoxDecoration(
              color: Colors.grey[900],
              boxShadow: [
                BoxShadow(
                  color: Colors.black.withOpacity(0.3),
                  blurRadius: 8,
                  offset: const Offset(0, -2),
                ),
              ],
            ),
            child: Column(
              children: [
                // 进度条
                LinearProgressIndicator(
                  value: provider.progress,
                  backgroundColor: Colors.grey[800],
                  valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
                  minHeight: 2,
                ),
                // 歌曲信息和控制按钮
                Expanded(
                  child: Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 16),
                    child: Row(
                      children: [
                        // 专辑封面缩略图
                        Container(
                          width: 48,
                          height: 48,
                          decoration: BoxDecoration(
                            color: Colors.grey[800],
                            borderRadius: BorderRadius.circular(4),
                          ),
                          child: const Icon(Icons.music_note, color: Colors.white),
                        ),
                        const SizedBox(width: 12),
                        // 歌曲信息
                        Expanded(
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.center,
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Text(
                                song.title,
                                style: const TextStyle(
                                  color: Colors.white,
                                  fontSize: 14,
                                  fontWeight: FontWeight.w500,
                                ),
                                maxLines: 1,
                                overflow: TextOverflow.ellipsis,
                              ),
                              Text(
                                song.artist,
                                style: TextStyle(
                                  color: Colors.grey[400],
                                  fontSize: 12,
                                ),
                                maxLines: 1,
                                overflow: TextOverflow.ellipsis,
                              ),
                            ],
                          ),
                        ),
                        // 播放控制按钮
                        Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            IconButton(
                              icon: const Icon(Icons.skip_previous, color: Colors.white),
                              onPressed: provider.previous,
                            ),
                            IconButton(
                              icon: Icon(
                                provider.isPlaying ? Icons.pause : Icons.play_arrow,
                                color: Colors.white,
                              ),
                              onPressed: provider.playPause,
                            ),
                          ],
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}

11. 进度条控件 - lib/widgets/seek_slider.dart

import 'package:flutter/material.dart';

class SeekSlider extends StatelessWidget {
  final double progress;
  final Duration position;
  final Duration duration;
  final ValueChanged<double> onChanged;

  const SeekSlider({
    super.key,
    required this.progress,
    required this.position,
    required this.duration,
    required this.onChanged,
  });

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        SliderTheme(
          data: SliderTheme.of(context).copyWith(
            trackHeight: 2.0,
            thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6.0),
            overlayShape: const RoundSliderOverlayShape(overlayRadius: 14.0),
            activeTrackColor: Colors.white,
            inactiveTrackColor: Colors.white24,
            thumbColor: Colors.white,
            overlayColor: Colors.white24,
          ),
          child: Slider(
            value: progress.clamp(0.0, 1.0),
            onChanged: onChanged,
          ),
        ),
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text(
                _formatDuration(position),
                style: TextStyle(
                  color: Colors.white.withOpacity(0.7),
                  fontSize: 12,
                ),
              ),
              Text(
                _formatDuration(duration),
                style: TextStyle(
                  color: Colors.white.withOpacity(0.7),
                  fontSize: 12,
                ),
              ),
            ],
          ),
        ),
      ],
    );
  }

  String _formatDuration(Duration duration) {
    final minutes = duration.inMinutes;
    final seconds = (duration.inSeconds % 60).toString().padLeft(2, '0');
    return '$minutes:$seconds';
  }
}

12. 圆形专辑封面 - lib/widgets/circular_album_art.dart

import 'dart:typed_data';
import 'package:flutter/material.dart';
import '../models/song_model.dart';
import '../services/media_scanner_service.dart';

class CircularAlbumArt extends StatefulWidget {
  final Song song;
  final bool isPlaying;

  const CircularAlbumArt({
    super.key,
    required this.song,
    required this.isPlaying,
  });

  
  State<CircularAlbumArt> createState() => _CircularAlbumArtState();
}

class _CircularAlbumArtState extends State<CircularAlbumArt>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  Uint8List? _albumArt;

  final MediaScannerService _scanner = MediaScannerService();

  
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 20),
      vsync: this,
    );

    if (widget.isPlaying) {
      _controller.repeat();
    }

    _loadAlbumArt();
  }

  
  void didUpdateWidget(CircularAlbumArt oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.isPlaying != oldWidget.isPlaying) {
      if (widget.isPlaying) {
        _controller.repeat();
      } else {
        _controller.stop();
      }
    }
    if (widget.song.id != oldWidget.song.id) {
      _loadAlbumArt();
    }
  }

  Future<void> _loadAlbumArt() async {
    final artwork = await _scanner.getAlbumArt(widget.song.albumId);
    if (artwork != null && mounted) {
      setState(() {
        _albumArt = Uint8List.fromList(artwork);
      });
    }
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Center(
      child: RotationTransition(
        turns: _controller,
        child: Container(
          width: 300,
          height: 300,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            boxShadow: [
              BoxShadow(
                color: Colors.black.withOpacity(0.5),
                blurRadius: 20,
                spreadRadius: 5,
              ),
            ],
          ),
          child: ClipOval(
            child: _albumArt != null
                ? Image.memory(
                    _albumArt!,
                    fit: BoxFit.cover,
                  )
                : Container(
                    color: Colors.grey[800],
                    child: const Icon(
                      Icons.music_note,
                      size: 100,
                      color: Colors.white54,
                    ),
                  ),
          ),
        ),
      ),
    );
  }
}

13. 应用入口 - lib/main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/audio_provider.dart';
import 'providers/playlist_provider.dart';
import 'screens/home_screen.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => AudioProvider()),
        ChangeNotifierProvider(create: (_) => PlaylistProvider()),
      ],
      child: MaterialApp(
        title: '音乐播放器',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          colorScheme: ColorScheme.fromSeed(
            seedColor: Colors.purple,
            brightness: Brightness.dark,
          ),
          useMaterial3: true,
        ),
        home: const HomeScreen(),
      ),
    );
  }
}

HarmonyOS 配置

1. 配置 module.json5

文件位置:ohos/entry/src/main/module.json5

{
  "module": {
    "name": "entry",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "phone",
      "tablet"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:icon",
        "label": "$string:EntryAbility_name",
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.READ_MEDIA",
        "reason": "$string:permission_read_media_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.MEDIA_LOCATION",
        "reason": "$string:permission_media_location_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.KEEP_BACKGROUND_RUNNING",
        "reason": "$string:permission_keep_background_running_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "always"
        }
      }
    ]
  }
}

2. 配置字符串资源

文件位置:ohos/entry/src/main/resources/base/element/string.json

{
  "string": [
    {
      "name": "module_desc",
      "value": "音乐播放器模块"
    },
    {
      "name": "EntryAbility_desc",
      "value": "音乐播放器入口"
    },
    {
      "name": "EntryAbility_name",
      "value": "音乐播放器"
    },
    {
      "name": "permission_read_media_reason",
      "value": "需要访问您的媒体文件以播放音乐"
    },
    {
      "name": "permission_media_location_reason",
      "value": "需要访问媒体位置信息"
    },
    {
      "name": "permission_keep_background_running_reason",
      "value": "需要在后台运行以持续播放音乐"
    }
  ]
}

3. 配置 build-profile.json5

文件位置:ohos/build-profile.json5

确保配置了正确的签名配置:

{
  "app": {
    "signingConfigs": [],
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "compatibleSdkVersion": "5.0.0(12)",
        "runtimeOS": "HarmonyOS"
      }
    ]
  }
}

常见问题解决

1. 权限问题

如果应用无法扫描到音乐文件,请确保:

  • 已在 module.json5 中正确配置权限
  • 已在设备设置中授予应用存储权限
  • 设备中有音频文件

2. 依赖包问题

如果遇到依赖包解析错误:

flutter clean
flutter pub get

3. 构建失败

如果 HarmonyOS 构建失败:

  • 确保 DevEco Studio 已正确安装
  • 检查签名配置是否正确
  • 尝试重启 DevEco Studio

成果展示

在这里插入图片描述

Logo

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

更多推荐