本文包含Android
中MVVM
體系中的不少部分,主要對ViewModel
+DataBinding
+RxJava
+LiveData
+Lifecycle
等筆者所使用的技術體系進行解析.java
本文字數較多,內容較爲完整而且後續還會追加更新,閱讀本篇文章須要較長時間,建議讀者分段閱讀.react
全部文字均爲我的學習總結和理解,僅供參考,若有紕漏還請指出,筆者不勝感激.android
Android Studio
版本=3.2
Jetpack
最低兼容到Android
=2.1
,API
=7
要回答這個問題首先就要介紹MVC
與MVP
這兩種模式,從MVC
到MVVM
其實你們想的都是怎麼把Model
和View
儘量的拆開(熟悉三者定義的朋友能夠跳過該節).git
MVC
(Model
-View
-Controller
)即傳統Android
開發中最經常使用的模式:github
Activity
/Fragment
做爲Controller
層,android.view.View
的子類以xml
構建文件構建起的佈局
做爲View
層SQLite
數據庫,網絡請求做爲Model
層.但因爲Activity
/Fragment
的功能過於強大
而且實際上包含了部分View
層功能,致使最後Activity
/Fragment
既承擔了View
的責任,又承擔了Controller
的責任.因此通常較複雜的頁面,Activity
/Fragment
很容易堆積代碼,最終致使Controller
混雜了View
層和業務邏輯(也就是大家所知道的一個Activity
三千行)web
在MVC
中View
層與Model
幾乎幾乎徹底沒有隔離,View
層能夠直接操做Model
層,Model
層的回調
裏也可能會直接給View
賦值.Controller
的概念被弱化,最後只剩下MV
沒有C
了.數據庫
這也將致使但你想把某個界面上的元素進行更新時,他會牽扯到一堆跟Model
層相關的代碼,這個問題在你變動Model
層的時候一樣也會出現,這個問題實際上是沒有很好的將邏輯分層致使的.編程
MVP
(Model
-View
-Presenter
)架構設計,是當下最流行的開發模式,目前主要以Google
推出的TodoMVP
爲主,MVP
不是一種框架,它實際上更相似一種分層思想
,一種接口約定
,具體體如今下面:api
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
,很難看.
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
這個道理也依然適用.
要用一個框架那麼就要先說它的坑
點.那就是不建議在使用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
實現,它們能夠工做得很好.
先說一點,DataBinding
風格的xml
會有"奇怪"的東西入侵Android
原生的xml
格式,這種格式LayoutInfalter
是沒法理解,可是,當你對這些奇怪的xml
使用LayoutInfalter#inflate
時亦不會報錯,而且佈局也正常加載了,這是爲何呢?
這是由於在打包時,Gradle
經過APT
把你的DataBinding
風格的xml
所有翻譯了一遍,讓LayoutInfalter
能讀懂他們,正是由於這個兼容的實現,而使得咱們能夠在使用和不使用DataBinding
間自由的切換.
要想使用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/>
複製代碼
data
標籤包裹的是變量領域,在這裏你可使用variable
定義這個佈局所要綁定的變量類型,使用name
來指定變量名,而後用type
來指定其類型.
若是一些類型比較長,並且由須要常用你能夠像Java
同樣使用import
導入他們(java.lang.*
會被默認導入),而後就不用寫出徹底限定名了,就像這樣
<import
type="android.view.View"
alias="Action"/>
<variable
name="action"
type="Action"/>
複製代碼
有必要時(好比名字衝突),你還能夠用Action
爲一個類型指定一個別名,這樣你就能在下文中使用這個別名.
熟悉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"/>
複製代碼
以@{}
包裹的位置被稱爲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
來作,佈局應該只負責渲染數據.
使用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
})
複製代碼
它還會爲你生成一個相似R
的BR
文件,裏面包含了你在DataBinding
風格xml
中定義的全部變量名的引用(因爲使用的是APT
生成,有時候須要Rebuild Project
才能刷新),好比咱們以前的action
,它會爲咱們生成BR.action
,咱們能夠這麼使用它
mBinding.setVariable(BR.action,new View.OnClickListener(){
@Override
void onClick(View v){
//TODO
}
})
複製代碼
在以前給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
,但這不是必須的,根據你本身的需求來進行定製
這裏有一點值得注意的是,你給ViewDataBinding
的賦值並非立刻生效的,而是在當前方法執行完畢回到事件循環後,並保證在下一幀渲染以前獲得執行,若是須要當即執行,請調用ViewDataBinding#executePendingBindings
若是你使用了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
將會是一個不錯的選擇.
某些第三方View
是確定沒有適配DataBinding
的,業界雖然一直說MVVM
好,但如今MVP
的開發方式畢竟仍是主流,雖然這種狀況咱們能夠用android:id
,而後在Activity
/Fragment
中解決,但有時候咱們想直接在xml
中配置,以消除一些樣板代碼,這時候就須要自定義正向綁定.
咱們可使用@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"/>
複製代碼
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爲參數的方法.
複製代碼
使用@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的聯繫
複製代碼
使用@BindingConversion
爲添加轉換層
@BindingConversion
public static ColorDrawable toDrawable(int color) {
return new ColorDrawable(color);
}
//能夠把color整形轉換爲android:src可接受的ColorDrawable類型
//可是轉換隻適用於直接的賦值
//若是你寫了複雜的表達式,好比使用了?:這種三元運算符
//那就照顧不到你了
複製代碼
有正向綁定就必定有反向綁定,正向綁定和反向綁定一塊兒構成了雙向綁定.
在咱們以前編寫的DataBinding
表達式中,好比TextView
中android:text
之類的屬性咱們都是直接賦值一個String
過去的,這就是正向綁定,咱們給View
的值可以直接反應到View
上,而反向綁定就是View
值的變化和也能反應給咱們.
全部使用以前全部使用@{}
包裹的都是正向綁定,而雙向綁定是@={}
,而且只支持變量,字段,Setter
(好比User#setName
,就寫@={user.name}
)的直接編寫而且不支持複雜表達式
實際上,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層變化
});
複製代碼
下面咱們回到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所註解的方法將得到數據並寫回到變量中.
複製代碼
下面進行一個小小的實戰吧,咱們能夠站在巨人的肩膀上造輪子.
//導入萬能適配器做爲基類,能夠大大豐富咱們通用適配器的功能
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/>
複製代碼
在Android
中,組件的管理組件的生命週期一直是一個比較麻煩的東西,而自Google
推出Android Jetpack
組件包以來,這個問題獲得的比較妥善的解決,Lifecycle
組件後來也成爲Android Jetpack
的核心。
以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
只是啓用了以前沒用過的功能罷了。
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
中的事件。
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
方法。
LiveData
是對Android
組件生命週期感知的粘性事件
,也就是說,在LiveData
持有數據時,你去訂閱它就能收到他最後一次接收到的數據.在實戰中,咱們能用到的LiveData
通常是它的兩個子類MutableLiveData
和MediatorLiveData
.
咱們能夠經過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
複製代碼
顧名思義就是可變的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
能夠有初始值,也能夠沒有,若是在沒有初始值的狀況下被訂閱,則訂閱者不會收到任何的值.
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
.
使用androidx.lifecycle.Transformations
這個工具類能夠將持有一種類型的LiveData
轉換爲另外一種LiveData
.他有相似於RxJava
的使用方式.
LiveData<Boolean> boolLiveData = getBoolLiveData();
LiveData<String> stringLiveData = Transformations.map(boolLiveData,bool->Boolean.toString(bool));
複製代碼
上面只是一個演示,實際上能夠執行更爲複雜的邏輯,而且這種轉換是惰性的,在沒有激活態觀察者時,這種轉換不會發生.
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;
}
}
複製代碼
咱們能夠經過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);
複製代碼
說白了就是反射調用構造函數建立,也是一目瞭然.
本篇文章只是針對響應式編程在MVVM
體系下的應用,不對RxJava
展開深度討論,可是後面還會專門出一篇文章討論RxJava
的有關知識.
RxJava
在MVVM
中主要用於發佈事件,下面是須要注意的一些點.
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
事件來到時被釋放,固然你也能夠指定其餘事件時釋放.
首先你可使用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();
複製代碼
筆者就Android
現有體系
下的各類類庫
和框架
,經過本身實踐的得出的經驗將其進行以下歸類,觀點僅供參考,在實踐中應該視項目特色進行適當進行改造.
現有體系下的內容:
Activity/Fragment
(佈局生命週期與邏輯控制器)android.view.View
及其子類設計原則:
View
層不該該承擔處理數據的責任,它應該只負責數據如何顯示.Model
層的任何引用,也不該該直接持有Model
層的數據.View
層正常的行爲應該是觀察某個ViewModel
,間接獲取該ViewModel
從Model
層中獲取並處理過能在View
層上直接顯示的數據,數據由ViewModel
保存,這樣能夠保證在Activity
重建時頁面上有關的數據不會丟失並且也不會形成View
層與Model
層的耦合.現有體系下的內容:
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
進行交互.現有體系下的內容:
EventBus
事件總線RxJava
事件流設計原則:
Jetpack
中實現的LiveData
可以很好的做爲數據持有者,而且是生命週期感知的,可是有些時候咱們須要向View
層發送一些單次的數據,這時LiveData
並不可以很好地工做.Rxjava
和EventBus
是更好的選擇.現有體系下的內容:
Jetpack ViewModel
Jetpack LiveData
Model
數據轉換成View
能直接顯示的數據的工具類設計原則:
ViewModel
一般應該使用LiveData
持有View
層數據的實際控制權ViewModel
能夠包含操做,可是ViewModel
不該該直接或者間接地引用View
,即便是方法中的參數也最好不要,由於ViewModel
不該該知道本身究竟是與哪個View
進行交互.ViewModel
與Model
的關係應該是——將Model
層產生的數據翻譯
成View
層可以直接消化吸取的數據。ViewModel
能夠向View
層發送事件,而後View
能夠訂閱這些事件以收到ViewModel
層的通知.現有體系下的內容:
Activity
無關的系統服務Room
(SQLite
數據庫)Retrofit
(網絡數據)SharedPreferences
設計原則:
Activity
請必定不要包含進來,如WindowManager
,它們屬於View
層.Model
層主要是原始數據的來源,因爲存儲格式/傳輸格式
與顯示格式
存在的巨大差別,View
層每每並不能很好的直接消化這些數據,這時就須要一個中間人
做爲翻譯
,由此抽象出了ViewModel
.我編寫了一個簡單的FTP
客戶端做爲本次MVVM
博文的演示Demo
,該項目簡單實踐了QMUI
+MVVM
+DataBinding
+RxJava
+LiveData
+Room
的技術棧並由kotlin
和Java
混編寫成,支持斷點續傳,代碼質量比較通常,有愛自取.
有些日子沒更新文章了,最近發生了一些事讓筆者完全從無限的狂熱中冷靜下來,開始耐心作事.
本篇文章多達10000+
字,感謝您在百忙之中抽空觀看.全部內容均爲我的學習總結與理解,僅供參考.
若是喜歡
個人文章
別忘了給我點個贊,拜託了這對我來講真的很重要.