Android MVVM
Android框架
如今,Android框架日益發展,從MVC到MVP、MVVM
-
MVC(Model-View-Controller)自不必說,大家應該都早已知道,在Android中,由於Activity層即承擔View的職責,又有Controller職責,導致Activity異常臃腫,測試難,維護難。
-
MVP(Model-View-Presenter)是從MVC演化而來,將View層與Model層解耦,Activity層作爲View,只與Presenter層進行交互,Presenter通過接口對View進行操作,而Model層也只與Presenter交互,View層和Model層不進行直接交互,Presenter作爲兩者的橋樑。如此一來三者各司其職,降低耦合,又便於測試,便於維護。
-
MVVM(Model-View-ViewModel)最早是由微軟提出的,在Android中,MVVM和MVP比較相似,但它的核心在於
DataBinding
,View的變化可以自動的反映在ViewModel,ViewModel的數據變化也會自動反應到View上。這樣我們就不用處理接收時間和View更新的工作,框架已經做好了。
MVC、MVP、MVVM優劣對比
-
MVC:其實Android本身還是符合MVC架構的,但是Activity又是View又是Controller,導致Activity臃腫,高度耦合,動輒千行代碼,維護測試困難。
-
MVP:
優勢:MVP對於MVC做了優化,V層與P層通過接口交互,Activity只負責UI變化,業務邏輯放在P層之中,M層與V層不能直接訪問,M與V層解耦。它的結構也更清晰了,每層都有它對應的職責,這樣的結構便於維護與單元測試。
劣勢:以爲V層與P層通過接口交互,定義接口就成爲了一個問題,粒度太小,就會導致接口過多,對應Activity代碼也會很多,粒度太大,解耦效果就差。且其本身雖然通過接口訪問進行解耦,但是如果控件變更,對應的邏輯也要做相應的變更
-
MVVM:萬事萬物皆有利弊
優勢:
- 低耦合,在MVVM中,數據是獨立於UI的,數據和業務邏輯是在獨立的ViewModel中,不涉及任何UI相關的事,不持有UI控件的引用,因此邏輯只需要關心數據即可。
- MVVM同樣是便於維護就單元測試的,但是由於MVVM的ViewModel並不像MVP的Presenter一樣是純java代碼,因此測試框架選擇不同。
劣勢:
- 結構相對複雜
- 數據綁定是的bug很難被調試。當我們看到界面異常的時候,可能是View的代碼有bug,也可能是Model的代碼有問題。數據綁定讓一個位置的bug快速傳遞到其他位置,要定義到原始出問題的地方就不那麼容易了。
- 對於過大的項目,數據綁定要話費更大的內存。
Android DataBinding
DataBinding是google搞出來的數據綁定框架,是解決界面邏輯的一個黑科技。
DataBinding已經有很多大牛寫過相關的文檔,優美的文筆,嚴謹的邏輯,我是自愧不如,就不再多做更多的介紹,只在此推薦如下一篇文章。結合DataBinding官方文檔,相信大家可以快速入門、精通。
DataBinding入門:Android MVVM到底是啥?看完就明白了
DataBinding官方文檔:https://developer.android.com/topic/libraries/data-binding/index.html#build_environment
MVVM實踐
Activity中綁定View與ViewModel
ViewModel中持有Model的引用,並且通過DataBinding綁定在View上,當data刷新時,view同步更新
PODEMO:登陸然後設置HOME界面List
包括兩個頁面,登陸頁面和主頁面
登陸頁面分爲LoginModel,LoginBean,LoginActivity,LoginViewModel
LoginModel中完成與服務器的交互,校驗用戶名密碼的準確性
/**
* 模擬網絡請求驗證登錄信息,實際應有回調
* @param name 用戶名
* @param pwd 密碼
* @return 校驗結果
*/
public boolean checkLoginInfo(String name, String pwd) {
return "vicky".equals(name) && "123".equals(pwd);
}
LoginBean中包含用戶名和密碼
private String loginName;
private String loginPwd;
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public String getLoginPwd() {
return loginPwd;
}
public void setLoginPwd(String loginPwd) {
this.loginPwd = loginPwd;
}
LoginActivity中綁定xml佈局,並設置EditText的TextWatcher
// DataBinding根據xml名稱設置類,用以綁定xml
private ActivityLoginBinding mBinding;
// 持有ViewModel對象可以調用其中的方法
private LoginViewModel mViewModel;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// DataBinding綁定方式
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_login);
mViewModel = new LoginViewModel(new LoginBean());
// 綁定完成後要設置ViewModel
mBinding.setLogin(mViewModel);
attachListener();
}
private void attachListener() {
mBinding.loginNameEdit.addTextChangedListener(mTextWatcher);
mBinding.loginPwdEdit.addTextChangedListener(mTextWatcher);
}
private TextWatcher mTextWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editable) {
if (mBinding.loginNameEdit.getText().length() > 0
&& mBinding.loginPwdEdit.getText().length() > 0) {
mViewModel.setBtnVisible(true);
} else {
mViewModel.setBtnVisible(false);
}
}
};
Login xml佈局
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="login"
type="com.daydayup.mvvm.viewmodel.LoginViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:gravity="end"
android:paddingBottom="5dp"
android:paddingTop="5dp"
android:text="@string/login_name"
android:textSize="15sp" />
<EditText
android:id="@+id/login_name_edit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginStart="10dp"
android:layout_weight="3"
android:background="@null"
android:hint="@string/input_login_name"
android:inputType="text"
android:paddingBottom="5dp"
android:paddingTop="5dp"
android:text="@={login.loginName}"
android:textSize="15sp"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginStart="10dp"
android:layout_weight="1"
android:gravity="end"
android:paddingBottom="5dp"
android:paddingTop="5dp"
android:text="@string/login_pwd"
android:textSize="15sp" />
<EditText
android:id="@+id/login_pwd_edit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginStart="10dp"
android:layout_weight="3"
android:background="@null"
android:hint="@string/input_login_pwd"
android:inputType="numberPassword"
android:paddingBottom="5dp"
android:paddingTop="5dp"
android:text="@={login.loginPwd}"
android:textSize="15sp" />
</LinearLayout>
<Button
android:id="@+id/login_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginStart="10dp"
android:layout_marginTop="20dp"
android:enabled="@{login.btnVisible}"
android:onClick="@{login.handleLogin}"
android:text="@string/login_btn_txt" />
</LinearLayout>
</layout>
LoginViewModel中設置獲取用戶名和密碼,通過View的綁定顯示這些信息在界面上,並制定登陸按鈕是否可點擊及點擊時間
// 持有Model對象,調用接口等
private LoginModel mMode;
// 顯示並更新界面數據
private LoginBean mBean;
// 雙向綁定,實時控制btn的Enable
private boolean btnVisible;
// 參數按照需要添加
public LoginViewModel(LoginBean bean) {
this.mBean = bean;
mMode = new LoginModel();
}
public String getLoginName() {
return mBean.getLoginName();
}
public void setLoginName(String loginName) {
mBean.setLoginName(loginName);
}
public String getLoginPwd() {
return mBean.getLoginPwd();
}
public void setLoginPwd(String loginPwd) {
mBean.setLoginPwd(loginPwd);
}
// 雙向綁定的寫法 @Bindable及notifyPropertyChanged
@Bindable
public boolean isBtnVisible() {
return btnVisible;
}
public void setBtnVisible(boolean btnVisible) {
this.btnVisible = btnVisible;
notifyPropertyChanged(BR.btnVisible);
}
// 處理btn的點擊事件
public void handleLogin(View view) {
Context context = view.getContext();
if (mMode.checkLoginInfo(mBean.getLoginName(), mBean.getLoginPwd())) {
context.startActivity(new Intent(context, HomeActivity.class));
} else {
Toast.makeText(context, "error name or pwd", Toast.LENGTH_SHORT).show();
}
}
主頁面結構登錄界面相同
HomeModel獲取主頁面列表
// 模擬返回數據,此處應有回調
public ArrayList<HomeBean> getList() {
ArrayList<HomeBean> list = new ArrayList<>();
for (int i = 0; i < 3; i++) {
HomeBean homeBean = new HomeBean();
homeBean.setDescription("Description:" + i);
homeBean.setKeyWords("KeyWords:" + i);
homeBean.setSummary("Summary:" + i);
homeBean.setImg("http://img.bizhi.sogou.com/images/2012/03/14/140763.jpg");
list.add(homeBean);
}
return list;
}
HomeBean主頁列表item(本demo只有列表,如有其它,可以定義第二個bean,如Login一樣定義到ViewModel中)
private String description;
private String img;
private String keyWords;
private String summary;
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getImg() {
return img;
}
public void setImg(String img) {
this.img = img;
}
public String getKeyWords() {
return keyWords;
}
public void setKeyWords(String keyWords) {
this.keyWords = keyWords;
}
public String getSummary() {
return summary;
}
public void setSummary(String summary) {
this.summary = summary;
}
// ImageView綁定方法(PS:經測試"img"全工程共享)
@BindingAdapter("img")
public static void loadImg(ImageView imgView, String url) {
Glide.with(imgView.getContext()).load(url).into(imgView);
}
HomeActivity綁定xml,在onResume時刷新列表數據
// 持有Model對象,調用接口等
private HomeViewModel mViewModel;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityHomeBinding mBinding = DataBindingUtil.setContentView(this, R.layout.activity_home);
mViewModel = new HomeViewModel();
mBinding.setHome(mViewModel);
}
@Override
protected void onResume() {
super.onResume();
mViewModel.refreshList();
}
Home xml佈局
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="home"
type="com.daydayup.mvvm.viewmodel.HomeViewModel" />
</data>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView
android:id="@+id/home_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:onItemClick="@{home.onItemClick}"
app:itemView="@{home.itemView}"
app:items="@{home.items}" />
</LinearLayout>
</layout>
Home item xml佈局
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="homeItem"
type="com.daydayup.mvvm.model.HomeBean" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="96dp">
<ImageView
android:id="@+id/iv"
android:layout_width="96dp"
android:layout_height="96dp"
android:contentDescription="@null"
android:padding="6dp"
app:img="@{homeItem.img}" />
<TextView
android:id="@+id/list_view_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_toEndOf="@id/iv"
android:layout_toRightOf="@id/iv"
android:ellipsize="end"
android:text="@{homeItem.description}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="2dp"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_toEndOf="@id/iv"
android:layout_toRightOf="@id/iv"
android:text="@{homeItem.keyWords}" />
</RelativeLayout>
</layout>
HomeViewModel顯示在界面上的數據定義及item點擊事件
private HomeModel mModel;
// list item(列表的bean對象如此加載)
public final ObservableList<HomeBean> items = new ObservableArrayList<>();
// item view
public final ItemView itemView = ItemView.of(BR.homeItem, R.layout.item_list_view);
public HomeViewModel() {
mModel = new HomeModel();
}
public void refreshList() {
items.clear();
items.addAll(mModel.getList());
}
// item點擊事件
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Context context = view.getContext();
Toast.makeText(context, "It's " + position + " item.", Toast.LENGTH_SHORT).show();
}
簡易DEMO,僅供參考!
Over.
參考: