1 MVVM總覽
本文包含Android
中MVVM
體系中的很多部分,主要對ViewModel
+DataBinding
+RxJava
+LiveData
+Lifecycle
等筆者所使用的技術體系進行解析.
本文字數較多,內容較為完整並且後續還會追加更新,閱讀本篇文章需要較長時間,建議讀者分段閱讀.
所有文字均為個人學習總結和理解,僅供參考,如有紕漏還請指出,筆者不勝感激.
1.1 配置環境
- 筆者的
Android Studio
版本=3.2
Jetpack
最低相容到Android
=2.1
,API
=7
1.2 為什麼要選擇MVVM?
要回答這個問題首先就要介紹MVC
與MVP
這兩種模式,從MVC
到MVVM
其實大家想的都是怎麼把Model
和View
儘可能的拆開(熟悉三者定義的朋友可以跳過該節).
1.2.1 MVC
MVC
(Model
-View
-Controller
)即傳統Android
開發中最常用的模式:
- 通常使用
Activity
/Fragment
作為Controller
層, - 以
android.view.View
的子類以xml
構建檔案構建起的佈局
作為View
層 - 以
SQLite
資料庫,網路請求作為Model
層.
但由於Activity
/Fragment
的功能過於強大
並且實際上包含了部分View
層功能,導致最後Activity
/Fragment
既承擔了View
的責任,又承擔了Controller
的責任.所以一般較複雜的頁面,Activity
/Fragment
很容易堆積程式碼,最終導致Controller
混雜了View
層和業務邏輯(也就是你們所知道的一個Activity
三千行)
在MVC
中View
層與Model
幾乎幾乎完全沒有隔離,View
層可以直接操作Model
層,Model
層的回撥
裡也可能會直接給View
賦值.Controller
的概念被弱化,最後只剩下MV
沒有C
了.
這也將導致但你想把某個介面上的元素進行更新時,他會牽扯到一堆跟Model
層相關的程式碼,這個問題在你變更Model
層的時候同樣也會出現,這個問題其實是沒有很好的將邏輯分層導致的.
1.2.2 MVP
MVP
(Model
-View
-Presenter
)架構設計,是當下最流行的開發模式,目前主要以Google
推出的TodoMVP
為主,MVP
不是一種框架,它實際上更類似一種分層思想
,一種介面約定
,具體體現在下面:
- 定義
IView
介面,並且在介面中約定View
層的各種操作,使用android.view.View
的子類以xml
構建檔案構建起的佈局
和Activity
/Fragment
作為佈局控制器,實現IView
這個View
層的介面,View
層的實際實現類保留一個IPresenter
介面的例項. - 定義
IPresenter
介面,並且在介面中約定Presenter
層的各種操作.可以使用一個與View
無關的類實現它,一般是XxxPresenterImpl
.通常情況下Presenter
層會包含Model
層的引用和一個IView
介面的引用,但不應該直接或者間接引用View
層android.view.View
的子類,甚至是操作的引數中也最好不要有android.view.View
的子類傳進來,因為它應該只負責業務邏輯和資料的處理並通過統一的介面IView
傳遞到View
層. - 不需要為
Model
層定義一個IModel
的介面,這一層是改造最小的.以前該怎麼來現在也差不多該怎麼來.但是現在Presenter
把它和View
隔開了,Presenter
就可以作為一段獨立的邏輯被複用.
MVP
模式解決了MVC
中存在的分層問題,Presenter
層被突出強調,實際上也就是真正意義上實現了的MVC
但是MVP
中其實仍然存在一些問題,比如當業務邏輯變得複雜以後,IPresenter
和IView
層的運算元量可能將會成對的爆炸式增長,新增一個業務邏輯,可能要在兩邊增加數個通訊介面,這種感覺很蠢.
並且,我們要知道一個Presenter
是要帶一個IView
的,當一個Presenter
需要被複用時,對應的View
就要去實現所有這些操作,但往往一些操作不是必須實現的,這樣會留下一堆TODO
,很難看.
1.2.3 MVVM
MVVM
(Model
-View
-ViewModel
)由MVP
模式演變而來,它由View
層,DataBinding
,ViewModel
層,Model
層構成,是MVP
的升級版並由Google
的Jetpack
工具包提供框架支援:
View
層包含佈局,以及佈局生命週期控制器(Activity
/Fragment
)DataBinding
用來實現View
層與ViewModel
資料的雙向繫結(但實際上在Android Jetpack
中DataBinding
只存在於佈局和佈局生命週期控制器之間,當資料變化繫結到佈局生命週期控制器時再轉發給ViewModel
,佈局控制器可以持有DataBinding
但ViewModel
不應該持有DataBinding
)ViewModel
與Presenter
大致相同,都是負責處理資料和實現業務邏輯,但是ViewModel
層不應該直接或者間接地持有View
層的任何引用,因為一個ViewModel
不應該直達自己具體是和哪一個View
進行互動的.ViewModel
主要的工作就是將Model
提供來的資料直接翻譯成View
層能夠直接使用的資料,並將這些資料暴露出去,同時ViewModel
也可以釋出事件,供View
層訂閱.Model
層與MVP
中一致.
MVVM
的核心思想是觀察者模式,它通過事件
和轉移View
層資料持有權
來實現View
層與ViewModel
層的解耦.
在MVVM
中View
不是資料的實際持有者,它只負責資料如何呈現以及點選事件的傳遞,不做的資料處理工作,而資料的處理者和持有者變成ViewModel
,它通過接收View
層傳遞過來的時間改變自身狀態,發出事件或者改變自己持有的資料觸發View
的更新.
MVVM
解決了MVP
中的存在的一些問題,比如它無需定義介面,ViewModel
與View
層徹底無關更好複用,並且有Google
的Android Jetpack
作為強力後援.
但是MVVM
也有自己的缺點,那就是使用MVVM
的情況下ViewModel
與View
層的通訊變得更加困難了,所以在一些極其簡單
的頁面中請酌情
使用,否則就會有一種脫褲子放屁的感覺,在使用MVP
這個道理也依然適用.
2 DataBinding
2.1 坑
要用一個框架那麼就要先說它的坑
點.那就是不建議在使用DataBinding
的模組同時使用apply plugin: 'kotlin-kapt'
.
因為現在kapt
還有很多Bug
,使用kapt
時,在Windows
下DataBinding
格式下的xml
中如果包含有中文,會報UTF-8
相關的錯誤.
筆者一開始猜想這是由於JVM
啟動引數沒有設定成-Dfile.encoding=UTF-8
導致的,在gradle.properties
中改過了,無果,Stack Overflow
搜過了,沒找到,如果有大佬知道怎麼解決,還請指點一二
如果你在模組中同時使用kotlin
和DataBinding
是可以的,但是請一定不要使用kapt
,除非JB
那幫大佬搞定這些奇怪的問題.
這就意味這你所有的kotlin
程式碼都不能依賴註解處理器來為你的程式碼提供附加功能,但是你可以把這些程式碼換成等價的Java
實現,它們可以工作得很好.
2.2 DataBinding的相容性
先說一點,DataBinding
風格的xml
會有"奇怪"的東西入侵Android
原生的xml
格式,這種格式LayoutInfalter
是無法理解,但是,當你對這些奇怪的xml
使用LayoutInfalter#inflate
時亦不會報錯,並且佈局也正常載入了,這是為什麼呢?
這是因為在打包時,Gradle
通過APT
把你的DataBinding
風格的xml
全部翻譯了一遍,讓LayoutInfalter
能讀懂他們,正是因為這個相容的實現,而使得我們可以在使用和不使用DataBinding
間自由的切換.
2.3 DataBinding風格的XML
要想使用DataBinding
,先在模組的build.gradle
中新增
android{
//省略...
dataBinding {
enabled = true
}
}
複製程式碼
來啟用DataBinding
支援.
DataBinding
不需要額外的類庫支援,它被附加在你的android
外掛中,它的版本號與你的android
外掛版本一致.
classpath 'com.android.tools.build:gradle:3.3.2'
複製程式碼
在DataBinding
風格的xml
中,最外層必須是layout
標籤,並且不支援merge
標籤,編寫xml
就像下面這樣
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="text"
type="String"/>
<variable
name="action"
type="android.view.View.OnClickListener"/>
</data>
<TextView
android:onClick="@{action}"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<layout/>
複製程式碼
2.3.1 變數領域
data
標籤包裹的是變數領域,在這裡你可以使用variable
定義這個佈局所要繫結的變數型別,使用name
來指定變數名,然後用type
來指定其型別.
如果一些型別比較長,而且由需要經常使用你可以像Java
一樣使用import
匯入他們(java.lang.*
會被預設匯入),然後就不用寫出完全限定名了,就像這樣
<import
type="android.view.View"
alias="Action"/>
<variable
name="action"
type="Action"/>
複製程式碼
有必要時(比如名字衝突),你還可以用Action
為一個型別指定一個別名,這樣你就能在下文中使用這個別名.
2.3.2 轉義字元
熟悉xml
的同學可能都知道<
和>
在xml
中是非法字元,那麼要使用泛型的時候,我們就需要使用xml
中的轉義字元<
和>
來進行轉義
//↓錯誤,編譯時會報錯×
<variable
name="list"
type="java.util.List<String>"/>
//↓正確,可以通過編譯√
<variable
name="list"
type="java.util.List<String>"/>
複製程式碼
data
標籤結束後就是原本的佈局編寫的位置了,這部分基本和以前差不多,只是加入了DataBinding
表示式
<data>
//......
<data/>
<TextView
android:onClick="@{action}"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
複製程式碼
2.3.3 DataBinding表示式
以@{}
包裹的位置被稱為DataBinding
表示式,DataBinding
表示式幾乎支援Java
所有的運算子,並且增加了一些額外的操作,這允許我們在xml
中有一定的Java
程式設計體驗,學過Java web
的同學可能會覺得它很像JSP
:
- 不需要
xml
轉義的二元運算+
,-
,/
,*
,%
,||
,|
,^
,==
- 需要
xml
轉義的二元運算&&
,>>
>>>
,<<
,>
,<
,>=
,<=
,與泛型一樣運算子>=
,>
,<
,<=
等,也是需要轉義的,&
需要用&
轉義,這確實有些蹩腳,但這是xml
的侷限性,我們無法避免,所以在DataBinding
風格的xml
中應該儘可能的少用這些符號. lambda
表示式@{()->persenter.doSomething()}
- 三元運算
?:
null
合併運算子??
,若左邊不為空則選擇左邊,否則選擇右邊
android:text="@{nullableString??`This a string`}"
複製程式碼
- 自動匯入的
context
變數,你可以在xml
中的任意表示式使用context
這個變數,該Context
是從該佈局的根View
的getContext
獲取的,如果你設定了自己的context
變數,那麼將會覆蓋掉它 - 若表示式中有字串文字
xml
需要特殊處理
用單引號包圍外圍,表示式使用雙引號
android:text='@{"This a string"}'
或者使用`包圍字串,對,就Esc下面那個鍵的符號
android:text="@{`This a string`}"
複製程式碼
- 判斷型別
instanceof
- 括號
()
- 空值
null
- 方法呼叫,欄位訪問,以及
Getter
和Setter
的簡寫,比如User#getName
和User#setName
現在都可以直接寫成@{user.name}
,這種表示式也是最簡單的表示式,屬於直接賦值表示式 - 預設值
default
,在xml
中
`android:text="@{file.name, default=`no name`}"`
複製程式碼
- 下標
[]
,不只是陣列,List
,SparseArray
,Map
現在都可以使用該運算子 - 使用
@
讀取資原始檔,如下,但是不支援讀取mipmap
下的檔案
android:text="@{@string/text}"
//或者把它作為表示式的一部分
android:padding="@{large? @dimen/large : @dimen/small}"
複製程式碼
有一些資源需要顯示引用
型別 | 正常情況 | DataBinding表示式引用 |
---|---|---|
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
ColorStateList | @animator | @stateListAnimator |
StateListAnimator | @color | @colorStateList |
還有一些操作是DataBinding
表示式中沒有的,我們無法使用它們:
- 沒有
this
- 沒有
super
- 不能建立物件
new
- 不能使用泛型方法的顯示呼叫
Collections.<String>emptyList()
編寫簡單的DataBinding
表示式,就像下面這樣
<data>
<improt type="android.view.View"/>
<variable
name="isShow"
type="Boolean"/>
<data/>
<TextView
android:visibility="@{isShow?View.VISIBLE:View.GONE}"
android:text="@{@string/text}"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
複製程式碼
應該避免出現較為複雜的DataBinding
表示式,以全部都是直接賦值表示式為佳,資料的處理應該交給佈局控制器或者ViewModel
來做,佈局應該只負責渲染資料.
2.3.4 使用在Java中生成的ViewDataBinding
使用DataBinding
後Android Studio
會為每個xml
佈局生成一個繼承自ViewDataBinding
的子型別,來幫助我們將xml
檔案中定義的繫結關係對映到Java
中.
比如,如果你有一個R.layout.fragment_main
的佈局檔案,那麼他就會為你在當前包下生成一個,FragmentMainBinding
的ViewDataBinding
.
在Java
實化DataBinding
風格xml
佈局與傳統方式有所不同.
- 在
Actvity
中
private ActivityHostBinding mBinding;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_host);
}
複製程式碼
- 在自定義
View
和Fragment
中
private FragmentMainBinding mBinding;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
mBinding = DataBindingUtil.inflate(inflater,
R.layout.fragment_main,
container,
false);
return mBinding.getRoot();
}
複製程式碼
- 在已經使用普通
LayoutInfalter
例項化的View
上(xml
必須是DataBinding
風格的,普通LayoutInflater
例項化佈局時不會觸發任何繫結機制,DataBindingUtil#bind
才會發生繫結)
View view = LayoutInflater.from(context).inflate(R.layout.item_view,null,false);
ItemViewBinding binding = DataBindingUtil.bind(view);
複製程式碼
你在xml
設定的變數他會在這個類中為你生成對應的Getter
和Setter
.你可以呼叫它們給介面賦值,比如之前的我們定義的action
.
//這裡的程式碼是Java8的lambda
mBinding.setAction(v->{
//TODO
})
複製程式碼
2.3.5 使用BR檔案
它還會為你生成一個類似R
的BR
檔案,裡面包含了你在DataBinding
風格xml
中定義的所有變數名的引用(由於使用的是APT
生成,有時候需要Rebuild Project
才能重新整理),比如我們之前的action
,它會為我們生成BR.action
,我們可以這麼使用它
mBinding.setVariable(BR.action,new View.OnClickListener(){
@Override
void onClick(View v){
//TODO
}
})
複製程式碼
2.3.6 傳遞複雜物件
在之前給xml
中的變數中賦值時,我們用的都是一些類似String
的簡單物件,其實我們也可以定義一些複雜的物件,一次性傳遞到xml
佈局中
//java
public class File
{
public File(String name,
String size,
String path)
{
this.name = name;
this.size = size;
this.path = path;
}
public final String name;
public final String size;
public final String path;
}
//xml
<data>
<variable
name="file"
type="org.kexie.android.sample.bean.File"/>
<data/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView
android:text="@{file.name}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:text="@{file.size}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:text="@{file.path}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<LinearLayout/>
複製程式碼
個人認為繫結到xml
中的資料最好是不可變的,所以上面的欄位中我使用了final
,但這不是必須的,根據你自己的需求來進行定製
2.3.7 繫結並非立即發生
這裡有一點值得注意的是,你給ViewDataBinding
的賦值並不是馬上生效的,而是在當前方法執行完畢回到事件迴圈後,並保證在下一幀渲染之前得到執行,如果需要立即執行,請呼叫ViewDataBinding#executePendingBindings
2.3.8 使用android:id
如果你使用了android:id
,那麼這個View
就也可以當成一個變數在下文的DataBinding
表示式中使用,就像寫Java
.它還會幫你View
繫結到ViewDataBinding
中,你可以這麼使用它們
//xml
<TextView
android:id="@+id/my_text"
android:layout_width="match_parent"
android:layout_height="wrap_context"/>
<TextView
android:id="@+id/my_text2"
android:text="@{my_text.getText()}"
android:layout_width="match_parent"
android:layout_height="wrap_context"/>
//在java中my_text被去掉下劃線,更符合java的命名習慣
mBinding.myText.setText("This is a new text");
複製程式碼
用過ButterKnife的同學可能都知道,ButterKnife
出過一次與gradle
版本不相容的事故,但是DataBinding
是與gradle
打包在一起釋出的,一般不會出現這種問題,如果你不想用ButterKnife
但有不想讓DataBinding
的風格的寫法入侵你的xml
太狠的話,只使用android:id
將會是一個不錯的選擇.
2.4 正向繫結
某些第三方View
是肯定沒有適配DataBinding
的,業界雖然一直說MVVM
好,但現在MVP
的開發方式畢竟還是主流,雖然這種情況我們可以用android:id
,然後在Activity
/Fragment
中解決,但有時候我們想直接在xml
中配置,以消除一些樣板程式碼,這時候就需要自定義正向繫結.
2.4.1 自定義正向繫結介面卡
我們可以使用@BindingAdapter
自定義在xml
中可使用的View
屬性,名字空間是不需要的,加了反而還會給你警告.
@Target(ElementType.METHOD)
public @interface BindingAdapter {
/**
* 與此繫結介面卡關聯的屬性。
*/
String[] value();
/**
* 是否必須為每個屬性分配繫結表示式,或者是否可以不分配某些屬性。
* 如果為false,則當至少一個關聯屬性具有繫結表示式時,將呼叫BindingaAapter。
*/
boolean requireAll() default true;
}
//@BindingAdapter需要一個靜態方法,該方法的第一個引數是與該介面卡相容的View型別
//從第二個引數開始,依次是你自定義的屬性傳進來的值.
//使用requireAll來指定這些屬性是全部需要,還是隻要一個就可以
//如果requireAll = false,觸發介面卡繫結時,沒有被設定的屬性將獲得該型別的預設值
//框架優先使用自定義的介面卡處理繫結
@BindingAdapter(value = {"load_async", "error_handler"},requireAll = true)
public static void loadImage(ImageView view, String url, String error) {
Glide.with(view)
.load(url)
.error(Glide.with(view).load(error))
.into(view);
}
//在xml中使用它(下面那兩個網址都不是實際存在的)
<ImageView
load_async="@{`http://android.kexie.org/image.png`}"
error_handler="@{`http://android.kexie.org/error.png`}"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
複製程式碼
2.4.2 第三方View適配
DataBinding
風格的xml
還能在一定程度上適配第三方View
//如果你的自定義View中有這麼一個Setter↓
public class RoundCornerImageView extends AppCompatImageView{
//......
public void setRadiusDp(float dp){
//TODO
}
}
//那麼你可以在xml中使用radiusDp來使用它
<org.kexie.android.ftper.widget.RoundCornerImageView
radiusDp="@{100}"
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="centerCrop"
android:src="@drawable/progress"/>
//它會自己為你去找名稱為setRadiusDp並且能接受100為引數的方法.
複製程式碼
2.4.3 xml中的屬性重定向
使用@BindingMethod
來將xml
屬性重定向:
@Target(ElementType.ANNOTATION_TYPE)
public @interface BindingMethod {
//需要重定向的View型別
Class type();
//需要重定向的屬性名
String attribute();
//需要重定向到的方法名
String method();
}
//這是DataBinding原始碼中,DataBinding對於系統自帶的TextView編寫的介面卡
//這是androidx.databinding.adapters.TextViewBindingAdapter的原始碼
@BindingMethods({
@BindingMethod(type = TextView.class, attribute = "android:autoLink", method = "setAutoLinkMask"),
@BindingMethod(type = TextView.class, attribute = "android:drawablePadding", method = "setCompoundDrawablePadding"),
@BindingMethod(type = TextView.class, attribute = "android:editorExtras", method = "setInputExtras"),
//......
})
public class TextViewBindingAdapter {
//......
}
//這樣就可以建立起xml中屬性與View中Setter的聯絡
複製程式碼
2.4.4 新增轉換層
使用@BindingConversion
為新增轉換層
@BindingConversion
public static ColorDrawable toDrawable(int color) {
return new ColorDrawable(color);
}
//可以把color整形轉換為android:src可接受的ColorDrawable型別
//但是轉換隻適用於直接的賦值
//如果你寫了複雜的表示式,比如使用了?:這種三元運算子
//那就照顧不到你了
複製程式碼
2.5 反向繫結
有正向繫結就一定有反向繫結,正向繫結和反向繫結一起構成了雙向繫結.
在我們之前編寫的DataBinding
表示式中,比如TextView
中android:text
之類的屬性我們都是直接賦值一個String
過去的,這就是正向繫結,我們給View
的值能夠直接反應到View
上,而反向繫結就是View
值的變化和也能反應給我們.
2.5.1 使用雙向繫結
所有使用之前所有使用@{}
包裹的都是正向繫結,而雙向繫結是@={}
,並且只支援變數,欄位,Setter
(比如User#setName
,就寫@={user.name}
)的直接編寫並且不支援複雜表示式
2.5.2 相容LiveData與ObservableField
實際上,android:text
不只能接受String
,當使用雙向繫結時,它也能接受MutableLiveData<String>
和ObservableField<String>
作為賦值物件,這種賦值會將TextView
的android:text
的變化繫結到LiveData(實際上是MutableLiveData)
或者是ObservableField
上,以便我們在View
的控制層(Activity
/Fragment
)更好地觀察他們的變化.
當然除了ObservableField
在androidx.databinding
包下還有不裝箱的ObservableInt
,ObservableFloat
等等.
但是為了支援LiveData
我們必須開啟第二版的DataBinding APT
.
在你的gradle.properties
新增
android.databinding.enableV2=true
複製程式碼
現在我們可以通過LiveData(實際上是MutableLiveData)
將android:text
的變化繫結到Activity
/Fragment
//xml
<data>
<variable
name="liveText"
type="MutableLiveData<String>">
<data/>
<TextView
android:text="@={text}"
android:layout_width="match_parent"
android:layout_height="wrap_context"/>
//然後在Activity/Fragment中
MutableLiveData<String> liveText = new MutableLiveData<String>();
mBinding.setLiveText(liveText);
liveText.observe(this,text->{
//TODO 觀察View層變化
});
複製程式碼
2.5.3 自定義反向繫結介面卡
下面我們回到androidx.databinding.adapters.TextViewBindingAdapter
的原始碼,繼續對自定義反向繫結介面卡進行分析.
//我們可以看到原始碼中使用了@InverseBindingAdapter自定義了一個反向繫結器
//指定了其屬性以及相關聯的事件
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
return view.getText().toString();
}
//併為這個事件新增了一個可接受InverseBindingListener的屬性
//為了說明方便,下面的程式碼已簡化,原始碼並非如此,但主要邏輯相同
@BindingAdapter(value = {"android:textAttrChanged"})
public static void setTextWatcher(TextView view , InverseBindingListener textAttrChanged){
view.addTextChangedListener(new TextWatcher(){
//......
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
textAttrChanged.onChange();
}
});
}
//至此android:text的反向繫結完成
//當你使用@={}時實際上是用android:textAttrChanged屬性向TextView設定了TextWatcher
//傳入的InverseBindingListener是反向繫結監聽器
//當呼叫InverseBindingListener的onChange時
//會呼叫@BindingAdapter所註解的方法將獲得資料並寫回到變數中.
複製程式碼
2.6 配合DataBinding打造通用RecyclerView.Adapter
下面進行一個小小的實戰吧,我們可以站在巨人的肩膀上造輪子.
//匯入萬能介面卡作為基類,可以大大豐富我們通用介面卡的功能
implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.46'
複製程式碼
由於基類很強大所以程式碼不多:
//X是泛型,可以是你在item中所使用的java bean
public class GenericQuickAdapter<X>
extends BaseQuickAdapter<X, GenericQuickAdapter.GenericViewHolder> {
//BR中的變數名
protected final int mName;
//layoutResId是DataBinding風格的xml
public GenericQuickAdapter(int layoutResId, int name) {
super(layoutResId);
mName = name;
openLoadAnimation();
}
@Override
protected void convert(GenericViewHolder helper, X item) {
//觸發DataBinding
helper.getBinding().setVariable(mName, item);
}
public static class GenericViewHolder extends BaseViewHolder {
private ViewDataBinding mBinding;
public GenericViewHolder(View view) {
super(view);
//繫結View獲得ViewDataBinding
mBinding = DataBindingUtil.bind(view);
}
@SuppressWarnings("unchecked")
public <T extends ViewDataBinding> T getBinding() {
return (T) mBinding;
}
}
}
//例項化
GenericQuickAdapter<File> adapter = new GenericQuickAdapter<>(R.layout.item_file,BR.file);
//在xml中使用起來就像這樣
<layout>
<data>
<variable
name="file"
type="org.kexie.android.sample.bean.File"/>
<data/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView
android:text="@{file.name}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:text="@{file.size}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:text="@{file.path}"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<LinearLayout/>
<layout/>
複製程式碼
3 Lifecycle
在Android
中,元件的管理元件的生命週期一直是一個比較麻煩的東西,而自Google
推出Android Jetpack
元件包以來,這個問題得到的比較妥善的解決,Lifecycle
元件後來也成為Android Jetpack
的核心。
3.1 匯入
以AndroidX
為例,要使用Lifecycle
元件,先在模組的build.gradle
檔案中新增依賴:
api 'androidx.lifecycle:lifecycle-extensions:2.1.0-alpha02'
複製程式碼
由於Lifecycle
元件由多個包構成,使用api
匯入時即可將其依賴的包全部匯入該模組,包括common
,livedata
,process
,runtime
,viewmodel
,service
等。
如果要使用Lifecycle
中的註解,你還需要新增如下註解處理器,以便在編譯時,完成對相應註解的處理。
annotationProcessor 'androidx.lifecycle:lifecycle-compiler:2.0.0'
複製程式碼
對於一個App
來說,使用Lifecycle
元件是沒有任何侵入性的,因為他已經天然的融合到Google
的appcompat
庫中了,而如今無論是什麼應用程式都幾乎離不開appcompat
,可以說整合Lifecycle
只是啟用了之前沒用過的功能罷了。
3.2 LifecycleOwner
LifecycleOwner
是Lifecycle
元件包中的一個介面,所有需要管理生命週期的型別都必須實現這個介面。
public interface LifecycleOwner
{
/**
* Returns the Lifecycle of the provider.
*
* @return The lifecycle of the provider.
*/
@NonNull
Lifecycle getLifecycle();
}
複製程式碼
但其實很多時候我們根本無需關心LifecycleOwner
的存在。在Android
中, Fragment
、Activity
、Service
都是具有生命週期的元件,但是Google
已經讓他們都實現了LifecycleOwner
這個介面,分別是androdx.fragment.app.Fragment
、AppCompatActivity
、androidx.lifecycle.LifecycleService
.
在專案中,只要繼承這些型別,可以輕鬆的通過LifecycleOwner#getLifecycle()
獲取到Lifecycle
例項.這是一種解耦實現,LifecycleOwner
不包含任何有關生命週期管理的邏輯,實際的邏輯都在Lifecycle
例項中,我們可以通過傳遞Lifecycle
例項而非LifecycleOwner
來防止記憶體洩漏.
而Lifecycle
這個類的只有這三個方法:
@MainThread
public abstract void removeObserver(@NonNull LifecycleObserver observer);
@MainThread
@NonNull
public abstract State getCurrentState();
@MainThread
public abstract void addObserver(@NonNull LifecycleObserver observer);
複製程式碼
getCurrentState()
可以返回當前該LifecycleOwner
的生命週期狀態,該狀態與LifecycleOwner
上的某些回撥事件相關,只會出現以下幾種狀態,在Java
中以一個列舉類抽象出來定義在Lifecycle
類中。
public enum State
{
DESTROYED,
INITIALIZED,
CREATED,
STARTED,
RESUMED;
}
複製程式碼
-
DESTROYED
,在元件的onDestroy
呼叫前,會變成該狀態,變成此狀態後將不會再出現任何狀態改變,也不會傳送任何生命週期事件 -
INITIALIZED
,建構函式執行完成後但onCreate
未執行時為此狀態,是最開始時的狀態 -
CREATED
,在onCreate
呼叫之後,以及onStop
呼叫前會變成此狀態 -
STARTED
,在onStart
呼叫之後,以及onPause
呼叫前會變成此狀態 -
RESUMED
,再onResume
呼叫之後會變成此狀態
addObserver
,此方法可以給LifecycleOwner
新增一個觀察者,來接收LifecycleOwner
上的回撥事件。回撥事件也是一個列舉,定義在Lifecycle
類中:
public enum Event
{
/**
* Constant for onCreate event of the {@link LifecycleOwner}.
*/
ON_CREATE,
/**
* Constant for onStart event of the {@link LifecycleOwner}.
*/
ON_START,
/**
* Constant for onResume event of the {@link LifecycleOwner}.
*/
ON_RESUME,
/**
* Constant for onPause event of the {@link LifecycleOwner}.
*/
ON_PAUSE,
/**
* Constant for onStop event of the {@link LifecycleOwner}.
*/
ON_STOP,
/**
* Constant for onDestroy event of the {@link LifecycleOwner}.
*/
ON_DESTROY,
/**
* An {@link Event Event} constant that can be used to match all events.
*/
ON_ANY
}
複製程式碼
每種事件都對應著Fragment
/Activity
中的事件。
3.3 LifecycleObserver
LifecycleObserver
是生命週期的觀察者,可能是這個包中我們最常用的介面了.
檢視原始碼得知,他就是一個空介面,不包含任何實現,但是若我們想使用,還是得繼承此介面。
public interface LifecycleObserver { }
複製程式碼
繼承LifecycleObserver
後使用@OnLifecycleEvent
註解(這時之前申明得註解處理器派上了用場),並設定需要監聽的生命週期回撥事件。
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void test()
{
///TODO...
}
複製程式碼
然後在Activity
/Fragment
中:
getLifecycle().addObserver(yourLifecycleObserver);
複製程式碼
即可在執行時收到相應的的回撥事件,但是注意新增@OnLifecycleEvent
註解的方法應該是包內訪問許可權或是public
的,否則可能在編譯時會報錯,或者收不到回撥。
若想在執行時移除LifecycleObserver
,同樣也還有Lifecycle#removeObserver
方法。
4 LiveData
LiveData
是對Android
元件生命週期感知的粘性事件
,也就是說,在LiveData
持有資料時,你去訂閱它就能收到他最後一次接收到的資料.在實戰中,我們能用到的LiveData
一般是它的兩個子類MutableLiveData
和MediatorLiveData
.
4.1 LiveData基本使用
我們可以通過LiveData#observe
來觀察它所持有的值的變化,還可以通過LiveData#getValue
來直接獲取內部儲存的值(非執行緒安全)
//LiveData 一般是用來給ViewModel儲存資料的
public class MyViewModel extends ViewModel{
private MutableLiveData<Boolean> mIsLoading = new MutableLiveData<>();
LiveData<Boolean> isLoading(){
return mIsLoading;
}
}
//Activity/Fragment觀察ViewModel
mViewModel.isLoading().observe(this, isLoading -> {
//TODO 發生在主執行緒,觸發相關處理邏輯
});
//LiveData是依賴Lifecycle實現的
//傳入的this是LifecycleOwner
//LiveData只會通知啟用態的(STARTED和RESUMED)的LifecycleOwner
//並且在Activity/Fragment被重建也能重新接收到LiveData儲存的資料
//在元件DESTROYED時,LiveData會把它移出觀察者列表
//當然你也可以不關聯LifecycleOwner,讓訂閱一直保持.
//需要這樣時需要使用observeForever
mViewModel.isLoading().observeForever(isLoading -> {
//TODO
});
//這個訂閱永遠不會被取消
//除非你顯示呼叫LiveData#removeObserver
複製程式碼
4.2 MutableLiveData
顧名思義就是可變的LiveData
,基類LiveData
預設是不可變的,MutableLiveData
開放了能夠改變其內部所持有資料的介面.
public class MutableLiveData<T> extends LiveData<T> {
/**
* Creates a MutableLiveData initialized with the given {@code value}.
*
* @param value initial value
*/
public MutableLiveData(T value) {
super(value);
}
/**
* Creates a MutableLiveData with no value assigned to it.
*/
public MutableLiveData() {
super();
}
@Override
public void postValue(T value) {
super.postValue(value);
}
@Override
public void setValue(T value) {
super.setValue(value);
}
}
複製程式碼
分別是postValue
和setValue
,其中setValue
內部檢查執行緒是否為主執行緒,不允許在子執行緒中使用,用了就報錯.postValue
會將值通過主執行緒的Handler
轉發到主執行緒上.
LiveData
可以有初始值,也可以沒有,如果在沒有初始值的情況下被訂閱,則訂閱者不會收到任何的值.
4.3 MediatorLiveData
MediatorLiveData
繼承自MutableLiveData
,它主要用來實現多個LiveData
資料來源的合併.
public class MediatorLiveData<T> extends MutableLiveData<T> {
private SafeIterableMap<LiveData<?>, Source<?>> mSources = new SafeIterableMap<>();
@MainThread
public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged) {
Source<S> e = new Source<>(source, onChanged);
Source<?> existing = mSources.putIfAbsent(source, e);
if (existing != null && existing.mObserver != onChanged) {
throw new IllegalArgumentException(
"This source was already added with the different observer");
}
if (existing != null) {
return;
}
if (hasActiveObservers()) {
e.plug();
}
}
@MainThread
public <S> void removeSource(@NonNull LiveData<S> toRemote) {
Source<?> source = mSources.remove(toRemote);
if (source != null) {
source.unplug();
}
}
@CallSuper
@Override
protected void onActive() {
for (Map.Entry<LiveData<?>, Source<?>> source : mSources) {
source.getValue().plug();
}
}
@CallSuper
@Override
protected void onInactive() {
for (Map.Entry<LiveData<?>, Source<?>> source : mSources) {
source.getValue().unplug();
}
}
private static class Source<V> implements Observer<V> {
final LiveData<V> mLiveData;
final Observer<? super V> mObserver;
int mVersion = START_VERSION;
Source(LiveData<V> liveData, final Observer<? super V> observer) {
mLiveData = liveData;
mObserver = observer;
}
void plug() {
mLiveData.observeForever(this);
}
void unplug() {
mLiveData.removeObserver(this);
}
@Override
public void onChanged(@Nullable V v) {
if (mVersion != mLiveData.getVersion()) {
mVersion = mLiveData.getVersion();
mObserver.onChanged(v);
}
}
}
}
複製程式碼
它比MutableLiveData
多了兩個方法addSource
和removeSource
,通過這兩個方法我們可以將其他LiveData
合併到此LiveData
上,當其他LiveData
發生改變時,此LiveData
就能收到通知.
@MainThread
public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged)
@MainThread
public <S> void removeSource(@NonNull LiveData<S> toRemote)
複製程式碼
通過檢視原始碼,我們可以知道在有觀察者時LiveData#onActive
會被回撥,MediatorLiveData
會在內部迭代,用observeForever
訂閱所有被合併進來的LiveData
,這樣就能接收所有LiveData
的變化,在沒有觀察者時LiveData#onInactive
會被回撥,此時執行反操作removeObserver
.
4.4 變換
使用androidx.lifecycle.Transformations
這個工具類可以將持有一種型別的LiveData
轉換為另一種LiveData
.他有類似於RxJava
的使用方式.
LiveData<Boolean> boolLiveData = getBoolLiveData();
LiveData<String> stringLiveData = Transformations.map(boolLiveData,bool->Boolean.toString(bool));
複製程式碼
上面只是一個演示,實際上可以執行更為複雜的邏輯,並且這種轉換是惰性的,在沒有啟用態觀察者時,這種轉換不會發生.
5 ViewModel
5.1 自定義ViewModel
ViewModel
其實沒什麼可說的,其原始碼主要的部分其實就只有這些
public abstract class ViewModel {
protected void onCleared() {
}
}
複製程式碼
簡直一目瞭然,我們可以在ViewModel
上使用LiveData
作為欄位儲存資料,並編寫業務邏輯
(資料處理邏輯).就像這樣
public class MyViewModel extends ViewModel
{
public MutableLiveData<String> username = new MutableLiveData<>();
public MutableLiveData<String> password = new MutableLiveData<>();
public MutableLiveData<String> text = new MutableLiveData<>();
public void action1(){
//TODO
}
public void initName(){
username.setValue("Luke Luo");
}
//......
@Override
protected void onCleared() {
//TODO 清理資源
}
}
複製程式碼
onCleared
會在元件銷燬的時候回撥,我們可以重寫這個方法在ViewModel
銷燬時新增一些自定義清理邏輯.
ViewModel
還有一個子類AndroidViewModel
也是一目瞭然,只是儲存了Application
例項而已.
public class AndroidViewModel extends ViewModel {
@SuppressLint("StaticFieldLeak")
private Application mApplication;
public AndroidViewModel(@NonNull Application application) {
mApplication = application;
}
/**
* Return the application.
*/
@SuppressWarnings("TypeParameterUnusedInFormals")
@NonNull
public <T extends Application> T getApplication() {
//noinspection unchecked
return (T) mApplication;
}
}
複製程式碼
5.2 自定義ViewModel構造方式
我們可以通過ViewModelProviders
來獲取ViewModel
,這樣獲取的ViewModel
會繫結元件的生命週期(即在銷燬時自動呼叫onCleared
)
mViewModel = ViewModelProviders.of(this).get(CustomViewModel.class);
複製程式碼
在Android
的Lifecycle
實現中框架向Activity
中新增了一個繼承了系統Fragment
的ReportFragment
來彙報元件的生命週期,如果你使用的是appcompat
的Fragment
,那麼它對你就是不可見的,所以一定要避免使用系統的Fragment
(在API28
中已被標記為棄用).
ViewModel
通過Lifecycle
來管理自身釋放,在元件的ON_DESTROY
事件來到時,它的onCleared()
也會被呼叫.
如果你想有自定義建構函式引數的ViewModel
那你就得繼承ViewModelProvider.AndroidViewModelFactory
了
//自定義建構函式的ViewModel
public class NaviViewModel extends AndroidViewModel
{
private AMapNavi mNavi;
public NaviViewModel(AMapNavi navi,Application application)
{
super(application);
mNavi = navi;
}
//......
}
//繼承並重寫create
public final class NaviViewModelFactory
extends ViewModelProvider.AndroidViewModelFactory
{
private final AMapNavi navi;
private final Application application;
public NaviViewModelFactory(@NonNull Context context, AMapNavi navi)
{
super((Application) context.getApplicationContext());
this.application = (Application) context.getApplicationContext();
this.navi = navi;
}
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass)
{
try
{
Constructor<T> constructor = modelClass
.getConstructor(Application.class, AMapNavi.class);
return constructor.newInstance(application, navi);
} catch (Exception e)
{
return super.create(modelClass);
}
}
}
//使用
NaviViewModelFactory factory = new NaviViewModelFactory(context, navi);
mViewModel = ViewModelProviders.of(this, factory).get(NaviViewModel.class);
複製程式碼
說白了就是反射呼叫建構函式建立,也是一目瞭然.
6 RxJava
本篇文章只是針對響應式程式設計在MVVM
體系下的應用,不對RxJava
展開深度討論,但是後面還會專門出一篇文章討論RxJava
的有關知識.
RxJava
在MVVM
中主要用於釋出事件,下面是需要注意的一些點.
6.1 使用AutoDispose
RxJava
是響應式程式設計這種思想在JVM
這個平臺上的實現,所以它一開始並沒有為Android
平臺的特點而做出優化.
就像上面所介紹過的一樣,Android
的元件是有明確的生命週期的,如果在元件銷燬後,RxJava
仍有後臺執行緒
在執行且你的Observer
引用了你的Activity
,就會造成記憶體洩漏.
但其實RxJava
是提供了釋放機制的,那就是Disposeable
,只不過這個實現這個機制的邏輯需要我們手動在Activity#onDestroy
中進行硬編碼,這會帶來大量的樣板程式碼.
為了解決這一局面,在Android Jetpack
還沒有誕生的時候,有大神開發了RxLifecycle,但是這個框架需要強制繼承基類,對於一些現有專案的改造來說,其實是不太友好的,個人感覺並沒有從根本上解決問題.
Android Jetpack
誕生後AutoDispose給了我們另外一條出路.它使用RxJava2
中的as
運算子,將訂閱者
轉換成能夠自動釋放
的訂閱者物件
.
在你的build.gradle
中新增依賴:
implementation 'io.reactivex.rxjava2:rxjava:2.2.6'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'com.uber.autodispose:autodispose:1.1.0'
implementation 'com.uber.autodispose:autodispose-android-archcomponents:1.1.0'
複製程式碼
一個簡單的示例:
Observable.just(new Object())
//使用AutoDispose#autoDisposable
//並使用AndroidLifecycleScopeProvider#form
//指定LifecycleOwner和需要在哪一個事件進行銷燬
//關鍵↓是這行
.as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(activity, Lifecycle.Event.ON_DESTROY)))
.subscribe();
複製程式碼
上面程式碼的時間訂閱將會在元件的Lifecycle.Event.ON_DESTROY
事件來到時被釋放,當然你也可以指定其他事件時釋放.
6.2 防止多重點選
首先你可以使用JW大神
的RxBinding來實現這一需求,但是今天我們不討論RxBinding
,因為網上的討論RxBinding
的文章已經太多了,隨便抓一篇出來都已經非常優秀.
今天我們模仿RxBinding
實現一個簡單的,輕量化的,基於Java動態代理
的,並且相容所有第三方View
所自定義Listener
介面的防止多重點選機制.
二話不說先上程式碼:
import androidx.collection.ArrayMap;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import com.uber.autodispose.AutoDispose;
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider;
import io.reactivex.subjects.PublishSubject;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static io.reactivex.android.schedulers.AndroidSchedulers.mainThread;
public final class RxOnClick<X>
{
//預設最低的可取的時間
private static final int MINI_TIME = 200;
private final Class<X> mInterface;
private X mInner;
private LifecycleOwner mOwner;
private int mTime;
private Lifecycle.Event mEvent;
private RxOnClick(Class<X> type)
{
mInterface = type;
}
//從一個建立介面型別建立
public static <X> RxOnClick<X> create(Class<X> type)
{
return new RxOnClick<>(type);
}
//實際處理事件的Listener
public RxOnClick<X> inner(X inner)
{
mInner = inner;
return this;
}
//依附於的元件也就是LifecycleOwner
public RxOnClick<X> owner(LifecycleOwner owner)
{
mOwner = owner;
return this;
}
//只去time毫秒內的第一個結果作為有效結果
public RxOnClick<X> throttleFirst(int time)
{
mTime = time;
return this;
}
//在哪一個事件進行釋放
public RxOnClick<X> releaseOn(Lifecycle.Event event)
{
mEvent = event;
return this;
}
//建立代理類例項
@SuppressWarnings("unchecked")
public X build()
{
//檢查引數
if (mInterface == null || !mInterface.isInterface())
{
throw new IllegalArgumentException();
}
if (mTime < MINI_TIME)
{
mTime = MINI_TIME;
}
if (mEvent == null)
{
mEvent = Lifecycle.Event.ON_DESTROY;
}
if (mOwner == null || mInner == null)
{
throw new IllegalStateException();
}
//用反射遍歷獲取所有方法
Map<Method, PublishSubject<Object[]>> subjectMap = new ArrayMap<>();
for (Method method : mInterface.getDeclaredMethods())
{
PublishSubject<Object[]> subject = PublishSubject.create();
subject.throttleFirst(mTime, TimeUnit.MILLISECONDS)
.observeOn(mainThread())
.as(AutoDispose.autoDisposable(AndroidLifecycleScopeProvider.from(mOwner, mEvent)))
.subscribe(args -> method.invoke(mInner, args));
subjectMap.put(method, subject);
}
//使用動態代理代理代理該介面並使用PublishSubject進行轉發
return (X) Proxy.newProxyInstance(mInterface.getClassLoader(),
new Class[]{mInterface},
(proxy, method, args) -> {
//Object類的方法直接呼叫
if (Object.class.equals(method.getDeclaringClass()))
{
return method.invoke(proxy, args);
}
//否則轉換為Rx事件流
PublishSubject<Object[]> subject = subjectMap.get(method);
if (subject != null)
{
subject.onNext(args);
}
return null;
});
}
}
複製程式碼
上面類在設計上採用了Builder
模式,所以它實際是一個Builder
.
其核心原理就是使用Java的動態代理
機制建立Listener
的代理類,代理類不處理事件,而是將事件通過PublishSubject
(釋放訂閱後接收到的事件)轉換為RxJava
事件流推送到真正處理事件的Listener
上.
這樣我們就可以在這個事件流上對事件做手腳了,並且這樣還能相容RxBinding
所不能相容的第三方自定義View
.
比如上面就加入了xxx毫秒內只取第一次點選和繫結元件的生命週期,用起來的時候就像是下面,依然非常簡潔並且非常的有用:
View.OnClickListener listener = RxOnClick
.create(View.OnClickListener.class)
.owner(this)
.inner(v -> {
//TODO
})
.build();
複製程式碼
7 使用MVVM改造Android現有體系
筆者就Android
現有體系
下的各種類庫
和框架
,通過自己實踐的得出的經驗將其進行如下歸類,觀點僅供參考,在實踐中應該視專案特點進行適當進行改造.
7.1 View層
現有體系下的內容:
Activity/Fragment
(佈局生命週期與邏輯控制器)android.view.View
及其子類
設計原則:
View
層不應該承擔處理資料的責任,它應該只負責資料如何顯示.- 它不應該直接持有
Model
層的任何引用,也不應該直接持有Model
層的資料. View
層正常的行為應該是觀察某個ViewModel
,間接獲取該ViewModel
從Model
層中獲取並處理過能在View
層上直接顯示的資料,資料由ViewModel
儲存,這樣可以保證在Activity
重建時頁面上有關的資料不會丟失而且也不會造成View
層與Model
層的耦合.
7.2 DataBinding
現有體系下的內容:
Jetpack DataBinding
函式庫View
的Adapter
- ......
設計原則:
- 理想狀態下,
DataBinding
與View
構建的關係應該是資料驅動的,即只要資料不改變View
層實現的變更不會導致邏輯的重新編寫(如把TextView
改成EditText
也不需要修改一行程式碼). - 雖然
DataBinding
函式庫已經完成了大多數DataBinding
應該做的事,但是不要為了資料驅動而排斥使用android:id
來獲取View
並對View
直接賦值,雖然這不夠資料驅動,但是適當使用是可以的,畢竟Android
的View
層目前還沒有辦法做到完全的資料驅動(主要是第三方庫的相容問題). Adapter
應該屬於DataBinding
的一種,與DataBinding
函式庫中生成的DataBinding
相同,它也是使用資料來觸發View
層的改變.所以儘可能不要把它寫到ViewModel
中,但這不是必須的,做在對List
操作要求比較高的情況下可以寫到ViewModel
中,但要保證一個原則——ViewModel
應該只負責提供資料,而不應該知道這些資料要與何種View
進行互動.
7.3 事件傳遞
現有體系下的內容:
EventBus
事件匯流排RxJava
事件流
設計原則:
Jetpack
中實現的LiveData
能夠很好的作為資料持有者,並且是生命週期感知的,但是有些時候我們需要向View
層傳送一些單次的資料,這時LiveData
並不能夠很好地工作.Rxjava
和EventBus
是更好的選擇.
7.4 ViewModel層
現有體系下的內容:
Jetpack ViewModel
Jetpack LiveData
- 用於將
Model
資料轉換成View
能直接顯示的資料的工具類 - ......
設計原則:
ViewModel
通常應該使用LiveData
持有View
層資料的實際控制權ViewModel
可以包含操作,但是ViewModel
不應該直接或者間接地引用View
,即使是方法中的引數也最好不要,因為ViewModel
不應該知道自己到底是與哪一個View
進行互動.ViewModel
與Model
的關係應該是——將Model
層產生的資料翻譯
成View
層能夠直接消化吸收的資料。ViewModel
可以向View
層傳送事件,然後View
可以訂閱這些事件以收到ViewModel
層的通知.
7.5 Model層
現有體系下的內容:
- 部分與
Activity
無關的系統服務 Room
(SQLite
資料庫)Retrofit
(網路資料)SharedPreferences
- ......
設計原則:
- 涉及
Activity
請一定不要包含進來,如WindowManager
,它們屬於View
層. Model
層主要是原始資料的來源,由於儲存格式/傳輸格式
與顯示格式
存在的巨大差異,View
層往往並不能很好的直接消化這些資料,這時就需要一個中間人
作為翻譯
,由此抽象出了ViewModel
.
8 實戰
我編寫了一個簡單的FTP
客戶端作為本次MVVM
博文的演示Demo
,該專案簡單實踐了QMUI
+MVVM
+DataBinding
+RxJava
+LiveData
+Room
的技術棧並由kotlin
和Java
混編寫成,支援斷點續傳,程式碼質量比較一般,有愛自取.
9 參考資料以及擴充套件閱讀
10 結語
有些日子沒更新文章了,最近發生了一些事讓筆者徹底從無限的狂熱中冷靜下來,開始耐心做事.
本篇文章多達10000+
字,感謝您在百忙之中抽空觀看.所有內容均為個人學習總結與理解,僅供參考.
如果喜歡
我的文章
別忘了給我點個贊,拜託了這對我來說真的很重要.