MVVM模式--DataBinding的使用

葉少磐發表於2019-04-17

MVVM模式

介紹

首先上一張MVVM的架構圖

MVVM架構圖

  • View層即是圖中綠色的Activity/Fragment,它的主要職責是負責UI中的繪製以及與使用者互動,由ViewModel驅動,同時也監聽UI事件及其生命週期,驅動ViewModel。
  • ViewModel層即是圖中藍色的ViewModel,它建立關聯,將model和view進行繫結,它只做業務邏輯的操作,不持有任何控制元件的引用。當model更改後,通過ViewModel傳遞給View來進行更新。
  • Model層即是圖中的橘色Repository幷包括其下都是。Model層就是資料層,它的資料來源包括本地資料、快取資料以及網路資料。

本文將以谷歌推出的MVVM框架DataBinding作為示例。

基本使用

  1. 啟用DataBingding

在模組的build.gradle中新增以下程式碼。

    dataBinding {
        enabled = true
    }
複製程式碼
  1. 建立一個Bean類,這個bean類充當的就是Model。
public class StudentBean extends BaseObservable{

    private String name;
    private int age;

    public StudentBean(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Bindable
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
    }

    @Bindable
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
        notifyPropertyChanged(BR.age);
    }
}
複製程式碼
  1. 修改Activity的xml佈局檔案,View層
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="student"
            type="cn.panf.mvvm_new.bean.StudentBean" />
    </data>

    <LinearLayout xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context="cn.panf.mvvm_new.MvvmActivity">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{student.name}"
            android:textAlignment="center" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf(student.age)}"
            android:textAlignment="center" />

    </LinearLayout>
</layout>
複製程式碼
  1. 修改Activity類,ViewModel層
public class MvvmActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        setContentView(R.layout.activity_mvvm);
        ActivityMvvmBinding activityMvvmBinding =
                DataBindingUtil.setContentView(this, R.layout.activity_mvvm);
        StudentBean xiaoMing = new StudentBean("xiaoMing", 12);
        activityMvvmBinding.setStudent(xiaoMing);
    }
}
複製程式碼

這樣一個最簡單的databinding就完成了,那麼只要我們隨意修改StudentBean中的任一物件,UI介面就會重新整理。下面新增一個button按鈕測試一下。

首先新建一個處理點選事件的類:

public interface HandleClick {
    void buttonClick(View view);
}
複製程式碼

在佈局檔案中新增:

<variable
            name="clickhHandler"
            type="cn.panf.mvvm_new.HandleClick" />

<Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{clickhHandler::buttonClick}" />
複製程式碼

最後在Activity中:

//處理點選事件
        activityMvvmBinding.setClickhHandler(new HandleClick() {
            @Override
            public void buttonClick(View view) {
                xiaoMing.setAge(13);
            }
        });
複製程式碼

這樣你只要點選按鈕,那麼textView的資料就會自動重新整理而不需要任何手動賦值,這就是所謂的資料驅動

進階使用

下面我們將databinding在RecyclerView中來進行使用。

首先修改佈局檔案

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <LinearLayout xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context="cn.panf.mvvm_new.MvvmActivity">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/rv"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </LinearLayout>
</layout>
複製程式碼

為RecyclerView的item新建一個佈局layout_item_student.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="student"
            type="cn.panf.mvvm_new.bean.StudentBean" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:orientation="horizontal">

        <TextView
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"
            android:text="@{student.name}" />

        <TextView
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"
            android:text="@{String.valueOf(student.age)}" />

    </LinearLayout>
</layout>
複製程式碼

新建Adapter類:

public class StudentAdapter extends RecyclerView.Adapter<StudentAdapter.StudentViewHolder> {

    private List<StudentBean> mList;
    private Context context;

    public StudentAdapter(Context context, List<StudentBean> mList) {
        this.mList = mList;
        this.context = context;
    }

    @Override
    public StudentViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        LayoutItemStudentBinding layoutItemStudentBinding = DataBindingUtil
                .inflate(inflater, R.layout.layout_item_student, parent, false);
        return new StudentViewHolder(layoutItemStudentBinding);
    }

    @Override
    public void onBindViewHolder(StudentViewHolder holder, int position) {
        StudentBean studentBean = mList.get(position);
        holder.getLayoutItemStudentBinding().setStudent(studentBean);
    }

    @Override
    public int getItemCount() {
        return mList == null ? 0 : mList.size();
    }

    class StudentViewHolder extends RecyclerView.ViewHolder {

        private LayoutItemStudentBinding layoutItemStudentBinding;

        LayoutItemStudentBinding getLayoutItemStudentBinding() {
            return layoutItemStudentBinding;
        }

        StudentViewHolder(LayoutItemStudentBinding layoutItemStudentBinding) {
            super(layoutItemStudentBinding.getRoot());
            this.layoutItemStudentBinding = layoutItemStudentBinding;
        }
        
    }

}
複製程式碼

注意,在上文的StudentViewHolder的建構函式中,我把item對應的佈局的View物件改成了item對應佈局的ViewDataBinding物件,然後提供一個get方法來給外部呼叫,這樣做的好處是你不需要再去操作view去對資料進行修改,而是隻需要把你獲取到的資料直接傳給Bean即可,因為你在item佈局檔案裡已經把資料和View進行繫結了。

最後再來修改一下Activity:

public class MvvmActivity extends AppCompatActivity {

    private List<StudentBean> mList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        setContentView(R.layout.activity_mvvm);
        ActivityMvvmBinding activityMvvmBinding =
                DataBindingUtil.setContentView(this, R.layout.activity_mvvm);
//        final StudentBean xiaoMing = new StudentBean("xiaoMing", 12);
        addData();

        StudentAdapter adapter = new StudentAdapter(this, mList);
        LinearLayoutManager manager = new LinearLayoutManager(this, OrientationHelper.VERTICAL, false);
        activityMvvmBinding.rv.setLayoutManager(manager);
        activityMvvmBinding.rv.setAdapter(adapter);

    }

    private void addData() {
        for (int i = 0; i < 15; i++) {
            int age = new Random().nextInt(20);
            StudentBean studentBean = new StudentBean("student" + i, age);
            mList.add(studentBean);
        }
    }
}
複製程式碼

OK, it works!

總結

通過介紹了MVVM的相關定義以及提供了兩個例項來幫助理解,相信理解了本文的兩個例子,必然會理解MVVM這種資料驅動的魅力所在,所有程式碼沒有一處對控制元件進行賦值即可讓資料進行自動重新整理。但是,沒有最好,只有最合適。目前在android中最廣泛使用的還是MVP,而且在2017年穀歌推出了新的MVVM框架LiveData。

note: 目的在於以DataBinding來介紹MVVM,對DataBinding的更多用法並未詳細介紹,有興趣的可以去查閱更多相關資料。

更新

宮影讓我看看能不能把databinding針對recyclerView在多佈局上應用,我進行了嘗試發現確實可行。

  1. 首先是要對不同的viewType進行區分,在上面我們已經將對於資料完全和Bean進行繫結了,所以最好在繫結資料時在Bean中進行區分。那麼可以定義一個介面,使所有的Bean來實現它。
public interface IBaseItem {
    int getItemViewType();
}
複製程式碼

修改StudentBean類,並新增TeacherBean類。

//只放一張圖片,僅為了做區分
public class TeacherBean extends BaseObservable implements IBaseItem {

    private int resId;

    public TeacherBean(int resId) {
        this.resId = resId;
    }

    @Bindable
    public int getResId() {
        return resId;
    }

    public void setResId(int resId) {
        this.resId = resId;
        notifyPropertyChanged(BR.resId);
    }

    @Override
    public int getItemViewType() {
        return R.layout.layout_item_teacher;
    }
}

public class StudentBean extends BaseObservable implements IBaseItem{

    ......

    @Override
    public int getItemViewType() {
        return R.layout.layout_item_student;
    }
}

複製程式碼

layout_item_teacher.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="teacher"
            type="cn.panf.mvvm_new.bean.TeacherBean" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:orientation="horizontal">

        <ImageView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="center"
            app:imgUrl="@{teacher.resId}"/>

    </LinearLayout>
</layout>
複製程式碼

顯示圖片時由於dataBinding不支援直接寫int型別,這裡需要額外提供顯示image的方法。 可以新建一個類頁可以直接寫在Adapter裡面,看喜好。

public class ImgUtil {

    @BindingAdapter("imgUrl")
    public static void showImgByUrl(ImageView iv, int resId) {
        iv.setImageResource(resId);
    }
}
複製程式碼
  1. 修改原本的StudentAdapter類,因為新增了Teacher,這裡修改名稱為SchoolAdapter。
package cn.panf.mvvm_new;

import android.content.Context;
import android.databinding.DataBindingUtil;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import java.util.List;

import cn.panf.androidknowledge.R;
import cn.panf.androidknowledge.databinding.LayoutItemStudentBinding;
import cn.panf.androidknowledge.databinding.LayoutItemTeacherBinding;
import cn.panf.mvvm_new.bean.IBaseItem;
import cn.panf.mvvm_new.bean.StudentBean;
import cn.panf.mvvm_new.bean.TeacherBean;

/**
 * author: aaron.pf
 * date: 2019/4/17 19:49.
 * desc:
 */

public class SchoolAdapter extends RecyclerView.Adapter {

    private List<IBaseItem> mList;
    private LayoutInflater inflater;

    public SchoolAdapter(Context context, List<IBaseItem> mList) {
        this.mList = mList;
        inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        switch (viewType) {
            case R.layout.layout_item_student:
                LayoutItemStudentBinding layoutItemStudentBinding = DataBindingUtil
                        .inflate(inflater, R.layout.layout_item_student, parent, false);
                return new StudentViewHolder(layoutItemStudentBinding);
            case R.layout.layout_item_teacher:
                LayoutItemTeacherBinding layoutItemTeacherBinding = DataBindingUtil
                        .inflate(inflater, R.layout.layout_item_teacher, parent, false);
                return new TeacherViewHolder(layoutItemTeacherBinding);
            default:
                LayoutItemTeacherBinding layoutItemTeacherBinding1 = DataBindingUtil
                        .inflate(inflater, R.layout.layout_item_teacher, parent, false);
                return new TeacherViewHolder(layoutItemTeacherBinding1);
        }

    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof StudentViewHolder) {
            StudentBean studentBean = (StudentBean) mList.get(position);
            ((StudentViewHolder) holder).getLayoutItemStudentBinding().setStudent(studentBean);
            //防止資料重新整理閃爍
            ((StudentViewHolder) holder).getLayoutItemStudentBinding().executePendingBindings();
        } else if (holder instanceof TeacherViewHolder) {
            TeacherBean teacherBean = (TeacherBean) mList.get(position);
            ((TeacherViewHolder) holder).getLayoutItemTeacherBinding().setTeacher(teacherBean);
            ((TeacherViewHolder) holder).getLayoutItemTeacherBinding().executePendingBindings();
        }
    }

    @Override
    public int getItemViewType(int position) {
        return mList.get(position).getItemViewType();
    }

    @Override
    public int getItemCount() {
        return mList == null ? 0 : mList.size();
    }

    class StudentViewHolder extends RecyclerView.ViewHolder {

        private LayoutItemStudentBinding layoutItemStudentBinding;

        LayoutItemStudentBinding getLayoutItemStudentBinding() {
            return layoutItemStudentBinding;
        }

        StudentViewHolder(LayoutItemStudentBinding layoutItemStudentBinding) {
            super(layoutItemStudentBinding.getRoot());
            this.layoutItemStudentBinding = layoutItemStudentBinding;
        }
    }

    class TeacherViewHolder extends RecyclerView.ViewHolder {

        private LayoutItemTeacherBinding layoutItemTeacherBinding;

        public LayoutItemTeacherBinding getLayoutItemTeacherBinding() {
            return layoutItemTeacherBinding;
        }

        public TeacherViewHolder(LayoutItemTeacherBinding layoutItemTeacherBinding) {
            super(layoutItemTeacherBinding.getRoot());
            this.layoutItemTeacherBinding = layoutItemTeacherBinding;
        }
    }

}

複製程式碼
  1. 最後在Activity中來測試一下
public class MvvmActivity extends AppCompatActivity {

    private List<IBaseItem> mList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        setContentView(R.layout.activity_mvvm);
        ActivityMvvmBinding activityMvvmBinding =
                DataBindingUtil.setContentView(this, R.layout.activity_mvvm);
//        final StudentBean xiaoMing = new StudentBean("xiaoMing", 12);

        addData();

        SchoolAdapter adapter = new SchoolAdapter(this, mList);
        LinearLayoutManager manager = new LinearLayoutManager(this, OrientationHelper.VERTICAL, false);
        activityMvvmBinding.rv.setLayoutManager(manager);
        activityMvvmBinding.rv.setAdapter(adapter);

    }

    private void addData() {
        for (int i = 0; i < 10; i++) {
            mList.add(new StudentBean("stu1", 12));
            mList.add(new StudentBean("stu2", 15));
            mList.add(new TeacherBean(R.mipmap.ic_launcher));
            mList.add(new StudentBean("stu3", 16));
            mList.add(new TeacherBean(R.mipmap.ic_launcher));
            mList.add(new StudentBean("stu4", 18));
            mList.add(new TeacherBean(R.mipmap.ic_launcher));
        }

    }
}
複製程式碼

OK, it works again!

MVVM進階使用

思考

這裡雖然實現了功能,但是能否更進一步將Adapter抽取成通用的呢?能否將ViewHolder抽取一下呢?畢竟這兩個ViewHolder重複的程式碼略多呀。

相關文章