前言
上篇文章给大家分享了我的WanAndroid
项目,简要描述了一下项目的整体结构。由于作者非常喜欢听音乐,鉴于对音乐的情怀,就在一个技术社区App中强行加了一个音乐播放功能。关于这个播放器的设计,我也花了很多心思,应用了很多设计模式,今天就逐个给大家进行分型。
贴一下上篇文章的链接:https://juejin.im/post/5f09ac336fb9a07e93303162
先细心阅读,文末会给出github链接
1. 需求背景
设计一个音乐播放功能,包含一个播放界面,包含播放/暂停、上一首、下一首、播放列表、播放模式、进度更新等,返回到首页又一个悬浮窗包含播放状态、音乐信息,大概就长下面这样:
当前这是第一版,功能比较简单,后面我会持续完善。
2. 定义播放器
第一步肯定要先来定义一个音乐播放器,这里采用的是Android原生提供的MediaPlayer
,使用MediaPlayer
之前先考虑一个事情,可以直接将MediaPlayer
定义在业务中吗?答案是否定的,为什么?大概有如下几点:
- 如果将
MediaPlayer
与业务进行耦合,每次做修改势必会影响到业务,进而会产生不可预期的错误。 - 如果某一天需要将
MediaPlayer
替换,定会牵扯到大量代码,那代价会相当之大.
如何解决上述问题?其实我们可以先设计一个接口,该接口包含一个音乐播放器所有功能。如下
interface IPlayer {
...
...
/**
* 播放新的音频
* @param path 本地路径
*/
fun play(path: String)
/**
* 播放
*/
fun resume()
/**
* 暂停
*/
fun pause()
/**
* 停止播放,释放播放内容
*/
fun stop()
...
...
}
本篇文章会侧重讲设计思想,所以为了节省篇幅我只会贴出部分关键代码,下同。
定义一个类MediaPlayerHelper
实现IPlayer
接口,内部通过MediaPlayer
实现每个方法对应的功能,并基于IPlayer
接口编程。这种写法其实就是设计原则中的基于接口而非实现编程
,好处就是隐藏了具体实现,修改具体实现不会影响到上层业务。并且符合开闭原则(对 扩展开放、修改关闭)
,如果需要对MediaPlayer
替换,直接写一个类实现IPlayer
接口中的功能,然后对目标类做替换即可。用一段代码表示:
private val playerHelper: IPlayer = MediaPlayerHelper()
//替换为
private val playerHelper: IPlayer = XXPlayerHelper()
关于这部分完整代码可至package com.zs.base_library.play(包名)
目录下参考
3. 状态管理
一个音乐播放器通常要与整个App
的生命周期保持一致,并且多处UI状态如:Notification
、播放页
、首页悬浮
必须保持一致,所以此时我们需要一个单例
来维护播放状态与音乐信息,这个单例
大概长这样:
class PlayerManager private constructor(){
//单例创建
companion object {
val instance: PlayerManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
PlayerManager()
}
}
....
private val playerHelper: IPlayer = MediaPlayerHelper()
fun pause() {
...
playerHelper.pause()
}
private fun resume() {
...
playerHelper.resume()
}
...
...
}
PlayerManager
由单例模式实现,内部包含了所有IPlayer
方法,通过PlayerManager
指使IPlayer
去实现具体的播放操作,并且生命周期与App进程
一致,可在App
任意地方操作播放器。
PlayerManager
作用大概有如下几点:
- 将目标对象
IPlayer
与具体业务进行隔离 - 内部维护了观察者
(后面会讲到)
,对状态同一做分发。 - 统一管理
播放模式
和播放列表
结合其特性分析,PlayerManager
实则也是一个代理类。如果想对音频播放做一些附加操作,比如记录播放日志、播放时长等,都可以统一在PlayerManager
中实现。
为了进一步符合单一设计原则
像播放列表
之类的我都抽成了单独的类去管理。完整代码可至package com.zs.zs_jetpack.play(包名)
下参考
4. 状态分发
基于上面的设计,我们如何在具体的界面做UI渲染与交互呢?
首先来尝试第一种方案:
- 进入到具体的
Activity/Fragment
,通过PlayerManager
获取到播放信息与状态,填充到具体的View
- 当播放状态、信息改变时如点击了暂停按钮,通过
PlayerManager
暂停音频,然后手动将对应的View
置为暂停状态
关于上述方案,如果只有播放/暂停
一个按钮没啥问题,大胆去使用吧。但实际上我们面临的时多界面中的多操作,加一块有一二十个,而且不同界面的状态信息也必须保持一致,还按照这种方式去写,相信我,你会欲死欲仙
的。
关于这种方案,我的答案是:弃之
第二种方案
第一种方案面临的问题是:状态
与UI
容易产生一致性问题。那么我们能不能做一种设计,播放让状态去驱动
UI?
何为状态驱动UI:
顾名思义,就是播放状态改变后第一时间通知到视图层,视图层只做
UI
渲染。在此背景下我又想到了另一种设计模式观察者模式
,只需将视图层定义为观察者,随后与被观察者PlayerManager
进行绑定,统一由PlayerManager
下发播放信息,视图层拿到状态第一时间对UI
进行渲染。
捋清了思路我们来做代码上的设计
首先定义一个观察者接口:
interface AudioObserver {
/**
* 歌曲信息
* 空实现,部分界面可不用实现
*/
fun onAudioBean(audioBean: AudioBean){}
/**
* 播放状态,目前有四种。可根据类型进行扩展
* release
* start
* resume
* pause
*
* 空实现,部分界面可不用实现
*/
fun onPlayStatus(playStatus:Int){}
/**
* 当前播放进度
* 空实现,部分界面可不用实现
*/
fun onProgress(currentDuration: Int,totalDuration:Int){}
/**
* 播放模式
*/
fun onPlayMode(playMode:Int)
}
实现了该接口就可被视为观察者
被观察者是PlayerManager
,当内部状态发生改变时统一通知到观察者对象。关于管理观察者的代码大概是这样的:
class PlayerManager private constructor(){
/**
* 音乐观察者集合,目前有三个
* 1.播放界面
* 2.悬浮窗
* 3.通知栏
*/
private val observers = mutableListOf()
private fun resume() {
...
playerHelper.resume()
//状态改变,通知观察者
sendPlayStatusToObserver()
}
private fun pause() {
...
playerHelper.pause()
//状态改变,通知观察者
sendPlayStatusToObserver()
}
/**
* 给观察者发送播放状态
*/
private fun sendPlayStatusToObserver() {
observers.forEach {
it.onPlayStatus(playStatus)
}
}
/**
* 注册观察者
*/
fun register(audioObserver: AudioObserver) {
observers.add(audioObserver)
//TODO 注册时手动更新观察者,相当于粘性通知
notifyObserver(audioObserver)
}
/**
* 解除观察者
*/
fun unregister(audioObserver: AudioObserver) {
observers.remove(audioObserver)
}
/**
* 手动更新观察者
*/
private fun notifyObserver(audioObserver: AudioObserver) {
...
...
}
}
有了上面的骨架代码,将Notification
、播放页
、首页悬浮
实现AudioObserver
接口并且与PlayerManager
进行绑定,在对应的方法中做视图渲染,这样就可以实现状态驱动UI
,解决了状态
与UI
的一致性问题。同时可以基于此模式在任意处做播放信息的视图展示,对扩展开放,修改关闭,无处不在的开闭原则。
数据绑定
关于这个项目我使用到了Jetpack
中的DataBinding
,将状态数据
与View
进行绑定,当几个观察者观察到PlayerManager
状态改变时,由DataBinding
自动渲染到View
中。
到此我们就实现了一个真正的状态驱动UI
,从状态分发到UI渲染之间没有任何多余操作,一气呵成~~
关于PlayerManager
和AudioObserver
代码在package com.zs.zs_jetpack.play(包名)
目录下
综上所述
- 通过基于
IPlayer
接口编程,将功能组件与业务做隔离 - 通过单例实现
PlayerManager
使播放信息在进程中共享 -
PlayerManager
实则也是一个代理类,将播放逻辑与具体业务进行隔离,并且在内部统一管理了观察者
和播放列表
- 基于观察者模式,解决了
状态
与UI
的一致性问题 - 最后由
DataBinding
将状态数据
与View
进行绑定,真正的实现状态
驱动UI
最后附上github地址:
https://github.com/zskingking/Jetpack-WanAndroid
关于播发模块的代码在项目中的路径,文章中已经给出,请仔细阅读。