如何把 MVVM 应用到工程来

阅读 560
收藏 30
2017-03-08
原文链接:github.com

Android data-binding framework has been invented more than one year, now I think it's time to research it and apply it to real project. So I will talk about the MVVM in this post.

I. RoboBinding

Before I talk about the Android data-binding framework, I want to talk about another data-binding library RoboBinding. This library is created way before Android data-binding framework. I got to know this library two years ago, and found it very easy to use. This library uses the compile-time annotation to generate source code of data-binding for you. I have to say, this library is better than today(201703)'s Android data-binding library. Its two-way binding, AdapterView binding is even more easier than Android data-binding library. If you are interested, you can read the source code, and I hope you will enjoy the code.

II. Data-Binding

You can open data-binding feature by using this:

android { 
    ... ... ...
    dataBinding {
        enabled = true
    }
}

Then you can change the structure of layout xml, import and and tell the layout xml you are using this data.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" >

    <data>
        <variable name="user" type="ca.six.bindingdemo.User"/>
    </data>

    <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent"
        android:orientation="vertical">
        <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:textSize="22dp" android:text="@{user.name}"/>
    </LinearLayout>

</layout>

And your Activity will be easy. No more "findViewById()", no more "tv.setText(...)", Pretty clean.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ActivityBindingDemoSimpleBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_binding_demo_simple);

    User user = new User("jack", "a nice man", true);
    binding.setUser(user);
}

Data-binding is not just that. There are more feature you need to learn:

  • two-way binding
  • custom attribute
  • AdapterView's binding ; RecyclerView's binding
  • ...

But this post is about the architecture, is about MVVM, so I will not talk too much about data-binding. I may create a new post about data-binding later.

III. MVVM

For those person who know MVVM is always with data-binding, I have to say, MVVM is not necessarily using data-binding.

In the MVP pattern, our Presenter must know the existance of View, so when you get the data from Model, you can call, like view.refresh(data);. But in MVVM, our middle-layer, ViewModel (not Presenter anymore), does not need know the existance of View, or it does not care about the View. Anyone could be the View, what I(ViewModel) should do is just send the UI logic out, any view can hold it and deal with it.

To fulfill such an architecture, you have at least two ways:

  • Data-Binding : What you should do is just changing the model/data, the data-binding framework will be in charge of deliver this change event to the view.
  • RxJava : This solution is like Observable pattern. The ViewModel just send the change event, and his subscriber will get notified. By the way, ViewModel does not know, and does not need to know which class is the receiver.

IV. MVVM with data-binding

Since you want to apply MVVM, your code is actually divided into three parts:

  • Model: same as MVP. Deal with the http/database/... data
  • View: Activity/Fragment/layout xml.
  • ViewModel: take charge of UI logic and data change.

Here is a typical step to build a MVVM screen:

step 1. POJO class

We have a User class. And its fields' change should be sent by notifyPropertyChanged(), and it's field's value should be get by @Bindable method. Here is the code:

public class User extends BaseObservable {
    public String name;
    public boolean isMale;

    public User(String name, boolean isMale ) {
        this.isMale = isMale;
        this.name = name;
    }

    public void setIsMale(boolean male) {
        isMale = male;
        notifyPropertyChanged(BR.male); // BR is a class generated by data-binding
    }

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


    @Bindable
    public boolean isMale() {
        return isMale;
    }

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

}

step 3. layout/xml

Right now, the xml should have a root element that called <layout>. And our xml contains the data and the real view.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable name="user" type="ca.six.bindingdemo.tmp.User"/>
        <variable name="handler" type="ca.six.bindingdemo.tmp.Binding01ViewModel"/>
        <import type="android.view.View"/>
    </data>

    <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">

        <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:textSize="20dp" android:text="@{`name = ` + user.name}"/>
        
        <CheckBox android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:textSize="20dp" android:text="isMale" android:checked="@={user.isMale}"
            android:onCheckedChanged="@{(view, isChecked) -> handler.onChecked(isChecked)}"
            />
        <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:textSize="14dp" android:text="Wonder Woman"
            android:visibility="@{user.isMale ? View.GONE : View.VISIBLE}" />

        <Button android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:textSize="20dp" android:text="GetEditTextVlaue"
            android:onClick="@{handler::click01}"
            />

    </LinearLayout>

</layout>

step 4. ViewModel

ViewModel takes charge of change of POJO classes, and the click/select/... events.

public class Binding01ViewModel implements ICustomListener{

    private final ActivityBindingDemoSimpleBinding binding;
    private User user;

    public Binding01ViewModel(ActivityBindingDemoSimpleBinding binding) {
        user = new User("szw", "developer", true, R.drawable.ic_face);
        this.binding = binding;
        binding.setUser(user);
        binding.setHandler(this);
    }

    public void click01(View v) {
        System.out.println("szw click01 : " + user);
    }


    public void onChecked(boolean isChecked) {
        user.setName(user.name + (isChecked ? " Y " : " N "));
        System.out.println("szw click02 : " + user);
    }

}
  1. The ActivityBindingDemoSimpleBinding class in the constructor is actually a class generated by data-binding. Our layout xml is activity_binding_demo_simple.xml, so the generated class's name is ActivityBindingDemoSimpleBinding.java.

  2. As shown in the layout xml, we need two variables, one is user, and the other is handler. You have to tell the ActivityBindingDemoSimpleBinding class the two instance, so you have to call binding.setUser() and binding.setHandler().

  3. the click01() and onChecked() methods are the click/checkChanged event method.

  4. Every time the name and isMale changed, our UI will get refreshed too. It's the beauty of data-binding.

step 4. Activity

Since the data-binding framework is in charege of refreshing screen, and the ViewModel takes charge of data change, so our Activity is much easier. No more findViewById(), no more textView.setText(); and imageView.setImageResource(). All you need is just create a binding and a ViewModel.

public class Binding01Activity extends Activity {

    private Binding01ViewModel vm;
    private ActivityBindingDemoSimpleBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_binding_demo_simple);
        vm = new Binding01ViewModel(binding);

    }

}

As you can see, what you need to do is just update the User class, you do not need to call view.refreshUser(newUser); like MVP does. This way, our separation is even cleaner than MVP. Because the ViewModel does not hold the reference of View, so the separation between View and ViewModel is more better. And the tests on ViewModel is as easy as Presenter.

V. OneBindingAdapter

By the way, I personally hate write each Adapter for each RecyclerView. So I did some homework and create a OneBindingAdapter.

The layout of RecyclerView's each item is "layout/item_rv.xml"

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable name="item" type="ca.six.bindingdemo.tmp.rv.TmpItem"/>
    </data>

    <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical">
        <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:textSize="20dp" android:text="@{item.name}"/>
        <TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
            android:textSize="20dp" android:text="@{String.valueOf(item.age)}"/>
    </LinearLayout>

</layout>

So after create a layout xml, which contains and view, you can just use this:

    OneBindingAdapter<TmpItem> adapter = new OneBindingAdapter<>(this, R.layout.item_rv, BR.item, data);
    RecyclerView rv = binding.rvBindingDemo;
    rv.setLayoutManager(new LinearLayoutManager(this));
    rv.setAdapter(adapter);
  1. the constructor of OneBindingAdapter need the context, item layout resouce, and BR.~.
  2. About the "BR.", the "" is the element in the layout xml

VI. Conclusion

MVVM is a new pattern and will make your development easier. And it helps you separate the UI and logic, makes unit test avaiable. So why not give it a try?

评论