MVP模式实战(音乐APP-Android-Kotlin)

3,420 阅读11分钟

补一补前面偷懒的博客(1/4)
只是个人总结的文章不小心被你找到啦~
如果感兴趣的话项目地址在文末

1. What is that?

MVP是一种设计模式(框架),因为其出色的解耦功能广泛地用于Android工程中,它将应用程序分为Model-View-Presenter,各司其职,简称MVP

  • Model(模型) 负责对数据的处理和存储,将数据回调给Presenter
  • Presenter(主持者) 负责将View层的请求(如点击,更新视图数据)进行转发给对应的Model,接受回调后再通知View层更新视图
  • View(视图) 仅负责将显示数据
  • Contract(契约类) 仅仅用于定义View和Model的接口,便于管理和查看

一次简单的更新视图的基本流程

一次简单的更新视图的基本流程
顺序就是按照①②③④⑤来进行
1️⃣在View中,我们向Presenter发送一次更新TextView的请求
2️⃣Presenter收到请求后再向对应的Model发送获取String的请求(中间可能有耗时操作,所以可能需要回调接口)
3️⃣成功拿到数据后再通过回调接口给Presenter
4️⃣Presenter拿到数据后再触发View的回调
5️⃣最后完成View的视图更新。
自始至终,View做的事情只有处理用户的请求(更新TextView)并发送给Presenter,然后提供一个用来更新视图的回调;Presenter做的事情只有转发,自己本身不处理逻辑;model负责提供信息,同时包括数据的处理。

有的版本的MVP可能选择将数据处理放入Presenter中,然后model只有一个setter/getter的类似JavaBean的作用,但是我觉得这样处理使得Presenter变得很臃肿,所以我选择将逻辑处理放入Model。两种方式都可以√

2. MVP通用框架

2.1 Contract层

Contract并没有什么很通用个框架,因为每个视图和每一个model的工作各不相同,这里给出的是上图中的的范例

class DetailContract{
    interface DetailView{
        fun onChangeText(String)
    }

    interface DetailModel {
        fun getNowText(callBack: GetTextCallBack)
    }
    interface GetTextCallBack{
        fun onSuccess(str:String)
        fun onFail(info:String)
    }
}

2.2 Model层

Model也没有什么很通用的框架,这里给出的是上图中的范例

class SampleModel: DetailContract.DetailModel{
    override getNowText(callBack: GetTextCallBack){
        val str = ...
        //以上是获取String的操作
        if(str!=""){
            callBack.onSuccess(str)   
        }else{
            callBake.onFail("获取失败")
        }
    }
}

这里的具体Model类实现了Contract契约类中的接口,方便我们Presenter进行调用

2.3 View层

View在Android中一般包括两种,一种是Activity,一种是Fragment,这里只给出Activity的封装,Fragment类似,需要处理一些生命周期的问题。

Activity:

abstract class BaseActivity<V,T:BasePresenter<V>>:Activity(){

    val TAG:String = javaClass.simpleName

    protected lateinit var mPresenter: T

    lateinit var mContext: Context

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mContext = this
        //初始化Presenter
        mPresenter = createPresenter()
        //将Presenter和View绑定
        mPresenter.attachView(this as V)
        //初始化布局
        initView(savedInstanceState)
    }

    /**
     * 应该由子类进行实现的初始化view的方法
     */
    abstract fun initView(savedInstanceState: Bundle?)

    /**
     * 创建对应的Presenter
     */
    abstract fun createPresenter():T

    //解除绑定
    override fun onDestroy() {
        super.onDestroy()
        mPresenter.detachView()
    }
}

BaseActivity是一个抽象类,所有加入MVP模式的Activity都应该继承这个抽象类。 泛型V代表的是视图(即自己),T则是对应的Presenter。View层持有对应Presenter的引用,用来发送消息。

2.4 Presenter层

abstract class BasePresenter<T> {
    //View接口类型的弱引用,防止所持有的view已经被销毁,但是该presenter仍然持有,导致内存的泄露
    protected lateinit var mViewRef:Reference<T>

    //绑定View引用
    fun attachView(view:T){
        mViewRef = SoftReference<T>(view)
    }

    //获取当前绑定的View引用
    protected fun getView(): T? {
        return mViewRef.get()
    }

    //是否已绑定View
    fun isViewAttached(): Boolean {
        return mViewRef != null&&mViewRef.get()!=null
    }

    //解除引用
    fun detachView(){
        if (mViewRef != null){
            mViewRef.clear()
        }
    }
}

BasePresenter是一个抽象类,所有加入MVP模式的Presenter都应该继承该抽象类。 Presenter持有View层的一个弱引用,同时包括4个和弱引用有关的方法,分别是绑定View的引用,获取当前View的引用,判定是否已绑定了View,解除View的引用。
在具体的Presenter中还拥有一个对应Model对象。也就是Presenter同时持有View和Model,这样才可以做到信息的转发功能

传入的是Contract中的View接口类型是因为可以使得Presenter只通过接口向view传输传输信息。而不是一个具体的类型。

以上就是一些常用的框架,下面我们用实战来继续加深理解:

3. 实战

该范例选自红岩移动开发部的中期考核,内容为一个音乐App。仅仅分析播放页面(因为我就做了两个页面😭)

主页 播放页

主要功能就是播放播放音乐以及歌词的滚动 我们先来看看结构:

我这里只展开了一些重要的部分,一些网络请求、自定义view相关的就不涉及。

1. Contract层

我觉得首先应该编写的是这一层,它用来规范我们View和Model的具体行为: DetailMusicContract:

class DetailMusicContract{
    interface DetailView{
        fun showDetailMusic(name:String,author:String,imageUrl:String)
        fun showLyric(viewList:ArrayList<View>)
        fun showToast(message:String)
        fun changeLyricPosition(position:Int)
        fun changeNowTimeTextView(time:String)
        fun changeSeekBarPosition(position:Int)
    }

    interface DetailModel {
        fun getNowMusic(callBack: GetNowMusicCallBack)
        fun getLyric(context:Context,callBack: GetLyricCallBack)
    }
    interface GetNowMusicCallBack{
        fun onSuccess(music: MyMusic)
        fun onFail(info:String)
    }
    interface GetLyricCallBack{
        fun onSuccess(viewList: ArrayList<View>)
        fun onFail(info:String)
    }
}

interface DetailView中定义了6个方法

  • showDetailMuisc用来显示当前歌曲的名字,作者,以及图片
  • showLyric用来显示歌词(初始化ViewPager和Adapter)
  • changeLyricPosition用来更改当前歌词的位置(即实现歌词轮播)
  • changNowTimeTextView用来更改当前歌曲的播放时间
  • changeSeekBarPosition用来更变滑动条的进度

interface DetailModel中定义了两个方法

  • getNowMusic用于从管理音乐播放的Service(服务)中获取当前播放的音乐
  • getLyric用于获得当前播放的放音乐的歌词

Tips:很多情况下,Model的方法是后面才加的,以为你可能一开始不知道Model需要哪些方法

2. View层

BaseActivity之前已经展示过,实际上的BaseActivity会增加一些关于服务绑定的东西,不在本篇范畴之内
DetailMusicActivity:

class DetailMusicActivity : BaseActivity<DetailMusicContract.DetailView,
        DetailMusicPresenter>(),
        DetailMusicContract.DetailView,
        MyMusicPlayerManager.OnStartPlay,
        MyMusicPlayerManager.StartNextMusic,
        View.OnClickListener{
    override fun initView(savedInstanceState: Bundle?) {
        setContentView(R.layout.activity_detail)
        iv_detail_play.setOnClickListener(this)
        iv_detail_previous.setOnClickListener(this)
        iv_detail_next.setOnClickListener(this)
        iv_detail_back.setOnClickListener(this)
        //音乐准备完毕的回调
        MyMusicPlayerManager.instance.setOnStartPlay(this)
        MyMusicPlayerManager.instance.setStartNextMusic(this)
    }

    //实现绑定成功后的音乐数据
    override fun onService(name: ComponentName?, service: IBinder?) {
        Toast.makeText(this,"绑定成功",Toast.LENGTH_SHORT).show()
        changeNowMusic()
    }

    override fun createPresenter(): DetailMusicPresenter {
        return DetailMusicPresenter()
    }

    override fun showDetailMusic(name: String, author: String, imageUrl: String) {
        tv_detail_name.text = name
        tv_detail_author.text = author
        ImageLoader.with(this)
                .from(imageUrl)
                .disposeWith(CutToCircle())
                .cacheWith(DoubleCacheUtils.getInstance())
                .into(iv_detail_music)
    }

    //改变音乐的时候必要操作,注意,这里可以进行一些歌词还没有获取但是已经可以进行的操作
    override fun changeNowMusic() {
        Log.d("刷新音乐","")
        mPresenter.getNowMusic()
        mPresenter.getLyric(this)
        mPresenter.startToChangeTextView()
        mPresenter.startToChangeSeekBar()
        sb_detail.max = MyMusicPlayerManager.instance.musicDuration()
        sb_detail.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener{
            var isTouch = false
            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
                if (isTouch){
                    val position = seekBar!!.progress
                    MyMusicPlayerManager.instance.musicSeekTo(position)
                    mPresenter.pause()
                }
            }

            override fun onStartTrackingTouch(seekBar: SeekBar?) {
                isTouch = true
            }

            override fun onStopTrackingTouch(seekBar: SeekBar?) {
                isTouch = false
                MyMusicPlayerManager.instance.play()
            }

        })
    }
    //触发显示歌词的回调,注意,这里应该放只有获取到了歌词之后才可以做出的ui操作
    override fun showLyric(viewList:ArrayList<View>) {
        Log.d("歌词显示回调","成功")
        runOnUiThread {
            val adapter = MyViewPagerAdapter(viewList)
            mb_lyric.init()
            mb_lyric.setScrollTime(1500)
            mb_lyric.setAdapter(adapter,this)
            mb_lyric.setTransformer(CustomTransformer())
            mPresenter.startToChangeLyric()
        }
    }

    override fun onNextMusic() {
        mPresenter.playNext()
    }

    override fun showToast(message:String) {
        Toast.makeText(this,message,Toast.LENGTH_SHORT).show()
    }

    override fun changeLyricPosition(position: Int) {
        runOnUiThread {
            mb_lyric.changeTo(position)
        }
    }

    override fun changeNowTimeTextView(time: String) {
        runOnUiThread{
            tv_detail_now.text = time
        }
    }

    override fun changeSeekBarPosition(position: Int) {
        runOnUiThread {
            sb_detail.progress = position
        }
    }

    //点击事件的集中处理
    override fun onClick(v: View?) {
        when{
            v!!.id == iv_detail_play.id -> {
                if (MyMusicPlayerManager.instance.isPlaying()){
                    mPresenter.pause()
                }else{
                    mPresenter.play()
                }
            }
            v.id == iv_detail_previous.id -> {
                mPresenter.playPrevious()
            }
            v.id == iv_detail_next.id -> {
                mPresenter.playNext()
            }
            v.id == iv_detail_back.id -> {
                this.finish()
            }
        }
    }

    /**
     * 生命周期相关
     */

    override fun onDestroy() {
        super.onDestroy()
        mPresenter.cancelTimer()
    }
}

看起来代码可能有点长,原因是这个页面相对来说有点复杂,但是整个View的结构很清晰。

  • override fun initView
    这个是继承自BaseActivity里的方法,用于首次启动后的初始化布局,可以看到我们在这里设置了一些控件的监听以及回调接口。需要解释的是MyMusicPlayerManager.instance.setOnStartPlay(this) MyMusicPlayerManager.instance.setStartNextMusic(this) 这两个方法,由于音乐播放器需要从网络上异步加载音乐播放数据,所以需要设置一个音乐准备播放的回调接口以及播放完毕后切换到下一首的回调接口他们对应的方法为
    override fun changeNowMusic()当前的音乐发生改变时(上一首下一首)的触发的回调 override fun onNextMusic()当播放完毕后切换到下一首的回调接口
  • override fun onService
    这个是继承自BaseActivity里的方法(上文中的BaseActivity并没有加入,但是因为这个是一个音乐App,所以需要和当前Activity进行绑定)在这里我们进行的是绑定操作完毕后的操作:执行changeNowMusic()来进行音乐界面的初始化操作
  • override fun createPresenter
    继承自BaseActivity的方法,创建一个对应的Presenter实例
  • override fun showDetailMusic
    这个是在Contract接口中定义的方法,用于显示一些控件的值
  • override fun changeNowMusic
    MyMusicPlayerManager.OnStartPlay接口中定义的方法,具体内容为显示当前正在播放的歌曲的所有信息,里面向Presenter发送了四个消息,getNowMusic用于请求显示当前的音乐、getLyric用于请求歌词、startToChangeTextView用于请求开始不断更新当前播放时间、startToChangeSeekBar用于请求开始不断更新SeekBar的进度。之后就是设置一些SeekBar的监听来实现对音乐进度的控制
  • override fun showLyric
    这个是在Contract接口中定义的方法,用来显示歌词,在最后的时候还想Presenter发送了请求开始滚动歌词界面的消息
  • override fun onNextMusic
    这个是定义在MyMusicPlayerManager.StartNextMusic接口中的方法,上文已经提及,在音乐自动播放完后出发的回调,即播放下一首歌 接下来的一些方法就不用多说了,showToast、changeLyricPosition、changeNowTimeTextView、changeSeekBarPosition、还有集中处理的onClick控件点击监听

View层总结
说了这么多,实际上View层的作用简而言之就是 输入 输出
根据用户的操作向Presenter发送请求、提供各式各样的接口来给Presenter和音乐服务进行回调

3. Presenter层

DetailMusicPresenter:

class DetailMusicPresenter : BasePresenter<DetailMusicContract.DetailView>(){
    private var lyricTimer:Timer = Timer()
    private var textViewTimer:Timer = Timer()
    private var seekBarTimer:Timer = Timer()
    private val detailMusicModel = DetailMusicModel()
    //获取目前播放的音乐的回调
    fun getNowMusic(){
        detailMusicModel.getNowMusic(object :DetailMusicContract.GetNowMusicCallBack{
            override fun onSuccess(music: MyMusic) {
                mViewRef.get()!!.showDetailMusic(music.name,music.author,music.imageUrl)
            }

            override fun onFail(info: String) {
                mViewRef.get()!!.showToast(info)
            }
        })
    }

    fun getLyric(context: Context){
        detailMusicModel.getLyric(context,object :DetailMusicContract.GetLyricCallBack{
            override fun onSuccess(viewList:ArrayList<View>) {
                mViewRef.get()!!.showLyric(viewList)
            }

            override fun onFail(info: String) {
                mViewRef.get()!!.showToast(info)
            }
        })
    }

    fun startToChangeLyric(){
        lyricTimer = Timer()
        lyricTimer.schedule(object : TimerTask() {
            override fun run() {
                mViewRef.get()!!.changeLyricPosition(MyMusicPlayerManager.instance.getNowLyricPosition())
            }
        }
        ,0,100)
    }

    fun startToChangeTextView(){
        textViewTimer = Timer()
        textViewTimer.schedule(object : TimerTask(){
            override fun run() {
                mViewRef.get()!!.changeNowTimeTextView(MyMusicPlayerManager.instance.nowTimeInMin())
            }
        },0,100)
    }

    fun startToChangeSeekBar(){
        seekBarTimer = Timer()
        seekBarTimer.schedule(object :TimerTask(){
            override fun run() {
                mViewRef.get()!!.changeSeekBarPosition(MyMusicPlayerManager.instance.musicCurrent())
            }
        },0,100)
    }

    //音乐控制
    fun play(){
        MyMusicPlayerManager.instance.play()
    }
    fun pause(){
        MyMusicPlayerManager.instance.pause()
    }
    fun playPrevious(){
        cancelTimer()
        MyMusicPlayerManager.instance.playPrevious()
    }
    fun playNext(){
        cancelTimer()
        MyMusicPlayerManager.instance.playNext()
    }
    fun cancelTimer(){
        lyricTimer.cancel()
        textViewTimer.cancel()
        seekBarTimer.cancel()
    }
}

因为Presenter的功能就是转发,所以代码不长,而且结构清晰

  • 首先拥有一个DetailModel实例,不需要太多阐述
  • getNowMusic
    这个是在View层中调用的,用来获得当前音乐的信息,代码并不难理解,向Model请求获得当前的音乐,成功的话就通过Presenter拥有的View层的引用来调用之前已经在Contract中定义好的showDetailMusic方法来通知View层更新,如果失败那就调用之前也在Contract中定义好的showToast方法来显示Toast信息提醒用户
  • fun getLyric
    这个是也是在View层中调用的,用来获取当前音乐的歌词,成功则回调View层的接口来显示歌词,否则显示Toast
  • fun startToChangeLyricfun startToChangeTextViewfun startToChangeSeekBar 同样是在View层中定义的,实现方式几乎一致,开启一个新的TimerTask,定时获取当前歌词应该在的position、当前已经播放时间、当前SeekBar应该在的进度,然后回调View层对应的方法来更新
  • 音乐控制不必多说,不过注意上一首下一首时需要先取消Timer,否则会出现报错
  • fun cancelTimer
    用来取消Timer,当换歌、View销毁时,由于Timer在子线程中执行的,会导致Lyric没有初始化,或者nullPoint报错

4. Model层

DetailMusic:

class DetailMusicModel: DetailMusicContract.DetailModel {
    override fun getNowMusic(callBack: DetailMusicContract.GetNowMusicCallBack){
        val music = MyMusicPlayerManager.instance.nowMusic()
        callBack.onSuccess(music)
    }

    override fun getLyric(context:Context,callBack: DetailMusicContract.GetLyricCallBack) {
        val music = MyMusicPlayerManager.instance.nowMusic()
        val request = Request.Builder("http://elf.egos.hosigus.com/music/lyric?id=${music.id}")
                .setMethod("GET").build()
        NetUtil.getInstance().execute(request,object :Callback{
            override fun onResponse(response: String?) {
                val mainJson = JSONObject(response)
                val str = mainJson.getJSONObject("lrc").getString("lyric")
                val lyric = Lyric(str!!)
                MyMusicPlayerManager.instance.nowMusic().lyric = lyric
                val viewList = ArrayList<View>()
                for (i in 0 until lyric.arrayList.size){
                    val view = LayoutInflater.from(context).inflate(R.layout.item_lyric,null)
                    view.findViewById<TextView>(R.id.tv_item_lyric).text=lyric.arrayList[i]
                    viewList.add(view)
                }
                callBack.onSuccess(viewList)
            }
            override fun onFailed(t: Throwable?) {

            }
        })
    }
}

Model类主要用来收集数据并提供给Presenter

  • override fun getNowMusic
    获取当前的音乐并回调到Presenter,而Presenter收到回调后会通知View层进行更新
  • override fun getLyric
    获取当前的歌词并打包成ArrayList回调给Presenter,而Presenter收到回调后会通知给View层进行更新

总结

本文只针对MVP的结构进行了分析,一些其他的内容,如音乐播放器,自定义歌词View等并没有涉及,如果感兴趣的话可以访问GitHub源码地址

Other

抱歉拉低了掘金的文章质量。。。
本人技术有限,仍然在学习当中,如果有什么不对的地方希望大佬们指正!!!
写的初衷也只是来总结罢了,并没有想过会有多少人看hhhhhh🤣