美文网首页我也码
一个音乐播放器的设计流程,含完整实例

一个音乐播放器的设计流程,含完整实例

作者: zskingking | 来源:发表于2020-07-15 15:55 被阅读0次

    前言

    上篇文章给大家分享了我的WanAndroid项目,简要描述了一下项目的整体结构。由于作者非常喜欢听音乐,鉴于对音乐的情怀,就在一个技术社区App中强行加了一个音乐播放功能。关于这个播放器的设计,我也花了很多心思,应用了很多设计模式,今天就逐个给大家进行分型。

    贴一下上篇文章的链接:https://juejin.im/post/5f09ac336fb9a07e93303162

    先细心阅读,文末会给出github链接

    1. 需求背景

    设计一个音乐播放功能,包含一个播放界面,包含播放/暂停、上一首、下一首、播放列表、播放模式、进度更新等,返回到首页又一个悬浮窗包含播放状态、音乐信息,大概就长下面这样:

    image

    当前这是第一版,功能比较简单,后面我会持续完善。

    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<AudioObserver>()
        
        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渲染之间没有任何多余操作,一气呵成~~

    关于PlayerManagerAudioObserver代码在package com.zs.zs_jetpack.play(包名)目录下

    综上所述

    • 通过基于IPlayer接口编程,将功能组件与业务做隔离
    • 通过单例实现PlayerManager使播放信息在进程中共享
    • PlayerManager实则也是一个代理类,将播放逻辑与具体业务进行隔离,并且在内部统一管理了观察者播放列表
    • 基于观察者模式,解决了状态UI的一致性问题
    • 最后由DataBinding状态数据View进行绑定,真正的实现状态驱动UI

    最后附上github地址:https://github.com/zskingking/Jetpack-WanAndroid

    关于播发模块的代码在项目中的路径,文章中已经给出,请仔细阅读。

    相关文章

      网友评论

        本文标题:一个音乐播放器的设计流程,含完整实例

        本文链接:https://www.haomeiwen.com/subject/oeqjhktx.html