Android極簡MVVM,從一個基類庫談起

語言: CN / TW / HK

highlight: a11y-dark

我報名參加金石計劃1期挑戰——瓜分10萬獎池,這是我的第1篇文章,點選檢視活動詳情

Hello啊各位老鐵,今天帶來一個老生常談的技術,MVVM,這篇文章,主要詳細介紹如何封裝一個MVVM的基類庫,以及MVVM架構模式在實際業務中的用法,最後會把實際的封裝程式碼開源,並提供遠端依賴,方便給到大家使用以及二次修改,儘量做到細緻入微,淺顯易懂,OK,廢話不多贅述,我們進入正文。

這篇文章大概會按照以下幾個模組進行闡述,此次封裝,做到絕無第三方依賴,都是Android原生的程式碼封裝,請放心使用,如果您想直接進行使用,請直接跳到第4步,整合使用即可,此次的封裝,和目前主流的MVVM架構模式,會完美契合,讓架構模式簡單化,讓業務程式碼清晰化,必須值得推薦使用。

一、MVVM簡單概括

二、基於MVVM模式如何封裝基類庫

三、實戰封裝

四、封裝後在業務中如何使用

五、開源以及Demo檢視

溫馨提示:內容稍多,請合理安排好時間,如果不想查閱具體封裝過程,底部有開源地址,可以直接檢視。

一、MVVM簡單概括

MVVM的開發模式,相對來說低耦合,業務之間邏輯顯得也十分分明,Model層負責將請求的資料交給ViewModel層;ViewModel層負責將請求到的資料做業務邏輯處理,最後交給View層去展示,與View一一對應;View層只負責介面繪製重新整理,不處理業務邏輯,非常適合進行獨立模組開發。

image.png

三層簡單概括

1、Model:資料層,包含資料實體和對資料實體的操作。

2、View:檢視層,對應於Activity,XML,View,負責資料顯示以及使用者互動。

3、ViewModel:關聯層,將Model和View進行繫結,Model或者View更改時,實時重新整理對方。

需要注意:

1、View只做和UI相關的工作,不涉及任何業務邏輯,不涉及操作資料,不處理資料,也就是UI和資料是嚴格分開的。

2、ViewModel只做和業務邏輯相關的工作,不涉及任何和UI相關的操作,不持有控制元件引用,不更新UI。

二、基於MVVM模式如何封裝基類庫

MVVM我們已經清晰,然而針對現有的三層,我們如何進行拆解封裝呢?面對這樣的一個問題,我們也是需要從三層以及和實際的業務進行相結合,從實際業務中來,也要從實際業務中去,這是我們封裝的一個潛在因素,一旦脫離了實際,封裝的再優秀,也只是一個花瓶,中看不中用。

針對MVVM中的三層,其實,我們在封裝中,也是基於這三層,View,ViewModel和Model。View中,在實際的開發中,一般針對Activity和Fragment進行系統的抽取封裝,ViewModel一般會抽取一個父類,做一些公共的方法或屬性配置,Model層一般封裝的較少,根據實際業務,需要具體問題具體分析。

Activity和Fragment的封裝思路,其實是一致的,需要以簡單和複雜兩種方向進行抽取,一種是簡單的頁面繼承使用,一種是複雜的頁面繼承使用,這樣區分的一個目的,就是,專職專用,避免大材小用,而具體的封裝,除了使得程式碼簡潔化,更重要的拓展化,方便子類的呼叫。

在具體封裝的時候,與實際業務相結合,這個無比重要,比如實際的大部分頁面,都帶有一個標題欄,那麼標題欄就可以直接封裝父類裡面,像子類拓展出,更改標題,右側按鈕,左側按鈕等功能屬性;除了統一的標題欄,另外就是子類的檢視了,關於子類的檢視傳遞,這個是必須的,可以直接抽象出一個必須要實現的方法,其他的,比如狀態列的改變,預設頁的設定等等,也需要在父類中統一的給出。

複雜的頁面是基於簡單的頁面而來的,這裡的複雜,一般是包含很多邏輯的處理,那麼,我們就可以增加ViewModel層和Model層了,目前基於DataBinding的實現方式,無論簡單和複雜,都是必須需要考慮的,也就是說在父類中,我們就需要向子類提供出可以拿到的databinding和viewmodel,一般以泛型的方式引入,這樣子類再繼承的時候,就可以很方便的進行呼叫。

在複雜的頁面,也就是包含ViewModel層和Model層的時候,需要考慮繫結檢視variable的傳遞,也就是當前的ViewModel和那個xml進行繫結,當然這是在需要的時候,必須要操作的,除了檢視繫結,常見的,資料請求狀態,比如請求成功,請求失敗,預設頁顯示和隱藏,Dialog的顯示和隱藏,LiveData的資料回傳等等,在複雜的頁面中也是需要我們考慮的,除此之外,ViewModel中如何和View層的生命週期繫結,在實際的業務中也是不得不需要考慮的。

除了以上的常規考慮,在實際的業務中,比如事件訊息傳遞,PagerAdapter使用,狀態列透明等很多和基類的相關的功能,我們其實也可以進行封裝進去,便於子類的呼叫。

三、實戰封裝

通過第2條中的拆解和具體的封裝思路,不妨我們進行實戰一下,由於Activity和Fragment的封裝思路以及相關屬性和方法,大部分都是雷同的,所以目前只介紹Activity,更詳細的封裝,還請大家參考原始碼。

1、Activity的簡單封裝

簡單封裝,不攜帶ViewModel,只傳遞ViewDataBinding,子類必須重寫的方法只有一個initData,其他均為選擇性重寫,如果相對邏輯比較簡單的頁面,可以繼承此類。

一個很簡單的普通封裝,就是把共有的常見的,封裝到父類裡,便於子類的呼叫,具體什麼方法,什麼邏輯進行採取封裝,需要我們根據具體業務或者公司的相關情況而定,以下是原始碼。

```kotlin abstract class BaseActivity(@LayoutRes layoutId: Int = 0) : AppCompatActivity(layoutId) {

private var mActionBarView: ActionBarView? = null
private var mLayoutError: LinearLayout? = null
private var mLayoutId = layoutId
lateinit var mBinding: VB


override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    try {
        //預設狀態列為白底黑字
        darkMode(BaseConfig.statusBarDarkMode)
        statusBarColor(ContextCompat.getColor(this, BaseConfig.statusBarColor))
        setContentView(R.layout.activity_base)
        val baseChild = findViewById<LinearLayout>(R.id.layout_base_child)
        mLayoutError = findViewById(R.id.layout_empty_or_error)
        mActionBarView = findViewById(R.id.action_bar)
        if (mLayoutId == 0) {
            mLayoutId = getLayoutId()
        }


        if (savedInstanceState != null && getIntercept()) {
            noEmptyBundle()
            return
        }


        val childView = layoutInflater.inflate(mLayoutId, null)
        baseChild.addView(childView)
        mBinding = DataBindingUtil.bind(childView)!!


        initView()
        initData()
    } catch (e: Exception) {
        e.printStackTrace()
        noEmptyBundle()
    }
}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:獲取檢視id
 */
open fun getLayoutId(): Int {
    return 0
}


open fun initView() {}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:初始化資料
 */
abstract fun initData()




/**
 * AUTHOR:AbnerMing
 * INTRODUCE:動態改變狀態列顏色和標題
 */
fun setDarkTitle(dark: Boolean, color: Int, title: String) {
    try {
        darkMode(dark)
        statusBarColor(ContextCompat.getColor(this, color))
        setBarTitle(title)


    } catch (e: Exception) {
        e.printStackTrace()
    }


}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:設定標題
 */
fun setBarTitle(title: String) {
    mActionBarView!!.visibility = View.VISIBLE
    mActionBarView!!.setBarTitle(title)
}




/**
 * AUTHOR:AbnerMing
 * INTRODUCE:隱藏左側按鈕
 */


fun hintLeftMenu() {
    mActionBarView!!.hintLeftBack()
}




/**
 * AUTHOR:AbnerMing
 * INTRODUCE:獲取ActionBarView
 */
fun getActionBarView(): ActionBarView {
    return mActionBarView!!
}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:隱藏標題欄
 */
fun hintActionBar() {
    mActionBarView?.visibility = View.GONE
}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:Bundle為空進行攔截,解決改變許可權後重回App崩潰問題
 */
open fun getIntercept(): Boolean {
    return false
}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:Bundle為空時的邏輯處理,解決改變許可權後重回App崩潰問題
 */
open fun noEmptyBundle() {}




override fun onDestroy() {
    super.onDestroy()
    try {
        LiveDataBus.removeObserve(this)
        LiveDataBus.removeStickyObserver(this)
    } catch (e: Exception) {
        e.printStackTrace()
    }
}




/**
 * AUTHOR:AbnerMing
 * INTRODUCE:透明狀態列
 */
fun translucentWindow(dark: Boolean) {
    try {
        immersive(0, dark)
    } catch (e: Exception) {
        e.printStackTrace()
    }


}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:設定預設頁
 */
fun setEmptyOrError(view: View) {
    mLayoutError?.visibility = View.VISIBLE
    mLayoutError?.removeAllViews()
    mLayoutError?.addView(view)
}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:隱藏
 */
fun hintEmptyOrErrorView() {
    mLayoutError?.visibility = View.GONE
}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:獲取錯誤或為空的view
 */
fun getEmptyOrErrorView(): LinearLayout {
    return mLayoutError!!
}

} ```

涉及的方法概述

| 方法名 | 引數 | 概述 | | --- | --- | --- | | getLayoutId | 無參 | 子類傳遞的layout,用於載入檢視,可以通過構造方法傳遞,也可以通過此方法傳遞。 | | initView | 無參 | 初始化View,非必須重寫 | | initData | 無參 | 初始化資料 | | setDarkTitle | dark: Boolean, color: Int, title: String,1、dark: Boolean,狀態列顏色,true就是黑色,false就是白色。2、color: Int,狀態列背景顏色,3、title: String,標題欄內容 | 設定標題,狀態列背景及顏色 | | setBarTitle | title: String,標題欄內容 | 設定標題 | | hintLeftMenu | 無參 | 隱藏左側按鈕 | | getActionBarView | 無參 | 獲取標題欄View,可以操作標題欄裡的任何控制元件 | | hintActionBar | 無參 | 隱藏標題欄 | | translucentWindow | dark: Boolean,狀態列顏色,true就是黑色,false就是白色 | 透明狀態列 | | setEmptyOrError | view: View,傳遞的預設View檢視 | 設定預設檢視 | | hintEmptyOrErrorView | 無參 | 隱藏預設檢視 | | getEmptyOrErrorView | 無參 | 獲取預設檢視 |

簡單的Activity沒有什麼好說的,都是中規中矩,具體的使用請大家看第四條,具體使用即可。

2、Activity的複雜封裝

也談不上覆雜,只是在繼承簡單頁面的基礎之上多加了一個ViewModel,相對於比較複雜的頁面,就可以繼承此類,此類,拓展了ViewModel,可以在ViewModel裡進行邏輯的書寫,此類也是MVVM的標準執行,V繼承於BaseVMActivity,VM繼承於BaseViewModel,至於M,可以在VM中通過getRepository方法進行獲取。

具體程式碼邏輯如下:

BaseVMActivity繼承於BaseActivity。

```kotlin abstract class BaseVMActivity(@LayoutRes layoutId: Int = 0) : BaseActivity(layoutId) {

lateinit var mViewModel: BM


override fun initData() {
    mViewModel = getViewModel()!!
    val variableId = getVariableId()
    if (variableId != -1) {
        mBinding.setVariable(getVariableId(), mViewModel)
        mBinding.executePendingBindings()
    }
    initVMData()
    observeLiveData()
    initState()
    lifecycle.addObserver(mViewModel)
}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:獲取繫結的xml id
 */
open fun getVariableId(): Int {
    return -1
}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:初始化狀態
 */
private fun initState() {
    mViewModel.mStateViewLiveData.observe(this, {
        when (it) {
            StateLayoutEnum.DIALOG_LOADING -> {
                dialogLoading()
            }
            StateLayoutEnum.DIALOGD_DISMISS -> {
                dialogDismiss()
            }
            StateLayoutEnum.DATA_ERROR -> {
                dataError()
            }
            StateLayoutEnum.DATA_NULL -> {
                dataEmpty()
            }
            StateLayoutEnum.NET_ERROR -> {
                netError()
            }
            StateLayoutEnum.HIDE -> {
                hide()
            }
        }
    })
}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:初始化資料
 */
abstract fun initVMData()


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:LiveData的Observer
 */
open fun observeLiveData() {


}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:dialog載入
 */
open fun dialogLoading() {}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:dialog隱藏
 */
open fun dialogDismiss() {}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:資料錯誤
 */
open fun dataError() {}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:資料為空
 */
open fun dataEmpty() {}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:網路錯誤或請求錯誤
 */
open fun netError() {}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:隱藏某些佈局或者預設頁等
 */
open fun hide() {}


private fun getViewModel(): BM? {
    //這裡獲得到的是類的泛型的型別
    val type = javaClass.genericSuperclass
    if (type != null && type is ParameterizedType) {
        val actualTypeArguments = type.actualTypeArguments
        val tClass = actualTypeArguments[1]
        return ViewModelProvider(
            this,
            ViewModelProvider.AndroidViewModelFactory.getInstance(application)
        )
            .get(tClass as Class<BM>)
    }
    return null
}

override fun onDestroy() { super.onDestroy() try { lifecycle.removeObserver(mViewModel) } catch (e: Exception) { e.printStackTrace() } }

} ```

封裝涉及的方法概述

| 方法名 | 引數 | 概述| | --- | --- | --- | | getVariableId | 無參 | 獲取繫結的xml variable,也就是當前的xml和哪個物件進行繫結,用於xml裡直接資料繫結 | | initVMData | 無參 | 初始化資料,必須要實現的方法 | | observeLiveData | 無參 | LiveData的Observer,UI層監聽ViewModel層的資料改變 | | dialogLoading | 無參 | dialog載入 | | dialogDismiss | 無參 | dialog隱藏 | | dataError | 無參 | 資料錯誤 | | dataEmpty | 無參 | 資料為空 | | netError | 無參 | 資料錯誤 | | hide | 無參 | 隱藏預設頁等其他頁面 |

BaseViewModel

BaseViewModel相對比較簡單,只提供了一個可以獲取Repository的方法,還有一個是重新整理UI檢視的一個LiveData,就是資料請求,Dialog載入,預設頁載入的狀態。更改狀態,只需要呼叫changeStateView方法即可,子類可以重寫生命週期方法,便於生命週期的考慮。

```kotlin

open class BaseViewModel : ViewModel() , BaseObserver{ /* * 控制狀態檢視的LiveData / val mStateViewLiveData = MutableLiveData()

/**
 * 更改狀態檢視的狀態
 */
public fun changeStateView(
    state: StateLayoutEnum
) {
    // 對引數進行校驗
    when (state) {
        StateLayoutEnum.DIALOG_LOADING -> {
            mStateViewLiveData.postValue(StateLayoutEnum.DIALOG_LOADING)
        }
        StateLayoutEnum.DIALOGD_DISMISS -> {
            mStateViewLiveData.postValue(StateLayoutEnum.DIALOGD_DISMISS)
        }
        StateLayoutEnum.DATA_ERROR -> {
            mStateViewLiveData.postValue(StateLayoutEnum.DATA_ERROR)
        }
        StateLayoutEnum.DATA_NULL -> {
            mStateViewLiveData.postValue(StateLayoutEnum.DATA_NULL)
        }
        StateLayoutEnum.NET_ERROR -> {
            mStateViewLiveData.postValue(StateLayoutEnum.NET_ERROR)
        }
        StateLayoutEnum.HIDE -> {
            mStateViewLiveData.postValue(StateLayoutEnum.HIDE)
        }
    }


}


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:獲取Repository
 */
inline fun <reified R> getRepository(): R? {
    try {
        val clazz = R::class.java
        return clazz.newInstance()
    } catch (e: Exception) {
        e.printStackTrace()
    }
    return null
}

/* * AUTHOR:AbnerMing * INTRODUCE:生命週期初始化 / override fun onCreate() { }

/* * AUTHOR:AbnerMing * INTRODUCE:生命週期頁面可見 / override fun onStart() { }

/* * AUTHOR:AbnerMing * INTRODUCE:生命週期頁面獲取焦點 / override fun onResume() { }

/* * AUTHOR:AbnerMing * INTRODUCE:生命週期頁面失去焦點 / override fun onPause() {

}

/* * AUTHOR:AbnerMing * INTRODUCE:生命週期頁面不可見 / override fun onStop() {

}

/* * AUTHOR:AbnerMing * INTRODUCE:生命週期頁面銷燬 / override fun onDestroy() { }

} ``` 複雜的Activity,大家可以發現,其實就是標準的MVVM形式封裝,Fragment的封裝也是基於此,搞清楚上述,基本上我們這個基類庫就完成了大半,確實也沒什麼好說的,大家直接看使用吧。

四、封裝後在業務中如何使用

通過以上的封裝,我們在業務層所有的頁面就可以繼承父類進行使用,以達到程式碼的高度統一,使得架構模式簡單化,讓業務程式碼清晰化,目前的封裝,大家可以直接封裝成庫或者打成aar給到其他開發者使用,目前我已經上傳到遠端,不想麻煩的老鐵,可以直接按照下面的步驟進行使用。

1、在你的根專案下的build.gradle檔案下,引入maven。

```

allprojects { repositories { maven { url "https://gitee.com/AbnerAndroid/almighty/raw/master" } } } ```

2、在你需要使用的Module中build.gradle檔案下,引入依賴。

```

dependencies { implementation 'com.vip:base:1.0.2' } ```

通過以上的Maven倉庫依賴,我們就可以愉快的進行使用了,下面針對各個封裝的功能進行一個簡單的演示,當然,大家可以直接看原始碼中的例項,那裡相對比較全面。

1、普通的Activity的繼承

如果,你的Activity頁面邏輯比較簡單,建議繼承BaseActivity,此父類,沒有與ViewModel相結合,只包含正常且簡單的邏輯處理,目前必須重寫的只有一個initData方法,其他方法,大家可以根據業務重寫即可。

kotlin class MainActivity : BaseActivity<ActivityMainBinding>(R.layout.activity_main) { /** * AUTHOR:AbnerMing * INTRODUCE:初始化資料 */ override fun initData() { setBarTitle("主頁") } }

2、ViewModel形式Activity的繼承

View層,需要繼承BaseVMActivity

```kotlin class TestViewModelActivity : BaseVMActivity(R.layout.activity_view_model) {

override fun initVMData() {
    setBarTitle("ViewModel方式使用")
}

} ```

ViewModel層,需要繼承BaseViewModel

實際的業務中,遇到網路請求,預設頁展示,Dialog顯示隱藏,呼叫changeStateView方法,UI層只需要重寫對應的方法即可。

```kotlin

class TestViewModel : BaseViewModel() {

  /**
 * AUTHOR:AbnerMing
 * INTRODUCE:獲取需要的Repository
 */
private val repository by lazy {
    getRepository<TestRepository>()
}

} ``` Model層,一般根據實際需要,進行具體的封裝使用。

```kotlin

class TestRepository {

} ```

3、DataBinding形式使用

View層,繼承BaseVMActivity,返回當前檢視的繫結variable

```kotlin

class DataBindActivity : BaseVMActivity(R.layout.activity_data_bind) {

override fun initVMData() {
    setBarTitle("DataBinding使用")
}


override fun getVariableId(): Int {
    return BR.data
}

} ```

ViewModel層繼承BaseViewModel

```kotlin

class DataBindViewModel : BaseViewModel() {

var oneWayContent = "單向繫結資料測試"


var twoWayContent = "雙向繫結資料測試"


/**
 * AUTHOR:AbnerMing
 * INTRODUCE:獲取雙向繫結資料
 */
var clickListener = View.OnClickListener {


    Toast.makeText(it.context, twoWayContent, Toast.LENGTH_SHORT).show()
}

} ```

XML檢視,直接繫結

```xml

<data>


    <variable
        name="data"
        type="com.abner.base.bind.DataBindViewModel" />
</data>


<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingLeft="@dimen/gwm_dp_20"
    android:paddingRight="@dimen/gwm_dp_20">


    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="@dimen/gwm_dp_20"
        android:text="@{data.oneWayContent}" />


    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/gwm_dp_20"
        android:hint="雙向繫結"
        android:text="@={data.twoWayContent}" />


    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:layout_marginTop="@dimen/gwm_dp_20"
        android:onClick="@{data.clickListener}"
        android:text="獲取雙向繫結資料" />


</LinearLayout>

```

4、Fragment的簡單使用

如果,你的Fragment頁面邏輯比較簡單,建議繼承BaseFragment,此父類,沒有與ViewModel相結合,只包含正常且簡單的邏輯處理,目前必須重寫的只有一個initData方法,其他方法,大家可以根據業務重寫即可。

```kotlin class TestPagerFragment : BaseFragment (R.layout.fragment_test_pager) {

override fun initData() {


}

} ```

5、ViewModel形式Fragment的繼承

View層,需要繼承BaseVMFragment

```kotlin class TestViewModelPagerFragment : BaseVMFragment(R.layout.fragment_test_pager) {

override fun initVMData() {

}

} ``` ViewModel層,需要繼承BaseViewModel

```kotlin class TestFragmentViewModel :BaseViewModel(){

    /**
 * AUTHOR:AbnerMing
 * INTRODUCE:獲取需要的Repository
 */
private val repository by lazy {
    getRepository<TestRepository>()
}

} ``` Model層,一般根據實際需要,進行具體的封裝使用。

```kotlin

class TestRepository {

} ```

6、Fragment的DataBinding形式使用和Activity類似,就不贅述了。

7、事件訊息匯流排使用

普通事件傳送

kotlin LiveDataBus.send("send", "我傳送了一條普通訊息") 普通傳送事件接收

```kotlin

LiveDataBus.observe(this, "send", Observer { Toast.makeText(this, it, Toast.LENGTH_SHORT).show() }) ``` 粘性事件傳送

```kotlin

LiveDataBus.sendSticky("sendSticky", "我傳送了一條粘性事件訊息") ``` 粘性事件接收

```kotlin

LiveDataBus.observeSticky(this, "sendSticky", Observer { Toast.makeText(this, it, Toast.LENGTH_SHORT).show() }) ``` 更多的其他功能使用,大家直接看Github即可,上邊有比較清晰的介紹。

image.png

五、開源以及Demo檢視

以上的封裝,目前已經開源,大家可以下載檢視原始碼,或者進行二次更改使用,地址是:

https://github.com/AbnerMing888/VipBase

相關Demo,大家可以down下專案,執行即可,這裡簡單貼張效果圖:

image.png

目前的封裝,沒有過多的冗餘程式碼,完全可以滿足實際的業務需要,大家可以按照這種模式試驗一番,遇到問題,可以多多交流,畢竟,技術是開放的,交流中才能不斷的進步,當然了,需要結合自己的實際業務進行使用,畢竟專案中不應存在多個架構模式,MVC也好,MVP,MVVM,MVI也罷,無論使用哪種,適合的才是最好的。

好了,各位老鐵,這篇文章就到這裡,下篇文章《元件化開發,從未如此簡單》正在撰寫中,大家敬請期待!