MVP模式實戰(音樂APP-Android-Kotlin)

補一補前面偷懶的博客(1/4)
只是我的總結的文章不當心被你找到啦~
若是感興趣的話項目地址在文末java

1. What is that?

MVP是一種設計模式(框架),由於其出色的解耦功能普遍地用於Android工程中,它將應用程序分爲Model-View-Presenter,各司其職,簡稱MVPgit

  • Model(模型) 負責對數據的處理和存儲,將數據回調給Presenter
  • Presenter(主持者) 負責將View層的請求(如點擊,更新視圖數據)進行轉發給對應的Model,接受回調後再通知View層更新視圖
  • View(視圖) 僅負責將顯示數據
  • Contract(契約類) 僅僅用於定義View和Model的接口,便於管理和查看

一次簡單的更新視圖的基本流程 github

一次簡單的更新視圖的基本流程
順序就是按照①②③④⑤來進行
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:ide

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🤣

相關文章
相關標籤/搜索