MVVM 架构与数据绑定库 已翻译 100%

oschina 投递于 2016/12/21 11:12 (共 16 段, 翻译完成于 12-23)
阅读 5178
收藏 47
3
加载中

Model-View-Presenter (MVP) architecture is widely used in today’s Android apps to separate the view from the presentation logic and the model by introducing a presenter. Model-View-ViewModel (MVVM) is quite similar to MVP, with the view model acting as an enhanced presenter, using a data binder to keep the view model and the view in sync. By binding the view to view model properties, the data binder can handle view updates without the need to manually set changed data back to the view (e.g. no more setText() or setVisibility() on a TextView). As with the presenter in MVP, the view model can easily be unit tested. This article gives an introduction to both the data binding library and the MVVM architectural pattern and how they work together on Android.

已有 1 人翻译此段
我来翻译

Data Binding

What is data binding?

Data binding means that data from data sources is bound to data consumers. For us this usually means that data from local storage or network is bound to layouts. Also, an important feature of data binding is that data changes are automatically synchronized between sources and consumers.

What are the benefits of the data binding library?

TextView textView = (TextView) findViewById(R.id.label);
EditText editText = (EditText) findViewById(R.id.userinput);
ProgressBar progressBar = (ProgressBar) findViewById(R.id.progress);
 
editText.addTextChangedListener(new TextWatcher() {
   @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
   @Override public void afterTextChanged(Editable s) { }
   @Override public void onTextChanged(CharSequence s, int start, int before, int count) {
       model.setText(s.toString());
   }
});
 
textView.setText(model.getLabel());
progressBar.setVisibility(View.GONE);

We’ve all been writing this kind of code. Lots of findViewById() calls and later many calls to setters and listeners and so on. Even with libraries like ButterKnife it doesn’t really get better. With the data binding library, this is a thing of the past.

A binding class is created on compile time for you, which provides all views with an ID as fields. This means no more findViewById(). Actually it’s faster than manually calling findViewById() multiple times, because the data binding library creates code that traverses the view hierarchy only once.

The binding class also implements the binding logic from the layout files, so all those setters are actually called in the binding class. You don’t have to care about it anymore. In short, this means less code in your activities, fragments and view holders.

已有 1 人翻译此段
我来翻译

How to setup data binding?

android {
   compileSdkVersion 25
   buildToolsVersion "25.0.1"
   ...
   dataBinding {
       enabled = true
   }
   ...
}

First thing to do is to add dataBinding { enabled = true } to your app’s build.gradle. This tells the build system to enable additional processing for data binding, like creating the binding classes from your layout files.

<layout xmlns:android="http://schemas.android.com/apk/res/android">
  <data>
    <variable name="vm" type="com.example.ui.main.MainViewModel" />
    <import type="android.view.View" />
  </data>
  ...
</layout>

Next, wrap your layout’s top element in <layout> tags, so that a binding class is created for this layout. The binding class has the name of your layout xml file with Binding added at the end, e.g. ActivityMainBinding for activity_main.xml. As you can see above, namespace declarations also move to the layout tag. Then – inside the layout tag – declare the data you are binding as variables, giving them a name and type. In our case the only variable will be the view model, but more to this later. Optionally you can import classes so that you can use constants like View.VISIBLE or static methods.

已有 1 人翻译此段
我来翻译

How to bind data?

<TextView
    android:id="@+id/my_layout"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:visibility="@{vm.visible ? View.VISIBLE : View.GONE}">
    android:padding="@{vm.bigPadding ? @dimen/paddingBig : @dimen/paddingNormal}"
    android:text='@{vm.text ?? @string/defaultText + "Additional text."}' />

Data binding instructions on view attributes start with an @ and are enclosed by brackets. You can use any variables and imports you declared in the data section. The expressions support nearly everything you can do in code, for example arithmetic operators or string concatenation.

As you can see on the visibility attribute, the ternary if-then-else operator is also supported. A null coalescing operator ?? is provided, which returns the right operand if the left one was null. You can see this above with the text attribute. You can access resources as you would in normal layouts, therefore you can for example choose different dimension resources based on a boolean property of one of your variables, as you can see with the padding attribute.

Properties of your declared variables can be accessed via field access syntax, even if your code uses getters and setters. You can see this again in the text attribute on the slides. vm.text calls the getText() method of the view model. Finally, some small restrictions apply, e.g. no new objects can be created. However, the data binding library is still very powerful.

已有 1 人翻译此段
我来翻译

Which attributes can be bound?

android:text="@{vm.text}"
android:visibility="@{vm.visibility}"
android:paddingLeft="@{vm.padding}"
android:layout_marginBottom="@{vm.margin}"
app:adapter="@{vm.adapter}"

Actually, most properties of the standard views are already supported by the data binding library. Internally, the library looks for setters on the view type for the attribute names where you use data binding. For example, when you bind data to the text attribute, the library looks for a setText() method in your view’s class with the right parameter type (in this case String).

This also means that you can use setters, which normally do not have a corresponding layout attribute, by using data binding. For example you could use the app:adapter attribute on a recycler view in the xml layout to set the adapter via data binding.

With standard attributes, not every one of those have a corresponding setter method on the View, for example paddingLeft. In this case, the data binding library ships a custom setter, so that binding to padding attributes works out of the box. Now, what to do when no custom setters are provided by the library, for example for layout_marginBottom?

已有 2 人翻译此段
我来翻译

Custom setters

@BindingAdapter("android:layout_marginBottom")
public static void setLayoutMarginBottom(View v, int bottomMargin) {
   ViewGroup.MarginLayoutParams layoutParams =
           (ViewGroup.MarginLayoutParams) v.getLayoutParams();
  
   if (layoutParams != null) {
       layoutParams.bottomMargin = bottomMargin;
   }
}

For these cases, custom setters can be written. Setters are annotated with the @BindingAdapter annotation, which takes the layout attribute name as argument, for which the binding adapter should be called. Above you see a binding adapter for layout_marginBottom.

The method must be public static void and must accept as parameters first the view type for which the binding adapter should be called and then the data to be bound with your desired type. In this example, we define a binding adapter for type View (and subtypes) with a bound type of int. Finally, implement the binding adapter. For layout_marginBottom, we need to get the layout parameters and set the bottom margin. Easy.

@BindingAdapter({"imageUrl", "placeholder"})
public static void setImageFromUrl(ImageView v, String url, int drawableId) {
   Picasso.with(v.getContext().getApplicationContext())
           .load(url)
           .placeholder(drawableId)
           .into(v);
}

It’s also possible to require multiple attributes to be set for a binding adapter to be called. To achieve this, provide your list of required attribute names to the @BindingAdapter annotation. Also, each of these attributes now have a typed parameter in the method. These BindingAdapters are only called, when all declared attributes are set.

I always like to define a binding adapter for loading images from URLs with a placeholder during loading. As you can see above, this is pretty easy with binding adapters and by using the Picasso image loading library. However, you can use any approach you want in your custom binding adapters.

已有 1 人翻译此段
我来翻译

Applying the binding in code

MyBinding binding;
 
// For Activity
binding = DataBindingUtil.setContentView(this, R.layout.layout);
// For Fragment
binding = DataBindingUtil.inflate(inflater, R.layout.layout, container, false);
// For ViewHolder
binding = DataBindingUtil.bind(view);
 
// Access the View with ID text_view
binding.textView.setText(R.string.sometext);
 
// Setting declared variables
binding.set<VariableName>(variable);

Now that we defined our bindings in the xml file and have written custom setters, how do we apply the binding in code? The data binding library does all the hard work for us, by generating a binding class. For getting an instance of the corresponding binding class for your layout, use the helper methods provided by the library. For activities use  DataBindingUtil.setContentView(), for fragments use inflate() and for view holders use bind(). As already mentioned, the binding class provides all views which have an ID defined as final fields. Also, you set the variables you declared in the layout files on the binding object.

已有 1 人翻译此段
我来翻译

Auto-updating the layout

One of the benefits of data binding is that the layout can be updated automatically by the library when data changes. However, the library still needs to be notified of data changes. This is accomplished by having the variables you set on the binding implement the Observable interface (don’t confuse this with the RxJava Observable).

For simple data types like int or boolean, the library already provides appropriate types which implement Observable, for example ObservableBoolean. Also, there is an ObservableField type for use with other objects, like strings.

public class MyViewModel extends BaseObservable {
   private Model model = new Model();
 
   public void setModel(Model model) {
       this.model = model;
       notifyChange();
   }
   
   public void setAmount(int amount) {
       model.setAmount(amount);
       notifyPropertyChanged(BR.amount);
   }
 
   @Bindable public String getText() { return model.getText(); }
   @Bindable public String getAmount() { return Integer.toString(model.getAmount()); }
}

In more complex situations, like with view models, a BaseObservable class exists, which provides easy methods for notifying the layout of changes. As you can see above in the setModel() method, we can then update the whole layout at once when the model changes by calling notifyChange().

When you look at setAmount(), you see that only one property of our model is changed. In this case, we do not want the whole layout to be updated, but just those parts that use this exact property. To achieve this, the corresponding getters of the property can be annotated with @Bindable. Then, a field in the BR class is generated, which can be passed into the notifyPropertyChanged() method. With this, the data binding library only updates those parts of your layout that actually depend on the changed property.

Summary

  • Declare variables in the layout files and use them to bind attributes of your views.

  • Create the binding in code and set the variables.

  • Make sure that your variable types implement Observable – for example by extending BaseObservable – so that data changes are automatically reflected by the layouts.

已有 1 人翻译此段
我来翻译

Model View ViewModel architecture

Now let’s look at the ModelViewViewModel architecture and how the three components in this architectural pattern work together.

  • The view is the user interface, the layout. In Android this usually means an Activity, Fragment or ViewHolder and its corresponding inflated xml layout file.

  • The model is our business logic layer, which provides methods for interacting with data.

  • The view model acts as a middleman between view and model, by exposing the data from the model via properties and containing the UI state. Also, it defines commands which can be called on events like clicks. View models contain the presentation logic of your app.

In the MVVM architectural pattern, the view and the view model mainly interact with each other through data binding. Ideally, the view and view model should not know about each other. The bindings should be the glue between view and view model and handle most of the stuff in both directions. In Android however, they can not really be independent:

  1. you have to save and restore state, but the state is now in the view model.

  2. you need to tell your view model about lifecycle events.

  3. you might encounter situations where you need to call view methods directly.

For these cases, both the view and the view model should implement interfaces, which are then used for communication via commands, if necessary. In almost all cases, however, only an interface for the view model is needed, since the data binding library handles the interactions with the view, and custom components can be used e.g. when a context is needed.

The view model also updates the model, e.g. by adding a new element to the database or updating an existing one. It is also used to fetch data from the model. Ideally, the model should also notify the view model of changes, but this depends on the implementation.

Now, generelly speaking, the separation of view and view model makes the presentation logic easily testable and also helps with maintenance in the long run. Together with the data binding library this means less code and cleaner code.

已有 1 人翻译此段
我来翻译

Example

<layout xmlns:android="...">
  <data>
    <variable name="vm" type="pkg.MyViewModel" />
  </data>
 
  <FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
 
    <EditText
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:visibility="@{vm.shouldShowText}"
      android:text="@={vm.text}" />
 
    <Button
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:onClick="@{vm::onButtonClick}"
      android:text="@string/button"/>
  </FrameLayout>
</layout>

When you want to use MVVM architecture, your layouts should only reference one variable, the specific view model for this view, in this case MyViewModel. In the view model, you provide properties for the layout. This can be as easy as returning a String from a model object or more complex, depending on your use case.

public class MyViewModel extends BaseObservable {
   private Model model = new Model();
 
   public void setModel(Model model) {
       this.model = model;
       notifyChange();
   }
 
   public boolean shouldShowText() {
       return model.isTextRequired();
   }
 
   public void setText(String text) {
       model.setText(text);
   }
 
   public String getText() {
       return model.getText();
   }
 
   public void onButtonClick(View v) {
       // Save data
   }
}

Here we have a text property. As we have an EditText for user input, we can use two way data-binding, to also have the data binding library save the inputs back to the view model. For this, we create both a setter and a getter and bind the property to the text attribute of our EditText, but this time with an = sign before the bracket, which signals the library that we want two way data binding here.

Also, we only want to show the EditText when our model says that text input is required. For this, we provide a boolean property in our view model and bind it to the visibility attribute. For this to work, we also have to create a binding adapter, which sets the visibility to GONE when false and VISIBLE when true.

@BindingAdapter("android:visibility")
public static void setVisibility(View view, boolean visible) {
   view.setVisibility(visible ? View.VISIBLE : View.GONE);
}
已有 1 人翻译此段
我来翻译
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(0)

返回顶部
顶部