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

栏目: Android · 发布时间: 5年前

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

补一补前面偷懒的博客(1/4)

只是个人总结的文章不小心被你找到啦~

如果感兴趣的话项目地址在文末

1. What is that?

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

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

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

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

顺序就是按照①②③④⑤来进行

:one:在View中,我们向Presenter发送一次更新TextView的请求

:two:Presenter收到请求后再向对应的Model发送获取String的请求(中间可能有耗时操作,所以可能需要回调接口)

:three:成功拿到数据后再通过回调接口给Presenter

:four:Presenter拿到数据后再触发View的回调

:five:最后完成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。仅仅分析播放页面( 因为我就做了两个页面:sob: )

主页 播放页
MVP模式实战(音乐APP-Android-Kotlin)
MVP模式实战(音乐APP-Android-Kotlin)

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

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

我这里只展开了一些重要的部分,一些网络请求、自定义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
changeLyricPosition
changNowTimeTextView
changeSeekBarPosition

interface DetailModel 中定义了两个方法

getNowMusic
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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

终极算法

终极算法

[美] 佩德罗·多明戈斯 / 黄芳萍 / 中信出版集团 / 2017-1-1 / 68.00元

算法已在多大程度上影响我们的生活? 购物网站用算法来为你推荐商品,点评网站用算法来帮你选择餐馆,GPS系统用算法来帮你选择最佳路线,公司用算法来选择求职者…… 当机器最终学会如何学习时,将会发生什么? 不同于传统算法,现在悄然主导我们生活的是“能够学习的机器”,它们通过学习我们琐碎的数据,来执行任务;它们甚至在我们还没提出要求,就能完成我们想做的事。 什么是终极算法? ......一起来看看 《终极算法》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具