Android MVVM

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:萬事萬物皆有利弊

    優勢:

    1. 低耦合,在MVVM中,數據是獨立於UI的,數據和業務邏輯是在獨立的ViewModel中,不涉及任何UI相關的事,不持有UI控件的引用,因此邏輯只需要關心數據即可。
    2. MVVM同樣是便於維護就單元測試的,但是由於MVVM的ViewModel並不像MVP的Presenter一樣是純java代碼,因此測試框架選擇不同。

    劣勢:

    1. 結構相對複雜
    2. 數據綁定是的bug很難被調試。當我們看到界面異常的時候,可能是View的代碼有bug,也可能是Model的代碼有問題。數據綁定讓一個位置的bug快速傳遞到其他位置,要定義到原始出問題的地方就不那麼容易了。
    3. 對於過大的項目,數據綁定要話費更大的內存。

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.

參考:

  1. 認清Android框架 MVC,MVP和MVVM
  2. Android MVVM到底是啥?看完就明白了
  3. Android數據綁定框架DataBinding
  4. 如何構建Android MVVM 應用框架
  5. HTML特殊轉義字符對照表
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章