睿诚科技协会

Android网络音乐播放器,如何实现流畅播放与离线缓存?

项目概述与核心功能

一个功能完善的网络音乐播放器至少应包含以下功能:

Android网络音乐播放器,如何实现流畅播放与离线缓存?-图1
(图片来源网络,侵删)
  1. 音乐列表展示:

    • 从网络 API 获取音乐数据(如歌曲名、歌手、专辑封面 URL、音乐文件 URL)。
    • 使用 RecyclerView 以列表形式展示音乐。
    • 支持下拉刷新和上拉加载更多。
  2. 音乐播放控制:

    • 播放/暂停: 点击列表中的歌曲进行播放,并可以暂停/继续。
    • 上一首/下一首: 提供切换歌曲的功能。
    • 进度条: 显示和拖动当前播放进度。
    • 播放模式: 支持列表循环、单曲循环、随机播放。
  3. 后台播放:

    • 当用户按 Home 键或切换到其他应用时,音乐应能继续播放。
    • 通过通知栏显示播放信息和提供基本控制。
  4. 媒体会话:

    Android网络音乐播放器,如何实现流畅播放与离线缓存?-图2
    (图片来源网络,侵删)
    • 允许锁屏界面显示播放信息和进行控制。
    • 与 Android Auto、Google Assistant 等系统级服务集成。
  5. 播放队列管理:

    • 显示当前播放队列。
    • 支持拖拽排序、移除歌曲。

技术选型与依赖

我们需要选择合适的库来简化开发,提高效率。

核心库:

  • Kotlin + Coroutines: 现代 Android 开发的首选语言,协程用于处理异步网络请求和数据库操作,代码更简洁。
  • Retrofit: 强大的网络请求库,用于与音乐 API 交互。
  • OkHttp: Retrofit 的底层网络库,用于处理 HTTP 请求和缓存。
  • Glide/Coil: 图片加载库,用于高效加载和缓存专辑封面。
  • ExoPlayer: Google 官方推荐的视频/音频播放器,功能强大,高度可定制,支持多种媒体格式,并且内置了对后台播放和媒体会话的良好支持。
  • Room: SQLite 数据库的 ORM 封装,用于缓存音乐数据,实现离线播放和提升用户体验。
  • ViewModel: 保存和管理与 UI 相关的数据,使其在配置更改(如屏幕旋转)时不会丢失。
  • LiveData/StateFlow: 响应式编程组件,用于在数据变化时通知 UI 层。
  • DataStore: 用于存储用户的偏好设置,如播放模式、音量等(可以替代旧的 SharedPreferences)。

Gradle 依赖配置 (app/build.gradle.ktsapp/build.gradle):

Android网络音乐播放器,如何实现流畅播放与离线缓存?-图3
(图片来源网络,侵删)
// Kotlin + Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
// Retrofit + OkHttp
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation "com.squareup.okhttp3:logging-interceptor:4.11.0" // 用于网络日志
// ExoPlayer
implementation 'com.google.android.exoplayer:exoplayer:1.3.1' // 注意版本号,使用最新的
implementation 'com.google.android.exoplayer:exoplayer-core:1.3.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:1.3.1'
implementation 'com.google.android.exoplayer:extension-mediasession:1.3.1'
// Room
def room_version = "2.6.0"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" // Kotlin
annotationProcessor "androidx.room:room-compiler:$room_version" // Java
// Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// ViewModel & LiveData
def lifecycle_version = "2.6.2"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Image Loading (Coil is a modern alternative to Glide)
implementation "io.coil-kt:coil:2.4.0"
// DataStore
def dataStore_version = "1.1.0-alpha04"
implementation "androidx.datastore:datastore-preferences:$dataStore_version"

项目架构设计

我们采用 MVVM (Model-View-ViewModel) 架构,并结合 Repository 模式,使代码结构清晰、易于测试和扩展。

  1. View (Activity/Fragment): 负责 UI 层的展示和用户交互,它观察 ViewModel 的数据变化并更新 UI。
  2. ViewModel: 持有 UI 状态(如播放列表、当前播放歌曲、播放状态),并处理业务逻辑,它通过 Repository 获取数据。
  3. Repository: 数据仓库,它是数据源(网络 API、本地数据库)的单一入口。ViewModel 不关心数据来自网络还是本地,都从 Repository 获取。
  4. Model: 包含实体类(如 Music)和数据源接口(如 MusicDataSource)。

分步实现指南

步骤 1: 定义数据模型

创建一个数据类来表示一首音乐。

// Music.kt
data class Music(
    val id: String,
    val title: String,
    val artist: String,
    val url: String,      // 音乐文件URL
    val coverUrl: String, // 专辑封面URL
    val duration: Long    // 时长(毫秒)
)

步骤 2: 创建数据源和 Repository

  1. 创建 API 接口: 使用 Retrofit 定义如何获取音乐列表。

    // MusicApiService.kt
    interface MusicApiService {
        @GET("api/music/list") // 假设的API地址
        suspend fun getMusicList(): List<Music>
    }
  2. 创建本地数据库: 使用 Room 定义 DAO 和 Database。

    // MusicDao.kt
    @Dao
    interface MusicDao {
        @Query("SELECT * FROM music_table")
        fun getAllMusic(): Flow<List<Music>> // 使用Flow进行响应式查询
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        suspend fun insertAll(musicList: List<Music>)
    }
    // AppDatabase.kt
    @Database(entities = [Music::class], version = 1)
    abstract class AppDatabase : RoomDatabase() {
        abstract fun musicDao(): MusicDao
    }
  3. 创建 Repository: 实现 Repository 来统一管理网络和本地数据。

    // MusicRepository.kt
    class MusicRepository @Inject constructor(
        private val apiService: MusicApiService,
        private val musicDao: MusicDao
    ) {
        // 优先从数据库获取,如果没有则从网络获取并缓存
        fun getMusicList(): Flow<List<Music>> = flow {
            // 1. 尝试从数据库获取
            val cachedMusic = musicDao.getAllMusic().first()
            if (cachedMusic.isNotEmpty()) {
                emit(cachedMusic)
            }
            // 2. 从网络获取
            try {
                val networkMusic = apiService.getMusicList()
                // 3. 将新数据存入数据库
                musicDao.insertAll(networkMusic)
                emit(networkMusic)
            } catch (e: Exception) {
                // 如果网络请求失败,可以只返回缓存数据或抛出错误
                Log.e("MusicRepository", "Network request failed", e)
            }
        }.flowOn(Dispatchers.IO) // 在IO线程执行
    }

步骤 3: 创建 ViewModel

ViewModel 是连接 UI 和数据源的桥梁。

// MusicPlayerViewModel.kt
class MusicPlayerViewModel @ViewModelInject constructor(
    private val repository: MusicRepository
) : ViewModel() {
    private val _musicList = MutableStateFlow<List<Music>>(emptyList())
    val musicList: StateFlow<List<Music>> = _musicList
    init {
        // 在协程中加载数据
        viewModelScope.launch {
            repository.getMusicList().collect {
                _musicList.value = it
            }
        }
    }
    // ... 其他逻辑,如当前播放歌曲、播放状态等
}

步骤 4: 实现播放核心逻辑 (ExoPlayer)

这是播放器的核心,建议将 ExoPlayer 的逻辑封装在一个单独的 MusicPlayerService 中,以便实现后台播放。

  1. 创建 MusicPlayerService:

    // MusicPlayerService.kt
    class MusicPlayerService : Service(), Player.Listener {
        private var exoPlayer: ExoPlayer? = null
        private var currentMediaItem: MediaItem? = null
        private var playWhenReady = true
        private var currentWindow = 0
        private var playbackPosition = 0L
        // ... onCreate, onStartCommand, onBind, onDestroy ...
        private fun initializePlayer() {
            exoPlayer = ExoPlayer.Builder(this).build().apply {
                addListener(this@MusicPlayerService)
                setMediaItem(currentMediaItem ?: return)
                seekTo(currentWindow, playbackPosition)
                prepare()
                playWhenReady = this@MusicPlayerService.playWhenReady
            }
        }
        fun playMusic(music: Music) {
            val mediaItem = MediaItem.Builder()
                .setUri(music.url)
                .setMediaId(music.id)
                .setTitle(music.title)
                .setArtist(music.artist)
                .build()
            currentMediaItem = mediaItem
            if (exoPlayer == null) {
                initializePlayer()
            } else {
                exoPlayer?.setMediaItem(mediaItem)
                exoPlayer?.prepare()
                exoPlayer?.playWhenReady = true
            }
        }
        fun pauseMusic() {
            playWhenReady = false
            exoPlayer?.playWhenReady = false
        }
        // ... 实现Player.Listener接口的方法,如onPlaybackStateChanged, onIsPlayingChanged等
    }
  2. 在 ViewModel 中控制 Service: 使用 ServiceConnectionBound Service 模式,让 UI 可以与 MusicPlayerService 交互。

    // 在ViewModel或Fragment中
    private var musicService: MusicPlayerService? = null
    private var serviceBound = false
    private var connection: ServiceConnection? = null
    fun bindService(context: Context) {
        connection = object : ServiceConnection {
            override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
                val binder = service as MusicPlayerService.LocalBinder
                musicService = binder.getService()
                serviceBound = true
                // 可以在这里获取播放器状态,如当前播放歌曲、进度等
            }
            override fun onServiceDisconnected(name: ComponentName?) {
                serviceBound = false
            }
        }
        Intent(context, MusicPlayerService::class.java).also { intent ->
            context.bindService(intent, connection!!, Context.BIND_AUTO_CREATE)
        }
    }
    fun unbindService(context: Context) {
        if (serviceBound) {
            context.unbindService(connection!!)
            serviceBound = false
        }
    }
    fun playSelectedMusic(music: Music) {
        musicService?.playMusic(music)
    }

步骤 5: 实现 UI (RecyclerView 和 Activity)

  1. Adapter for RecyclerView:

    // MusicAdapter.kt
    class MusicAdapter(private val onMusicClick: (Music) -> Unit) :
        ListAdapter<Music, MusicAdapter.MusicViewHolder>(MusicDiffCallback()) {
        // ... onCreateViewHolder, onBindViewHolder ...
        inner class MusicViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
            fun bind(music: Music) {
                itemView.findViewById<TextView>(R.id.tv_title).text = music.title
                itemView.findViewById<TextView>(R.id.tv_artist).text = music.artist
                Glide.with(itemView.context)
                    .load(music.coverUrl)
                    .into(itemView.findViewById<ImageView>(R.id.iv_cover))
                itemView.setOnClickListener { onMusicClick(music) }
            }
        }
        // ... DiffCallback ...
    }
  2. Activity/Fragment UI 和逻辑:

    // MainActivity.kt
    class MainActivity : AppCompatActivity() {
        private lateinit var viewModel: MusicPlayerViewModel
        private lateinit var binding: ActivityMainBinding
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            binding = ActivityMainBinding.inflate(layoutInflater)
            setContentView(binding.root)
            viewModel = ViewModelProvider(this)[MusicPlayerViewModel::class.java]
            setupRecyclerView()
            // 观察音乐列表数据变化
            lifecycleScope.launch {
                repeatOnLifecycle(Lifecycle.State.STARTED) {
                    viewModel.musicList.collect { musicList ->
                        (binding.rvMusic.adapter as MusicAdapter).submitList(musicList)
                    }
                }
            }
        }
        private fun setupRecyclerView() {
            binding.rvMusic.apply {
                adapter = MusicAdapter { music ->
                    // 当用户点击一首歌时
                    viewModel.playSelectedMusic(music)
                }
                layoutManager = LinearLayoutManager(this@MainActivity)
            }
        }
        override fun onStart() {
            super.onStart()
            viewModel.bindService(this) // 绑定Service
        }
        override fun onStop() {
            super.onStop()
            viewModel.unbindService(this) // 解绑Service
        }
    }

步骤 6: 实现后台播放与通知

  1. 创建通知渠道 (在 Application 类的 onCreate 中):

    // MyApplication.kt
    class MyApplication : Application() {
        override fun onCreate() {
            super.onCreate()
            createNotificationChannel()
        }
        private fun createNotificationChannel() {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val serviceChannel = NotificationChannel(
                    "music_player_channel",
                    "Music Player",
                    NotificationManager.IMPORTANCE_DEFAULT
                )
                val manager = getSystemService(NotificationManager::class.java)
                manager.createNotificationChannel(serviceChannel)
            }
        }
    }
  2. MusicPlayerService 中创建并显示通知:

    // 在MusicPlayerService中
    private fun createNotification(): Notification {
        val intent = Intent(this, MainActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }
        val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
        return NotificationCompat.Builder(this, "music_player_channel")
            .setSmallIcon(R.drawable.ic_music_note) // 设置一个图标
            .setContentTitle("正在播放")
            .setContentText("歌曲名")
            .setContentIntent(pendingIntent)
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .build()
    }
    // 在onStartCommand中
    startForeground(1, createNotification())

步骤 7: 实现 MediaSession

MediaSession 是实现锁屏控制和系统集成的关键。

  1. MusicPlayerService 中初始化 MediaSession:

    // 在MusicPlayerService中
    private var mediaSession: MediaSessionCompat? = null
    private fun initializeMediaSession() {
        mediaSession = MediaSessionCompat(this, "MusicPlayerService").apply {
            setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
            setPlaybackState(PlaybackStateCompat.Builder().setState(PlaybackStateCompat.STATE_NONE, 0, 1.0f).build())
            setCallback(object : MediaSessionCompat.Callback() {
                override fun onPlay() {
                    exoPlayer?.play()
                }
                override fun onPause() {
                    exoPlayer?.pause()
                }
                override fun onSkipToNext() {
                    // 实现下一首逻辑
                }
                override fun onSkipToPrevious() {
                    // 实现上一首逻辑
                }
            })
            isActive = true
        }
    }
  2. MediaSession 连接到 MediaBrowserService: 为了让 Android Auto 等系统服务发现你的播放器,你需要继承 MediaBrowserService,这会让项目变得更复杂,但对于一个完整的播放器来说是必要的。


进阶功能与优化

  • 播放队列: 在 ViewModel 中维护一个 MutableList<Music> 作为播放队列,当用户选择“下一首”时,从队列中取出下一首歌曲并播放。
  • 播放模式: 使用 DataStore 存储用户的播放模式偏好(列表循环、单曲循环等),并在切换歌曲时根据此模式决定下一首歌曲。
  • 音频焦点: 在 MusicPlayerService 中处理 AudioManager.OnAudioFocusChangeListener,当其他应用(如来电)需要音频时,暂停你的播放;当音频焦点返回时,恢复播放。
  • 错误处理: 网络请求失败、音乐文件损坏等情况需要有友好的 UI 提示。
  • 性能优化:
    • 使用 DiffUtil 优化 RecyclerView 的更新效率。
    • 对专辑封面进行缓存(Glide/Coil 会自动处理)。
    • 使用 WorkManager 定期更新音乐库,而不是每次打开都强制刷新。

这个指南为你构建一个功能强大的 Android 网络音乐播放器提供了一个清晰的蓝图,你可以根据自己的需求和设计逐步实现和完善各个功能模块,祝你编码愉快!

分享:
扫描分享到社交APP
上一篇
下一篇