前言
由於我司項目較老有很多歷史包袱代碼結構也比較混亂,需求複雜的頁面動輒activity中1000多行,看着很是頭疼,於是趁着加班提前做完需求餘下的時間學習了mvvm對項目部分功能進行了改造,目前已經使用3個版本了,本篇博文分享下我使用的感受。
準備
這裏先說說關於mvvm的幾個問題(如有不對請輕噴 (╹▽╹))
-
首先說說我爲啥選擇mvvm而不是熟知的mvp。
主要原因是我覺得mvp接口寫起來有點麻煩,針對ui和model都得寫接口,然後這個粒度不好控制如果太細了就得寫一堆接口,太粗了又沒有複用性,並且presenter持有了ui引用在更新ui的時候還得考慮生命週期,還有activity引用的處理防止內存泄露這些問題我都覺得挺麻煩的而MvvM中databinding框架處理好了這些問題,所以我選擇了更加方便的mvvm,當然mvvm也不是沒有缺點下面會說到。
-
mvvm優缺點
- 優點:
- 數據源被強化,利用databinding框架實現雙向綁定技術,當數據變化的時候ui自動更新,ui上用戶操作數據自動更新,很好的做到數據的一致性。
- xml和activity處理ui操作、model提供數據、vm處理業務邏輯,各個層級分工明確,activity中代碼大大減少項目整體結構更加清晰。
- 很方便做ui的a/b測試可以共用同一個vm。
- 方便單元測試ui和vm邏輯完全分離。
- 缺點:
- bug很難被調試,數據綁定使得一個bug被傳遞到別的位置,要找到bug的原始位置不太容易。
- 由於要遵守模式的規範調用流程變得複雜。
- vm中會有很多被觀察者變量如果業務邏輯非常複雜會消耗更多內存。
- 優點:
-
mvvm一定要用databinding麼?
答案是 否。首先我們要瞭解到mvvm是數據驅動的架構,所以着眼點是數據的變化,那麼我們需要實現一套ui和數據雙向綁定的邏輯,當數據修改的時候通知ui改變,ui輸入或者點擊的時候觸發數據修改,而databinding就是幫你實現這個雙向綁定過程的框架,在xml中按它的語法去寫佈局,然後他會根據你在xml中所寫的生成對應的類幫你實現這個綁定過程,當然你也可以自己手動實現這個綁定過程,所以databinding是非必須的。
項目結構圖
上面是mvvm基本的結構圖,act/fra和xml是v處理ui操作、viewmodel是vm處理業務邏輯、repository是m提供數據,他們之間是一種單項的持有關係activity/fragment持有vm,vm持有model。
對於Repository不太理解的可以看看這篇文章Repository模式
實際使用
項目中我使用的是retrofit+rxjava+livedata+viewmodel+databinding+kotlin實現的mvvm
retrofit+rejava用來在model層從網絡獲取數據通知到vm
livedata是vm通知ui時使用可以感知生命週期防止內存泄漏npe問題(主要用在事件傳遞上)
viewmodel是vm可以在act/frg因配置修改銷燬的情況下複用
databinding實現ui和vm的雙向綁定
這裏來個具體例子,activity可見通知vm獲取數據,vm從model拿到數據然後更新被觀察者,ui自動刷新的流程。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="vm"
type="rocketly.mvvmdemo.viewmodel.HotCityListVM" />
</data>
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/srl"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:onRefreshListener="@{()->vm.onRefresh()}"//刷新自動觸發vm.onRefresh()方法
tools:context="rocketly.mvvmdemo.ui.MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:multiTypeItem="@{vm.cityList}"//這裏rv與vm中cityList綁定 />
</android.support.v4.widget.SwipeRefreshLayout>
</layout>
xml中Recyclerview和vm的cityList綁定
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main).apply {
vm = ViewModelProviders.of(this@MainActivity).get(HotCityListVM::class.java)
}
}
override fun onResume() {
super.onResume()
binding.vm?.onFirstLoad()//onResume調用vm.onFirstLoad()加載數據
}
}
activity在onResume通知vm加載數據
class HotCityListVM : BaseVM() {
val cityList = ObservableArrayList<Basic>()
val hotCityItemEvent = SingleLiveEvent<String>()
override fun onFirstLoad() {
super.onFirstLoad()
load()
}
override fun onRefresh() {
super.onRefresh()
load()
}
private fun load() {
CityRepository.getHotCityList(num = 50)
.subscribe(ApiObserver(success = {
resetLoadStatus()
cityList.clear()
cityList.addAll(it.HeWeather6[0].basic)
}, error = {
resetLoadStatus()
}))
}
fun hotCityItemClick(s: String) {
hotCityItemEvent.value = s
}
}
vm從model CityRepository獲取數據修改被觀察者對象cityList,然後ui監聽到數據修改執行recyclerview刷新,這一套流程就走完了,具體例子在MvvmDeno。
除了正常的請求數據顯示邏輯,這裏再演示下點擊事件的流程,彈dialog或者其他需要context的事件也是同樣方式。
recyclerview中item點擊事件傳遞到vm然後vm通知activty執行對應的邏輯。
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="vm"
type="rocketly.mvvmdemo.viewmodel.HotCityListVM" />
<variable
name="data"
type="rocketly.mvvmdemo.model.Basic" />
</data>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="50dp"
android:onClick="@{()->vm.hotCityItemClick(data.location)}"//調用vm的方法通知點擊了>
<TextView
android:id="@+id/tv_city_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{data.location}"
android:textColor="@android:color/black"
android:textSize="18sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/tv_lon"
app:layout_constraintTop_toTopOf="parent"
tools:text="上海" />
<TextView
android:id="@+id/tv_lon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:text="@{data.lon}"
android:textColor="@android:color/black"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/tv_city_name"
app:layout_constraintRight_toLeftOf="@+id/tv_lat"
app:layout_constraintTop_toTopOf="parent"
tools:text="(經度:555" />
<TextView
android:id="@+id/tv_lat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="2dp"
android:text="@{data.lat}"
android:textColor="@android:color/black"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/tv_lon"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="緯度:555)" />
</android.support.constraint.ConstraintLayout>
</layout>
class HotCityListVM : BaseVM() {
val hotCityItemEvent = SingleLiveEvent<String>()//給activty監聽的被觀察者livedata對象
fun hotCityItemClick(s: String) {//點擊方法
hotCityItemEvent.value = s
}
}
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main).apply {
vm = ViewModelProviders.of(this@MainActivity).get(HotCityListVM::class.java)
}
initListener()
}
private fun initListener() {
binding.vm?.apply {
hotCityItemEvent.observe(this@MainActivity) {//監聽item點擊事件
it ?: return@observe
Toast.makeText(this@MainActivity, "點擊了:$it", Toast.LENGTH_SHORT).show()
}
}
}
}
以前我們都是在item中直接執行點擊事件的,但爲了遵守mvvm的規範,邏輯都在vm處理又因爲vm不能持有context所以需要context的事件在通過livedata傳遞到activity執行。
那麼一般的數據請求和點擊事件的流程就講完了,接下來說說databinding原理。
DataBinding源碼淺析
通過前面的例子可以發現對於databinding的使用一般可以分爲如下幾步
- 在xml中按databinding的語法書寫佈局
- activity中使用
DataBindingUtil.setContentView()
綁定View並獲取binding對象 - 把xml中聲明的變量通過第二步得到的binding對象設置進去
第一步按databinding語法書寫xml,然後聲明瞭一個變量vm,並將rv與vm的cityList綁定,刷新監聽與vm的onRefresh方法綁定
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="vm"
type="rocketly.mvvmdemo.viewmodel.HotCityListVM" />
</data>
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/srl"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:onRefreshListener="@{()->vm.onRefresh()}"
tools:context="rocketly.mvvmdemo.ui.MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:multiTypeItem="@{vm.cityList}" />
</android.support.v4.widget.SwipeRefreshLayout>
</layout>
然後make project會生成一個佈局名稱+Binding的類,生成的路徑如下
這個類就是按我們xml中所寫生成的,這裏把完整的生成類貼出來可以大概的看下。
public class ActivityMainBinding extends android.databinding.ViewDataBinding implements android.databinding.generated.callback.OnRefreshListener.Listener {
@Nullable
private static final android.databinding.ViewDataBinding.IncludedLayouts sIncludes;
@Nullable
private static final android.util.SparseIntArray sViewsWithIds;
static {
sIncludes = null;
sViewsWithIds = null;
}
// views
@NonNull
public final android.support.v7.widget.RecyclerView rv;
@NonNull
public final android.support.v4.widget.SwipeRefreshLayout srl;
// variables
@Nullable
private rocketly.mvvmdemo.viewmodel.HotCityListVM mVm;
@Nullable
private final android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener mCallback2;
// values
// listeners
// Inverse Binding Event Handlers
public ActivityMainBinding(@NonNull android.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
super(bindingComponent, root, 1);
final Object[] bindings = mapBindings(bindingComponent, root, 2, sIncludes, sViewsWithIds);
this.rv = (android.support.v7.widget.RecyclerView) bindings[1];
this.rv.setTag(null);
this.srl = (android.support.v4.widget.SwipeRefreshLayout) bindings[0];
this.srl.setTag(null);
setRootTag(root);
// listeners
mCallback2 = new android.databinding.generated.callback.OnRefreshListener(this, 1);
invalidateAll();
}
@Override
public void invalidateAll() {
synchronized(this) {
mDirtyFlags = 0x4L;
}
requestRebind();
}
@Override
public boolean hasPendingBindings() {
synchronized(this) {
if (mDirtyFlags != 0) {
return true;
}
}
return false;
}
@Override
public boolean setVariable(int variableId, @Nullable Object variable) {
boolean variableSet = true;
if (BR.vm == variableId) {
setVm((rocketly.mvvmdemo.viewmodel.HotCityListVM) variable);
}
else {
variableSet = false;
}
return variableSet;
}
public void setVm(@Nullable rocketly.mvvmdemo.viewmodel.HotCityListVM Vm) {//根據我們xml中聲明的vm變量生成的set方法
this.mVm = Vm;
synchronized(this) {
mDirtyFlags |= 0x2L;
}
notifyPropertyChanged(BR.vm);
super.requestRebind();
}
@Nullable
public rocketly.mvvmdemo.viewmodel.HotCityListVM getVm() {
return mVm;
}
@Override
protected boolean onFieldChange(int localFieldId, Object object, int fieldId) {
switch (localFieldId) {
case 0 :
return onChangeVmCityList((android.databinding.ObservableArrayList<rocketly.mvvmdemo.model.Basic>) object, fieldId);
}
return false;
}
private boolean onChangeVmCityList(android.databinding.ObservableArrayList<rocketly.mvvmdemo.model.Basic> VmCityList, int fieldId) {
if (fieldId == BR._all) {
synchronized(this) {
mDirtyFlags |= 0x1L;
}
return true;
}
return false;
}
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
rocketly.mvvmdemo.viewmodel.HotCityListVM vm = mVm;
android.databinding.ObservableArrayList<rocketly.mvvmdemo.model.Basic> vmCityList = null;
if ((dirtyFlags & 0x7L) != 0) {
if (vm != null) {
// read vm.cityList
vmCityList = vm.getCityList();
}
updateRegistration(0, vmCityList);
}
// batch finished
if ((dirtyFlags & 0x7L) != 0) {
// api target 1
rocketly.mvvmdemo.utils.databinding.DataBindingExKt.setItem(this.rv, vmCityList);
}
if ((dirtyFlags & 0x4L) != 0) {
// api target 1
this.srl.setOnRefreshListener(mCallback2);
}
}
// Listener Stub Implementations
// callback impls
public final void _internalCallbackOnRefresh(int sourceId ) {
// localize variables for thread safety
// vm != null
boolean vmJavaLangObjectNull = false;
// vm
rocketly.mvvmdemo.viewmodel.HotCityListVM vm = mVm;
vmJavaLangObjectNull = (vm) != (null);
if (vmJavaLangObjectNull) {
vm.onRefresh();
}
}
// dirty flag
private long mDirtyFlags = 0xffffffffffffffffL;
@NonNull
public static ActivityMainBinding inflate(@NonNull android.view.LayoutInflater inflater, @Nullable android.view.ViewGroup root, boolean attachToRoot) {
return inflate(inflater, root, attachToRoot, android.databinding.DataBindingUtil.getDefaultComponent());
}
@NonNull
public static ActivityMainBinding inflate(@NonNull android.view.LayoutInflater inflater, @Nullable android.view.ViewGroup root, boolean attachToRoot, @Nullable android.databinding.DataBindingComponent bindingComponent) {
return android.databinding.DataBindingUtil.<ActivityMainBinding>inflate(inflater, rocketly.mvvmdemo.R.layout.activity_main, root, attachToRoot, bindingComponent);
}
@NonNull
public static ActivityMainBinding inflate(@NonNull android.view.LayoutInflater inflater) {
return inflate(inflater, android.databinding.DataBindingUtil.getDefaultComponent());
}
@NonNull
public static ActivityMainBinding inflate(@NonNull android.view.LayoutInflater inflater, @Nullable android.databinding.DataBindingComponent bindingComponent) {
return bind(inflater.inflate(rocketly.mvvmdemo.R.layout.activity_main, null, false), bindingComponent);
}
@NonNull
public static ActivityMainBinding bind(@NonNull android.view.View view) {
return bind(view, android.databinding.DataBindingUtil.getDefaultComponent());
}
@NonNull
public static ActivityMainBinding bind(@NonNull android.view.View view, @Nullable android.databinding.DataBindingComponent bindingComponent) {
if (!"layout/activity_main_0".equals(view.getTag())) {
throw new RuntimeException("view tag isn't correct on view:" + view.getTag());
}
return new ActivityMainBinding(bindingComponent, view);
}
/* flag mapping
flag 0 (0x1L): vm.cityList
flag 1 (0x2L): vm
flag 2 (0x3L): null
flag mapping end*/
//end
}
我們一般需要用到的方法大致可以分爲兩類
- 將生成的類與View綁定的bind方法和inflate方法(每個生成的binding類都會有)
- 給我們set我們在xml聲明變量的方法(這裏是setVm方法)
第二步在activity中通過DataBindingUtil.setContentView()
方法將Binding類與View綁定
javaclass MainActivity : AppCompatActivity() {
...
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main).apply {
vm = ViewModelProviders.of(this@MainActivity).get(HotCityListVM::class.java)
}
}
...
}
DataBindingUtil.setContentView()
最後調用如下
@Override
public android.databinding.ViewDataBinding getDataBinder(android.databinding.DataBindingComponent bindingComponent, android.view.View view, int layoutId) {
switch (layoutId) {
case rocketly.mvvmdemo.R.layout.activity_main: {
final Object tag = view.getTag();
if (tag == null) throw new java.lang.RuntimeException("view must have a tag");
if ("layout/activity_main_0".equals(tag)) {
return new rocketly.mvvmdemo.databinding.ActivityMainBinding(bindingComponent, view);//創建ActivityMainBinding
}
throw new java.lang.IllegalArgumentException("The tag for activity_main is invalid. Received: " + tag);
}
}
return null;
}
實際上就是new了一個ActivityMainBinding對象,那我們在看看它的構造方法
public ActivityMainBinding(@NonNull android.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
super(bindingComponent, root, 1);
final Object[] bindings = mapBindings(bindingComponent, root, 2, sIncludes, sViewsWithIds);
this.rv = (android.support.v7.widget.RecyclerView) bindings[1];
this.rv.setTag(null);
this.srl = (android.support.v4.widget.SwipeRefreshLayout) bindings[0];
this.srl.setTag(null);
setRootTag(root);
// listeners
mCallback2 = new android.databinding.generated.callback.OnRefreshListener(this, 1);
invalidateAll();
}
public final void _internalCallbackOnRefresh(int sourceId ) {//刷新監聽具體實現
// localize variables for thread safety
// vm != null
boolean vmJavaLangObjectNull = false;
// vm
rocketly.mvvmdemo.viewmodel.HotCityListVM vm = mVm;
vmJavaLangObjectNull = (vm) != (null);
if (vmJavaLangObjectNull) {
vm.onRefresh();
}
}
public final class OnRefreshListener implements android.support.v4.widget.SwipeRefreshLayout.OnRefreshListener {//刷新監聽包裝類
final Listener mListener;
final int mSourceId;
public OnRefreshListener(Listener listener, int sourceId) {
mListener = listener;
mSourceId = sourceId;
}
@Override
public void onRefresh() {
mListener._internalCallbackOnRefresh(mSourceId );
}
public interface Listener {
void _internalCallbackOnRefresh(int sourceId );
}
}
mapBindings方法就相當於findviewbyId把xml的佈局找到裝在一個數組中返回,然後賦值給成員變量方便我們調用,並且創建了一個刷新監聽,最後調用了一個invalidateAll()
這個方法最後會調用到executeBindings()
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
rocketly.mvvmdemo.viewmodel.HotCityListVM vm = mVm;
android.databinding.ObservableArrayList<rocketly.mvvmdemo.model.Basic> vmCityList = null;
if ((dirtyFlags & 0x7L) != 0) {
if (vm != null) {
// read vm.cityList
vmCityList = vm.getCityList();
}
updateRegistration(0, vmCityList);
}
// batch finished
if ((dirtyFlags & 0x7L) != 0) {
// api target 1
rocketly.mvvmdemo.utils.databinding.DataBindingExKt.setItem(this.rv, vmCityList);
}
if ((dirtyFlags & 0x4L) != 0) {
// api target 1
this.srl.setOnRefreshListener(mCallback2);
}
}
可以發現就相當於對srl設置了刷新監聽。
所以總結下第二步就是把view傳遞給binding類然後構造方法findviewbyid找到view賦值給成員變量並把view相關的點擊事件刷新等監聽初始化好,完成對view的綁定。
第三步傳遞xml中聲明的值給binding,完成對數據的綁定。
將vm傳遞到binding類中
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main).apply {
vm = ViewModelProviders.of(this@MainActivity).get(HotCityListVM::class.java)
}
}
完成對數據的綁定
public void setVm(@Nullable rocketly.mvvmdemo.viewmodel.HotCityListVM Vm) {
this.mVm = Vm;
synchronized(this) {
mDirtyFlags |= 0x2L;
}
notifyPropertyChanged(BR.vm);
super.requestRebind();
}
super.requestRebind()
最後也是調到
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
rocketly.mvvmdemo.viewmodel.HotCityListVM vm = mVm;
android.databinding.ObservableArrayList<rocketly.mvvmdemo.model.Basic> vmCityList = null;
if ((dirtyFlags & 0x7L) != 0) {
if (vm != null) {
// read vm.cityList
vmCityList = vm.getCityList();//拿到cityList
}
updateRegistration(0, vmCityList);//添加監聽
}
// batch finished
if ((dirtyFlags & 0x7L) != 0) {
// api target 1
rocketly.mvvmdemo.utils.databinding.DataBindingExKt.setItem(this.rv, vmCityList);
}
if ((dirtyFlags & 0x4L) != 0) {
// api target 1
this.srl.setOnRefreshListener(mCallback2);
}
}
拿到vm的cityList然後通過updateRegistration方法添加監聽,當數據改變的時候就會通知到binding刷新rv的item,這樣就實現了對數據的綁定。
經過上面的分析可以看出databinding就是幫我們實現了view和數據的雙向綁定並且都是通過原生的方法實現的,所以他是非必須的這些代碼我們也可以自己完成。
總結
從這3個版本MvvM的使用得到最大的感受還是思維上的轉變,由之前的事件驅動轉到數據驅動,更多的着眼於數據的變化,至於前面使用的那些庫都是爲了輔助數據驅動這個原則,其次就是解耦邏輯的抽離使代碼結構看起來更加清晰。