1. 程式人生 > 其它 >Android Jetpack之MVVM使用及封裝

Android Jetpack之MVVM使用及封裝

Android開發架構

如果開發過程中大家各自為戰,沒有統一規範,久而久之,專案程式碼會變得混亂且後續難以維護。當使用統一的架構模式後,有很多的好處,如:

  • 統一開發規範,使得程式碼整潔、規範,後續易於維護及擴充套件
  • 提高開發效率(尤其在團隊人員較多時)
  • 模組單一職責,使得模組專注自己內部(面向物件),模組間解耦

總之,開發架構是前人總結出來的一套行之有效的開發模式,目的是達到高內聚,低耦合的效果,使得專案程式碼更健壯、易維護。

Android中常見的架構模式有MVC(Model-View-Controller)MVP(Model-View-Presenter)MVVM(Model-View-ViewModel)

,一起來看下各自的特點:

MVC

MVC(Model-View-Controller)是比較早期的架構模式,模式整體也比較簡單。

MVC模式將程式分成了三個部分:

  • Model模型層:業務相關的資料(如網路請求資料、本地資料庫資料等)及其對資料的處理
  • View檢視層:頁面檢視(通過XML佈局編寫檢視層),負責接收使用者輸入、發起資料請求及展示結果頁面
  • Controller控制器層:M與V之間的橋樑,負責業務邏輯

MVC特點:

  • 簡單易用:上圖表述了資料整個流程:View接收使用者操作,通過Controller去處理業務邏輯,並通過Model去獲取/更新資料,然後Model層又將最新的資料傳回View
    層進行頁面展示。
  • 架構簡單的另一面往往是對應的副作用:由於XML佈局能力弱,我們的View層的很多操作都是寫在Activity/Fragment中,同時,Controller、Model層的程式碼也大都寫在Activity/Fragment中,這就會導致一個問題,當業務邏輯比較複雜時,Activity/Fragment中的程式碼量會很大,其違背了類單一職責,不利於後續擴充套件及維護。尤其是後期你剛接手的專案,一個Activity/Fragment類中的程式碼動輒上千行程式碼,那感覺著實酸爽:

當然,如果業務很簡單,使用MVC模式還是一種不錯的選擇。

MVP

MVP(Model-View-Presenter)

,架構圖如下:

MVP各模組職責如下

  • Model模型:業務相關的資料(如網路請求資料、本地資料庫資料等)及其對資料的處理
  • View檢視:頁面檢視(Activity/Fragment),負責接收使用者輸入、發起資料請求及展示結果頁面
  • Presenter:M與V之間的橋樑,負責業務邏輯

MVP特點View層接收使用者操作,並通過持有的Presenter去處理業務邏輯,請求資料;接著Presenter層通過Model去獲取資料,然後Model又將最新的資料傳回Presenter層,Presenter層又持有View層的引用,進而將資料傳給View層進行展示。

MVP相比MVC的幾處變化

  • View層與Model層不再互動,而是通過Presenter去進行聯絡
  • 本質上MVP是面向介面程式設計,Model/View/Presenter每層的職責分工明確,當業務複雜時,整個流程邏輯也是很清晰的

當然,MVP也不是十全十美的,MVP本身也存在以下問題:

  • View層會抽象成IView介面,並在IView中宣告一些列View相關的方法;同樣的,Presenter會被抽象成IPresenter介面及其一些列方法,每當實現一個功能時,都需要編寫多個介面及其對應的方法,實現起來相對比較繁瑣,而且每次有改動時,對應的介面方法也基本都會再去改動。
  • View層與Presenter層相互持有,當View層關閉時,由於Presenter層不是生命週期感知的,可能會導致記憶體洩漏甚至是崩潰。

ps:如果你的專案中使用了RxJava,可以使用 AutoDispose 自動解綁。

MVVM

MVVM(Model-View-ViewModel),架構圖如下:

MVVM各職責如下

  • Model模型:業務相關的資料(如網路請求資料、本地資料庫資料等)及其對資料的處理
  • View檢視:頁面檢視(Activity/Fragment),負責接收使用者輸入、發起資料請求及展示結果頁面
  • ViewModel:M與V之間的橋樑,負責業務邏輯

MVVM特點

  • View層接收使用者操作,並通過持有的ViewModel去處理業務邏輯,請求資料;
  • ViewModel層通過Model去獲取資料,然後Model又將最新的資料傳回ViewModel層,到這裡,ViewModel與Presenter所做的事基本是一樣的。但是ViewModel不會也不能持有View層的引用,而是View層會通過觀察者模式監聽ViewModel層的資料變化,當有新資料時,View層能自動收到新資料並重新整理介面。

UI驅動 vs 資料驅動

MVP中,Presenter中需要持有View層的引用,當資料變化時,需要主動呼叫View層對應的方法將資料傳過去並進行UI重新整理,這種可以認為是UI驅動;而MVVM中,ViewModel並不會持有View層的引用,View層會監聽資料變化,當ViewModel中有資料更新時,View層能直接拿到新資料並完成UI更新,這種可以認為是資料驅動,顯然,MVVM相比於MVP來說更加解耦。

MVVM的具體實現

上面介紹了MVC/MVP/MVVM的各自特點,其中MVC/MVP的具體使用方式,本文不再展開實現,接下來主要聊一下MVVM的使用及封裝,MVVM也是官方推薦的架構模式。

Jetpack MVVM

Jetpack是官方推出的一系列元件庫,使用元件庫開發有很多好處,如:

  • 遵循最佳做法:採用最新的設計方法構建,具有向後相容性,可以減少崩潰和記憶體洩漏
  • 消除樣板程式碼:開發者可以更好地專注業務邏輯
  • 減少不一致:可以在各種Android版本中執行,相容性更好。

為了實現上面的MVVM架構模式,Jetpack提供了多個元件來實現,具體來說有Lifecycle、LiveData、ViewModel(這裡的ViewModel是MVVM中ViewModel層的具體實現),其中Lifecycle負責生命週期相關;LiveData賦予類可觀察,同時還是生命週期感知的(內部使用了Lifecycle);ViewModel旨在以注重生命週期的方式儲存和管理介面相關的資料,針對這幾個庫的詳細介紹及使用方式不再展開,有興趣的可以參見前面的文章:

通過這幾個庫,就可以實現MVVM了,官方也釋出了MVVM的架構圖:

其中Activity/FragmentView層,ViewModel+LiveDataViewModel層,為了統一管理網路資料及本地資料資料,又引入了Repository中間管理層,本質上是為了更好地管理資料,為了簡單把他們統稱為Model層吧。

使用舉例

  • View層程式碼:
//MvvmExampleActivity.kt
class MvvmExampleActivity : BaseActivity() {

    private val mTvContent: TextView by id(R.id.tv_content)
    private val mBtnQuest: Button by id(R.id.btn_request)
    private val mToolBar: Toolbar by id(R.id.toolbar)

    override fun getLayoutId(): Int {
        return R.layout.activity_wan_android
    }

    override fun initViews() {
        initToolBar(mToolBar, "Jetpack MVVM", true)
    }

    override fun init() {
        //獲取ViewModel例項,注意這裡不能直接new,因為ViewModel的生命週期比Activity長
        mViewModel = ViewModelProvider(this).get(WanViewModel::class.java)

        mBtnQuest.setOnClickListener {
            //請求資料
            mViewModel.getWanInfo()
        }

        //ViewModel中的LiveData註冊觀察者並監聽資料變化
        mViewModel.mWanLiveData.observe(this) { list ->
            val builder = StringBuilder()
            for (index in list.indices) {
                //每條資料進行折行顯示
                if (index != list.size - 1) {
                    builder.append(list[index])
                    builder.append("\n\n")
                } else {
                    builder.append(list[index])
                }
            }
            mTvContent.text = builder.toString()
        }
    }
}
  • ViewModel層程式碼:
//WanViewModel.kt
class WanViewModel : ViewModel() {
    //LiveData
    val mWanLiveData = MutableLiveData<List<WanModel>>()
    //loading
    val loadingLiveData = SingleLiveData<Boolean>()
    //異常
    val errorLiveData = SingleLiveData<String>()

    //Repository中間層 管理所有資料來源 包括本地的及網路的
    private val mWanRepo = WanRepository()

    fun getWanInfo(wanId: String = "") {
        //展示Loading
        loadingLiveData.postValue(true)
        viewModelScope.launch(Dispatchers.IO) {
            try {
                val result = mWanRepo.requestWanData(wanId)
                when (result.state) {
                    State.Success -> mWanLiveData.postValue(result.data)
                    State.Error -> errorLiveData.postValue(result.msg)
                }
            } catch (e: Exception) {
                error(e.message ?: "")
            } finally {
                loadingLiveData.postValue(false)
            }
        }
    }
}
  • Repository層(Model層)程式碼:
class WanRepository {

    //請求網路資料
    suspend fun requestWanData(drinkId: String): BaseData<List<WanModel>> {
        val service = RetrofitUtil.getService(DrinkService::class.java)

        val baseData = service.getBanner()
        if (baseData.code == 0) {
            //正確
            baseData.state = State.Success
        } else {
            //錯誤
            baseData.state = State.Error
        }
        return baseData
    }
}

這裡只通過Retrofit請求了網路資料 玩Android 開放API,如果需要新增本地資料,只需要在方法裡新增本地資料處理即可,即 Repository是資料的管理中間層,對資料進行統一管理,ViewModel層中不需要關心資料的來源,大家各司其職即可,符合單一職責,程式碼可讀性更好,同時也更加解耦。在View層點選按鈕請求資料,執行結果如下:

以上就完成了一次網路請求,相比於MVPMVVM既不用宣告多個介面及方法,同時ViewModel也不會像Presenter那樣去持有View層的引用,而是生命週期感知的,MVVM方式更加解耦。

封裝

上一節介紹了Jetpack MVVM的使用例子,可以看到有一些程式碼邏輯是可以抽離出來封裝到公共部分的,那麼本節就嘗試對其做一次封裝。

首先,請求資料時可能會展示Loading,請求完後可能是空資料、錯誤資料,對應下面的IStatusView介面宣告:

interface IStatusView {
    fun showEmptyView() //空檢視
    fun showErrorView(errMsg: String) //錯誤檢視
    fun showLoadingView(isShow: Boolean) //展示Loading檢視
}

因為ViewModel是在Activity中初始化的,所以可以封裝成一個Base類:

abstract class BaseMvvmActivity<VM : BaseViewModel> : BaseActivity(), IStatusView {

    protected lateinit var mViewModel: VM
    protected lateinit var mView: View
    private lateinit var mLoadingDialog: LoadingDialog

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mLoadingDialog = LoadingDialog(this, false)
        mViewModel = getViewModel()!!
        init()
        registerEvent()
    }

    /**
     * 獲取ViewModel 子類可以複寫,自行初始化
     */
    protected open fun getViewModel(): VM? {
        //當前物件超類的Type
        val type = javaClass.genericSuperclass
        //ParameterizedType表示引數化的型別
        if (type != null && type is ParameterizedType) {
            //返回此型別實際型別引數的Type物件陣列
            val actualTypeArguments = type.actualTypeArguments
            val tClass = actualTypeArguments[0]
            return ViewModelProvider(this).get(tClass as Class<VM>)
        }
        return null
    }

    override fun showLoadingView(isShow: Boolean) {
        if (isShow) {
            mLoadingDialog.showDialog(this, false)
        } else {
            mLoadingDialog.dismissDialog()
        }
    }

    override fun showEmptyView() {
       ......
    }

    //錯誤檢視 並且可以重試
    override fun showErrorView(errMsg: String) {
       .......
    }

    private fun registerEvent() {
       //接收錯誤資訊
       mViewModel.errorLiveData.observe(this) { errMsg ->
           showErrorView(errMsg)
       }
       //接收Loading資訊
       mViewModel.loadingLiveData.observe(this, { isShow ->
           showLoadingView(isShow)
       })
    }

    abstract fun init()
}

Base類中初始化ViewModel,還可以通過官方activity-ktxfragment-ktx擴充套件庫,初始化方式:val model: VM by viewModels()

子類中繼承如下:

class MvvmExampleActivity : BaseMvvmActivity<WanViewModel>() {

    private val mTvContent: TextView by id(R.id.tv_content)
    private val mBtnQuest: Button by id(R.id.btn_request)
    private val mToolBar: Toolbar by id(R.id.toolbar)

    override fun getLayoutId(): Int {
        return R.layout.activity_wan_android
    }

    override fun initViews() {
        initToolBar(mToolBar, "Jetpack MVVM", true)
    }

    override fun init() {
        mBtnQuest.setOnClickListener {
            //請求資料
            mViewModel.getWanInfo()
        }
        /**
         * 這裡使用了擴充套件函式,等同於mViewModel.mWanLiveData.observe(this) {}
         */
        observe(mViewModel.mWanLiveData) { list ->
            val builder = StringBuilder()
            for (index in list.indices) {
                //每條資料進行折行顯示
                if (index != list.size - 1) {
                    builder.append(list[index])
                    builder.append("\n\n")
                } else {
                    builder.append(list[index])
                }
            }
            mTvContent.text = builder.toString()
        }
    }
}

我們把ViewModel的初始化放到了父類裡進行,程式碼看上去更簡單了。監聽資料變化mViewModel.mWanLiveData.observe(this) {} 方式改成observe(mViewModel.mWanLiveData) {}方式,少傳了一個LifecycleOwner,其實這是一個擴充套件函式,如下:

fun <T> LifecycleOwner.observe(liveData: LiveData<T>, observer: (t: T) -> Unit) {
    liveData.observe(this, { observer(it) })
}

ps:我們初始化View控制元件時,如 private val mBtnQuest: Button by id(R.id.btn_request),依然使用了擴充套件函式,如下:

fun <T : View> Activity.id(id: Int) = lazy {
    findViewById<T>(id)
}

不用像寫java程式碼中那樣時刻要想著判空,同時只會在使用時才會進行初始化,很實用!

說回來,接著是ViewModel層的封裝,BaseViewModel.kt

abstract class BaseViewModel : ViewModel() {
    //loading
    val loadingLiveData = SingleLiveData<Boolean>()
    //異常
    val errorLiveData = SingleLiveData<String>()

    /**
     * @param request 正常邏輯
     * @param error 異常處理
     * @param showLoading 請求網路時是否展示Loading
     */
    fun launchRequest(
        showLoading: Boolean = true,
        error: suspend (String) -> Unit = { errMsg ->
            //預設異常處理,子類可以進行覆寫
            errorLiveData.postValue(errMsg)
        }, request: suspend () -> Unit
    ) {
        //是否展示Loading
        if (showLoading) {
            loadStart()
        }

        //使用viewModelScope.launch開啟協程
        viewModelScope.launch(Dispatchers.IO) {
            try {
                request()
            } catch (e: Exception) {
                error(e.message ?: "")
            } finally {
                if (showLoading) {
                    loadFinish()
                }
            }
        }
    }

    private fun loadStart() {
        loadingLiveData.postValue(true)
    }

    private fun loadFinish() {
        loadingLiveData.postValue(false)
    }
}

擴充套件一下1、上面執行網路請求時,使用viewModelScope.launch來啟動協程,引入方式:

implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'

這樣就可以直接在ViewModel中啟動協程並且當ViewModel生命週期結束時協程也會自動關閉,避免使用GlobalScope.launch { }MainScope().launch { }還需自行關閉協程, 當然,如果是在Activity/Fragment、liveData中使用協程,也可以按需引入:

implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'

具體可以參見官方的 將 Kotlin 協程與生命週期感知型元件一起使用 這篇文章。

2、另外細心的讀者可能觀察到,上面我們的Loading、Error資訊監聽都是用的SingleLiveData,把這個類打程式碼貼一下:

/**
 * 多個觀察者存在時,只有一個Observer能夠收到資料更新
 * https://github.com/android/architecture-samples/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java
 */
class SingleLiveData<T> : MutableLiveData<T>() {
    companion object {
        private const val TAG = "SingleLiveEvent"
    }
    private val mPending = AtomicBoolean(false)

    @MainThread
    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        if (hasActiveObservers()) {
            Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
        }
        // Observe the internal MutableLiveData
        super.observe(owner) { t ->
            //如果expect為true,那麼將值update為false,方法整體返回true,
            //即當前Observer能夠收到更新,後面如果還有訂閱者,不能再收到更新通知了
            if (mPending.compareAndSet(true, false)) {
                observer.onChanged(t)
            }
        }
    }

    override fun setValue(@Nullable value: T?) {
        //AtomicBoolean中設定的值設定為true
        mPending.set(true)
        super.setValue(value)
    }

    /**
     * Used for cases where T is Void, to make calls cleaner.
     */
    @MainThread
    fun call() {
        value = null
    }
}

可以看到SingleLiveData還是繼承自MutableLiveData,區別是當多個觀察者存在時,只有一個Observer能夠收到資料更新,本質上是在observe()時通過CAS加了限制,註釋已經很詳細了,不再贅述。

子類中繼承如下:

class WanViewModel : BaseViewModel() {
    //LiveData
    val mWanLiveData = MutableLiveData<List<WanModel>>()

    //Repository中間層 管理所有資料來源 包括本地的及網路的
    private val mWanRepo = WanRepository()

    fun getWanInfo(wanId: String = "") {
        launchRequest {
            val result = mWanRepo.requestWanData(wanId)
            when (result.state) {
                State.Success -> mWanLiveData.postValue(result.data)
                State.Error -> errorLiveData.postValue(result.msg)
            }
        }
    }
}

最後是對Model層的封裝,BaseRepository.kt

open class BaseRepository {
    suspend fun <T : Any> executeRequest(
        block: suspend () -> BaseData<T>
    ): BaseData<T> {
        val baseData = block.invoke()
        if (baseData.code == 0) {
            //正確
            baseData.state = State.Success
        } else {
            //錯誤
            baseData.state = State.Error
        }
        return baseData
    }
}

資料基類BaseData.kt

class BaseData<T> {
    @SerializedName("errorCode")
    var code = -1
    @SerializedName("errorMsg")
    var msg: String? = null
    var data: T? = null
    var state: State = State.Error
}

enum class State {
    Success, Error
}

子類中繼承如下:

class WanRepository : BaseRepository() {
    suspend fun requestWanData(drinkId: String): BaseData<List<WanModel>> {
        val service = RetrofitUtil.getService(DrinkService::class.java)
        return executeRequest {
            service.getBanner()
        }
    }
}

到此,基本上就完成了。