搭建基于 MVVM 的項(xiàng)目框架
前言
從谷歌在 2017 年的 Google IO 宣布 Kotlin 成為 Android 開(kāi)發(fā)的官方語(yǔ)言開(kāi)始,已經(jīng)過(guò)去將近 2 年了,Kotlin 越來(lái)越被開(kāi)發(fā)者所關(guān)注,在 Github 的開(kāi)源項(xiàng)目中使用這門(mén)語(yǔ)言的也呈上升趨勢(shì)。
雖然批評(píng)的聲音也不少,說(shuō) Kotlin 只不過(guò)是語(yǔ)法糖的,拿來(lái)跟 Java 8/9/10 對(duì)比表示不過(guò)如此的,但是針對(duì) Android 開(kāi)發(fā)而言,這門(mén)語(yǔ)言是有生產(chǎn)力的,具體我在項(xiàng)目中可能會(huì)插入一些個(gè)人感受。
1. 淺談 MVP 和 MVVM
-
MVP
公司大概 1 年半前開(kāi)始改為用 MVP 模式來(lái)開(kāi)發(fā)代碼,相比曾經(jīng)上千行的 Activity 代碼,實(shí)在進(jìn)步了不少,V (View) 和 P (Presenter) 之間通過(guò)接口來(lái)互相訪問(wèn)與操作,一定程度抽象了代碼邏輯,確實(shí)有利于維護(hù)
基本上代碼目錄類(lèi)似這個(gè)
Model 層用了 Retrofit 和 RxJava 進(jìn)行網(wǎng)絡(luò)的或者本地的數(shù)據(jù)獲取,比較穩(wěn)定,就不進(jìn)行對(duì)比了,因?yàn)橐矝](méi)區(qū)別
其中的 MainContract 代碼可能是這樣子寫(xiě)的
public class MainContract {
interface View extends BaseView{
void showAd(Adverts adverts);
}
interface Presenter extends BasePresenter{
void loadAd();
Adverts getAppAdvert() ;
}
}
暫且不管我們?cè)?BaseView 和 BasePresenter 里做了什么操作,大致上看方法就是一個(gè)獲取廣告數(shù)據(jù)然后把廣告 List 傳遞到 View 進(jìn)行 UI 操作的功能。
之后讓 MainActivity 去實(shí)現(xiàn) View 接口 而 MainPresenter 去實(shí)現(xiàn) Presenter 接口,在初始化時(shí),互相都持有了對(duì)方的接口實(shí)例。
隨著生命周期的變化,可能出現(xiàn) NPE,或者內(nèi)存泄露,這確實(shí)也是我們上一個(gè)項(xiàng)目上線測(cè)試后出現(xiàn)的最多 Bug,添加了不少判空條件,更加加深了我去嘗試其它設(shè)計(jì)模式的愿望。
-
MVVM
時(shí)隔一年,谷歌在 2018 年的 Google IO 中發(fā)布了 JetPack 支持包,主打眾多開(kāi)發(fā)庫(kù)隨意添加使用,互不干擾,還順便把 v7 和 v4 支持包全改了個(gè)包名叫 androidx , 如何遷移到 androidx 可以之后再談。
為了完成 MVVM 的設(shè)計(jì),挑選了其中的 LiveData 和 ViewModel 進(jìn)行使用。
LiveData 其實(shí)跟 RxJava 一樣屬于觀察者模式的第三方庫(kù),一定程度上來(lái)說(shuō)是重復(fù)的,奈何各有優(yōu)勢(shì),所以在數(shù)據(jù)處理中繼續(xù)使用 Retrofit 和 RxJava 這套搭配,而在 UI 操作上添加了 LiveData 用于通知 V 端進(jìn)行頁(yè)面的刷新。
- LiveData 優(yōu)勢(shì)和劣勢(shì)
優(yōu)勢(shì):
1. 綁定生命周期,不會(huì)內(nèi)存泄露,放心把數(shù)據(jù)交給他保管
2. 默認(rèn)只在 Activity 和 Fragment 在 started 或 resumed 2 種狀態(tài)時(shí)通知 UI 更新數(shù)據(jù)
3. 當(dāng) UI 處于started 或 resumed 狀態(tài)外,但是還沒(méi)銷(xiāo)毀之前,一直會(huì)接收更新數(shù)據(jù),在 UI 處于可見(jiàn)狀態(tài)時(shí),只會(huì)通知最新的數(shù)據(jù)到 UI。
4. 屏幕旋轉(zhuǎn)重建后的 View 仍然能利用之前數(shù)據(jù)。
5. 以及其它。
劣勢(shì):
1. MutableLiveData 只能將完整的新數(shù)據(jù)作為值覆蓋舊數(shù)據(jù)才會(huì)通知觀察者,也就是說(shuō)利用 getValue() 方法對(duì)舊數(shù)據(jù)進(jìn)行微小修改也沒(méi)辦法觸發(fā)通知。
畢竟是實(shí)戰(zhàn)中發(fā)現(xiàn)的優(yōu)勢(shì)劣勢(shì),總結(jié)得不完全,其實(shí)也并不想長(zhǎng)長(zhǎng)寫(xiě)一大段干澀的字,請(qǐng)多包涵。
插播一個(gè) kt 語(yǔ)言很有意思的實(shí)例構(gòu)造方法,在 AbsFragment 主要是做了一個(gè)為頁(yè)面添加頂部操作欄的功能
有興趣可以看這一部分,不然跳過(guò)以下一大段
- 構(gòu)建 AbsFragment 基礎(chǔ)類(lèi)
abstract class AbsFragment : Fragment() {
private var titleBar: TitleBar? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
if (getContentViewLayoutID() != 0) {
titleBar = getTitleBar()
return if (titleBar != null) {
//略,在新建的 RelativeLayout 頂部添加自定義 titleBar,再添加主布局 layout
} else {
inflater.inflate(getContentViewLayoutID(), container, false)
}
}
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (titleBar != null) {
titleBar!!.apply {
//略,從 TitleBar 實(shí)例中獲取自定義 titleBar 所需要顯示的數(shù)據(jù),以及默認(rèn)值
}
}
initView()
}
/**默認(rèn)初始化頁(yè)面功能方法 */
protected abstract fun initView()
/** 返回布局layout*/
protected abstract fun getContentViewLayoutID(): Int
/**
* 是否添加title欄
* 不添加 返回 null
* 需要添加 返回 [initTitleBar] 方法
*/
protected abstract fun getTitleBar(): TitleBar?
/** 就是這一段比較有趣 */
protected fun initTitleBar(init: TitleHelper.() -> Unit): TitleBar {
return initAny(TitleBar(), init)
}
}
可能初入門(mén) kt 的朋友不太了解它的 lambda 怎么寫(xiě),舉個(gè)栗子
fun <T> lock(body: () -> T): T {
return body()
}
以上方法要求返回泛型 T ,直接返回從參數(shù)中得到的 body 函數(shù) "()" 空括號(hào)代表函數(shù)無(wú)參數(shù),"
-> T "代表函數(shù)將會(huì)返回 泛型 T
對(duì)使用函數(shù) lock 的人來(lái)說(shuō)
//大括號(hào)內(nèi)就是所填入的 body 函數(shù)
lock<String>(body = { "" })
//kt 約定,只有一個(gè) Lambda 表達(dá)式的方法應(yīng)該將大括號(hào)移到小括號(hào)外側(cè),于是變成以下
lock<String>() { "" }
// 其實(shí)空的小括號(hào)也可以省略,尖括號(hào)內(nèi)的泛型也由于 kt 語(yǔ)言的自動(dòng)推斷功能,會(huì)根據(jù)大括號(hào)內(nèi)的返回值自動(dòng)變化,故又可以省略
lock { "" }
回到 initTitleBar 這個(gè)方法,返回的是一個(gè) kt 的擴(kuò)展函數(shù)
/**
* 創(chuàng)建類(lèi)型安全的構(gòu)建器的方法
*/
fun <T : Any> initAny(any: T, init: T.() -> Unit): T {
any.init()
return any
}
跟上面的例子很像,但"T.() -> Unit" 是?個(gè)帶接收者的函數(shù)類(lèi)型。這意味著我
們需要向函數(shù)傳遞?個(gè) T 類(lèi)型的實(shí)例,并且我們可以在函數(shù)內(nèi)部調(diào)?該實(shí)例的成員。
關(guān)于 TitleBar 方法很簡(jiǎn)單,DslMarker 注解暫時(shí)不談
class TitleBar : TitleHelper {
var title: String = ""
override fun title(text: String) {
title = text
}
...
}
@DslMarker
annotation class TitleBarMarker
//抽象,避免內(nèi)部實(shí)例被直接操作
@TitleBarMarker
interface TitleHelper {
@TitleBarMarker
fun title(text: String)
...
}
所以 AbsFragment 的子類(lèi)實(shí)現(xiàn)類(lèi)似這樣子,只調(diào)用想要不同于默認(rèn)值的部分方法
override fun getTitleBar() = initTitleBar {
title("分類(lèi)")
}
插播結(jié)束
- 構(gòu)建 BaseFragment 基礎(chǔ)類(lèi)
我希望在 BaseFragment 中實(shí)現(xiàn)一些基礎(chǔ)的監(jiān)聽(tīng)者模式,基本只用到 ViewModel 和 LiveData 2個(gè)庫(kù)來(lái)完成
那先從 ViewModel 說(shuō)起
abstract class BaseViewModel : ViewModel() {
/** 顯示布局里的數(shù)據(jù)加載view */
private val _showLoadingView = MutableLiveData<Boolean>()
/** LiveData只有g(shù)et方法 */
val showLoadingView: LiveData<Boolean>
get() = _showLoadingView
/** 調(diào)用set方法即可決定是否顯示布局里的數(shù)據(jù)加載view */
fun setLoadingView(loading: Boolean) {
if (_showLoadingView.value != loading) {
_showLoadingView.value = loading
}
}
/* 基本上就是在初始化頁(yè)面需要請(qǐng)求的數(shù)據(jù)的時(shí)候,調(diào)用此方法 */
abstract fun sendRequest()
}
略微簡(jiǎn)化來(lái)下代碼,就變成如上到代碼,MutableLiveData 的公共方法有 setValue() 和 postValue() , 而他的父類(lèi) LiveData 的 setValue() 是個(gè) protected 方法 ,可以對(duì)外隱藏賦值操作,一定程度上讓數(shù)據(jù)操作完全局限在 ViewModel 中。
再來(lái)說(shuō)說(shuō) BaseFragment
abstract class BaseFragment<T : BaseViewModel> : AbsFragment(), BaseViewImp<T> {
lateinit var viewModel: T
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
viewModel = if (getModelFactory() == null) {
ViewModelProviders.of(this).get(getModel())
} else {
ViewModelProviders.of(this, getModelFactory()).get(getModel())
}
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
observeLoadingView()
}
private fun observeLoadingView() {
viewModel.showLoadingView.observe(viewLifecycleOwner, Observer { showLoad ->
getLoadingView().let { loadingView ->
loadingView?.visibility = if (isLoading) View.VISIBLE else View.GONE
dosth...
}
}
})
}
/** 需要時(shí)子類(lèi)返回loading頁(yè)面 */
protected open fun getLoadingView(): View? {
return null
}
}
interface BaseViewImp<T> {
/** 返回ViewModel的類(lèi)*/
fun getModel(): Class<T>
/** 當(dāng)需要給viewModel傳參時(shí),返回ViewModel的工廠*/
fun getModelFactory(): ViewModelProvider.Factory? {
return null
}
}
幾個(gè) kotlin 語(yǔ)法我啰嗦幾句,var lateinit 只能說(shuō)是提示編譯器,這個(gè)變量不要因?yàn)闆](méi)有初始化就給我報(bào)錯(cuò),我會(huì)在使用前擇期初始化,但是到運(yùn)行時(shí)忘記初始化了,也只有乖乖接收 NPE 錯(cuò)誤的選擇了。
let方法是前值非空就執(zhí)?代碼的簡(jiǎn)寫(xiě)
getModel() 返回 BaseViewModel 的子類(lèi) Class,而因?yàn)?ViewModel 初始化的特殊性,他是由 Fragemnt 或者 Activity 創(chuàng)建并且保管的,傳參數(shù)需要通過(guò)實(shí)現(xiàn) ViewModelProvider.Factory 接口來(lái)完成,例如以下這個(gè)類(lèi):
class DownloadFactory(
val novelId: String
) : ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return DownloadViewModel(novelId) as T
}
}
參數(shù) novelId 就傳遞到了類(lèi) DownloadViewModel(val novelId : String) 中啦