Android MVVM之Data Binding

作者: mixiu | 来源:发表于2016-09-14 11:36 被阅读2247次

    Data Binding Library


    https://github.com/soul-ferry/DataBindingDemo

    简介:


    Data Binding Library是一个灵活、兼容的support library,可以在Android2.1(API 7)以上使用。

    使用DataBing,Gradle的Android Plugin需要在1.5.0-alpha1以上。

    Build Environment


    首先需要在Android SDK manager中下载DataBing的library。然后在build.gradle中添加dataBindingg节点,如下:

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

    如果你的应用中有其他模块或依赖的库用到了Data Binding,也需要在buidl.gradle中配置。

    Data Binding 的布局文件


    第一个DataBinding表达式

    使用DataBinding后的布局文件与原来的布局文件差距很小,使用DataBinding后的布局文件的根布局是layout,并添加了data节点。布局格式如下

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
        <data>
            <!-- import and variable-->
        </data>
        <!-- 使用Data Binding前的根布局 -->   
    </layout>
    

    eg.

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
        <data>
            <variable name="user" type="com.example.User"/>
        </data>
        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <TextView android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{user.firstName}"/>
            <TextView android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{user.lastName}"/>
         </LinearLayout>
     </layout>
    

    data节点中的user变量描述了可能会在布局中用到的属性值。

    DataBinding表达式写在"@{}"中。在下面的例子中,TextView的text被设置为userfirstName

    <TextView android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="@{user.firstName}"/>
    

    Data对象 (ViewModel)

    假设你有一个User的POJO

    public class User {
        public final String firstName;
        public final String lastName;
        public User(String firstName, String lastName) {   
            this.firstName = firstName;
            this.lastName = lastName;
        }
    }
    

    这种对象的数据永不会改变。常用于应用中只读一次永之后不改变的数据。

    也可以使用JavaBeans:

    public class User {
        private final String firstName;
        private final String lastName;
        public User(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }
        public String getFirstName() {
            return this.firstName;
        }
        public String getLastName() {
            return this.lastName;
        }
     }
    

    DataBinding中上面两个User对象是一样的。@{user.firstName}会访问第一个类的firstName字段第二个类的
    getFirstName()方法。如果存在firstName()方法,也可能会访问firstName()方法。

    绑定数据

    默认情况下,Binding类会根据布局文件的名字自动生成。如main_activity.xml的布局文件会生成MainActivityBinding。一个简单的数据绑定如下:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        MainActivityBinding binding = DataBindingUtil.setContentView(this,
        R.layout.main_activity);
        User user = new User("Test", "User");
        binding.setUser(user);
    }
    

    Event Handling

    Data Binding提供了两种处理事件的方法。

    • Method References(方法引用):你可以直接引用和listener方法签名一致的方法。当一个表达式被解析为Method ReferencesData Binding会把该方法包装成一个listener并设置到对应的View。如果表达式被解析为nullData Binding会设置成对应的listenernull

    • Listener Bindings:也可以使用lambda,Data Binding总是会创建设置到View上的listener,当事件被分发到该Viewlambda表达式会被解析。

    Method References

    与可以在android:onClick属性指定调用的Activity方法类似,事件可以直接绑定到Handler的方法上。后者的优势在于实在编译时处理,如果方法不存在或签名不一致会得到一个编译时错误。下面的例子展示了该用法:

    public class MyHandlers {
        public void onClickFriend(View view) { ... }
    }
    
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
       <data>
           <variable name="handlers" type="com.example.Handlers"/>
           <variable name="user" type="com.example.User"/>
       </data>
       <LinearLayout
           android:orientation="vertical"
           android:layout_width="match_parent"
           android:layout_height="match_parent">
           <TextView android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="@{user.firstName}"
               android:onClick="@{handlers.onClickFriend}"/>
       </LinearLayout>
    </layout>
    

    注意:Handler中方法必须与监听器中方法参数及返回值一样。如上面的例子中Handler的方法为public void onClickFriend(View view)android:onClick需要的监听器为OnClickListener,其方法为public void onClick(View view)

    Listener Bindings

    Listener Bindings只要求Handler中方法与监听器中的方法返回值一致即可。

    public class Presenter {
        public void onSaveClick(Task task){}
    }
    
      <?xml version="1.0" encoding="utf-8"?>
      <layout xmlns:android="http://schemas.android.com/apk/res/android">
          <data>
              <variable name="task" type="com.android.example.Task" />
              <variable name="presenter" type="com.android.example.Presenter" />
          </data>
          <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent">
              <Button android:layout_width="wrap_content" android:layout_height="wrap_content"
              android:onClick="@{() -> presenter.onSaveClick(task)}" />
          </LinearLayout>
      </layout>
    

    在上面的例子中问我们并没有定义传递给 onClick(android.view.View)方法的view参数。Listener bindings提供了两种传递监听器参数的选择:你可以忽略所有的参数。如果你更喜欢使用带参数的,上面的例子也可以改为

    android:onClick="@{(view) -> presenter.onSaveClick(task)}
    

    或者也可以传递更多参数

    public class Presenter {
        public void onSaveClick(View view, Task task){}
    }
    
    android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}
    
    避免监听器混淆

    Listener表达式非常强大,可以让你写出更易读的代码。另一方面,复杂的Listener表达式会让你的布局变得难以读懂、管理。Listener表达式应该尽量简单,只从UI传递有效数据到Callback的方法中。所有的业务逻辑都应该该在Callback的方法中实现。

    存在一些特有的点击事件,它们的点击事件的属性并不是android:onClick。下面的表格列举了对应的属性:

    Class Listener Setter Attribute
    SearchView setOnSearchClickListener(View.OnClickListener) android:onSearchClick
    ZoomControls setOnZoomInClickListener(View.OnClickListener) android:onZoomIn
    ZoomControls setOnZoomOutClickListener(View.OnClickListener) android:onZoomOut

    Layout Details(布局详情)


    Imports

    可以在<data>节点使用import语法,与java相同java.lang.*也是自动导入的。

    <data>
        <import type="android.view.View"/>
    </data>
    

    当有相同类名时,可以使用别名:

    <import type="android.view.View"/>
    <import type="com.example.real.estate.View" alias="Vista"/>
    
    Variables

    Variables用来描述布局文件中的属性值。

    <data>
        <import type="android.graphics.drawable.Drawable"/>
        <variable name="user"  type="com.example.User"/>
        <variable name="image" type="Drawable"/>
        <variable name="note"  type="String"/>
    </data>
    

    在编译期间会对variable进行类型检查,只有实现了Observable接口或是一个observable collectionvariable才会被观察,即当该variable的数据发生变化UI会更新。

    为了适配不同的屏幕你可能会有多个布局文件,Data Binding会合并这些布局文件中的variable,所以这些布局文件中的variable不能有混淆。

    Data Binding会为每个variable自动生成对应的settergetter方法。variable在调用setter前都会有一个默认值:引用类型->nullint->0、布尔值->false

    Data Binding会自动生成一个contextvariable。它来值跟布局的getContext()。可以被用户的variable覆盖。

    Includes

    variable在使用了<include/>布局中的应用:

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:bind="http://schemas.android.com/apk/res-auto">
       <data>
           <variable name="user" type="com.example.User"/>
       </data>
       <LinearLayout
           android:orientation="vertical"
           android:layout_width="match_parent"
           android:layout_height="match_parent">
           <include layout="@layout/name"
               bind:user="@{user}"/>
           <include layout="@layout/contact"
               bind:user="@{user}"/>
       </LinearLayout>
    </layout>
    

    name.xmlcontact.xml布局文件都必须有user variable

    不支持include直接作为merge的直接子元素,如下面的布局:

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:bind="http://schemas.android.com/apk/res-auto">
       <data>
           <variable name="user" type="com.example.User"/>
       </data>
       <merge>
           <include layout="@layout/name"
               bind:user="@{user}"/>
           <include layout="@layout/contact"
               bind:user="@{user}"/>
       </merge>
    </layout>
    

    表达式语言

    共同的功能

    下面的表达式和Java中的一样

    • Mathematical + - / * %
    • String concatenation +
    • Logical && ||
    • Binary & | ^
    • Unary + - ! ~
    • Shift >> >>> <<
    • Comparison == > < >= <=
    • instanceof
    • Grouping ()
    • Literals - character, String, numeric, null
    • Cast
    • Method calls
    • Field access
    • Array access []
    • T*ernary operator ?:

    eg.

    android:text="@{String.valueOf(index + 1)}"
    android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
    android:transitionName='@{"image_" + id}'
    
    消失的操作

    在Java中可用,但在Databinding中不可用的操作

    • this
    • super
    • new
    • Explicit generic invocation
    Null Coalescing Operator(非空运算)
    android:text="@{user.displayName ?? user.lastName}"
    

    等于

    android:text="@{user.displayName != null ? user.displayName : user.lastName}"
    
    Property Reference(属性引用)

    无论Data采用的是那种定义方法,POJO、Java Beans或ObservableFields,在DataBinding的语法中引用都用下面
    的方式。

    android:text="@{user.lastName}"
    
    Avoiding NullPointerException(自动避免空指针异常)

    Data Binding生成的代码会自动检查null值,避免空指针异常。

    例如@{user.name},如果user为空,user,name会被赋值为默认值(null)。如果是int类型的默认值为0。

    Collections (集合)

    常见的集合:arrays, lists, sparse lists, and maps可以通过[]访问。

    <data>
        <import type="android.util.SparseArray"/>
        <import type="java.util.Map"/>
        <import type="java.util.List"/>
        <variable name="list" type="List<String>"/>
        <variable name="sparse" type="SparseArray<String>"/>
        <variable name="map" type="Map<String, String>"/>
        <variable name="index" type="int"/>
        <variable name="key" type="String"/>
        </data>
        …
        android:text="@{list[index]}"
        …
        android:text="@{sparse[index]}"
        …
        android:text="@{map[key]}"
    
    String Literals (字符串)

    如果需要在@{}表达式内使用"",布局的属性值需要使用''

    android:text='@{map["firstName"]}'
    

    或者使用下面的方式

    android:text="@{map[`firstName`}"
    android:text="@{map['firstName']}"
    
    Resources (资源文件)

    可以使用通常的语法在表达式中访问资源文件:

    android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
    

    格式化的字符串和plurals

    android:text="@{@string/nameFormat(firstName, lastName)}"
    android:text="@{@plurals/banana(bananaCount)}"
    

    一些资源需要使用指定的引用方式

    Type Normal Reference Expression Reference
    String[] @array @stringArray
    int[] @array @intArray
    TypedArray @array @typedArray
    Animator @animator @animator
    StateListAnimator @animator @stateListAnimator
    color int @color @color
    ColorStateList @color @colorStateList

    Data Objects (数据对象)


    任何一个POJO都可以用于Data Binding,但是修改了一个并POJO不会更新UI。Data Binding的real power是让你的数据对象(Data Objects)发生数据改变时能够发出通知。这里有3种数据改变通知机制:Observable objects, observable fields, observable collections

    当一个实现了通知机制的数据对象的数据发生改变并且绑定到了UI,UI会自动更新。

    Observable Objects (Observable对象)

    Observableinterface有一个添加和移除listeners的机制,但是发送通知取决于开发者。为了方便开发,Data Binding提供了一个BaseObservable类,BaseObservable实现了listener注册机制。实现类要负责在数据改变时发出通知,这需要通过在getter方法加上@Bindable注解并且在setter方法中发出通知。

    private static class User extends BaseObservable {
        private String firstName;
        private String lastName;
        
        @Bindable
        public String getFirstName() {
            return this.firstName;
        }
        @Bindable
        public String getLastName() {
            return this.lastName;
        }
        public void setFirstName(String firstName) {
            this.firstName = firstName;
            notifyPropertyChanged(BR.firstName);
        }
        public void setLastName(String lastName) {
            this.lastName = lastName;
            notifyPropertyChanged(BR.lastName);
        }
    }
    

    @Bindable注解会在编译期间自动在BR类中生成一个字段,BRR类似。

    ObservableFields

    Data Binding还提供了另外更细粒度、更方便的创建Observable类的方式:ObservableFieldsObservableBoolean, ObservableByte, ObservableChar, ObservableShort, ObservableInt, ObservableLong, ObservableFloat, ObservableDouble, and ObservableParcelable

    eg.

    private static class User {
        public final ObservableField<String> firstName =
            new ObservableField<>();
        public final ObservableField<String> lastName =
            new ObservableField<>();
        public final ObservableInt age = new ObservableInt();
    }
    

    如何访问:

    user.firstName.set("Google");
    int age = user.age.get();
    

    Observable Collections

    一些App使用集合保存数据。Observable Collections允许使用通过来访问数据。

    ObservableArrayMap

    用于引用类型,如String

    ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
    user.put("firstName", "Google");
    user.put("lastName", "Inc.");
    user.put("age", 17);
    

    在布局文件中的使用

    <data>
        <import type="android.databinding.ObservableMap"/>
        <variable name="user" type="ObservableMap<String, Object>"/>
    </data>
    …
    <TextView
        android:text='@{user["lastName"]}'
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text='@{String.valueOf(1 + (Integer)user["age"])}'
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    
    ObservableArrayList

    int

    ObservableArrayList<Object> user = new ObservableArrayList<>();
    user.add("Google");
    user.add("Inc.");
    user.add(17);
    

    在布局文件中的使用

    <data>
        <import type="android.databinding.ObservableList"/>
        <import type="com.example.my.app.Fields"/>
        <variable name="user" type="ObservableList<Object>"/>
    </data>
    …
    <TextView
        android:text='@{user[Fields.LAST_NAME]}'
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    

    Generated Binding(生成Binding)


    上文已经说过生成的Binding类名由布局文件名决定。

    Creating(创建)

    Binding类应该尽可能早地创建,绑定layout的方式有多种,都是通过调用Binding类的静态方法。如:

    MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
    MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater, viewGroup, false);
    MyLayoutBinding binding = MyLayoutBinding.bind(viewRoot);
    
    ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
    parent, attachToParent);
    ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
    

    Views With IDs(带id的View)

    Data Binding会为布局文件中每个带ID的View生成一个public final的字段。这可以让替代findViewById以更快的方式访问View:

    <layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
       <variable name="user" type="com.example.User"/>
    </data>
    <LinearLayout
       android:orientation="vertical"
       android:layout_width="match_parent"
       android:layout_height="match_parent">
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.firstName}"
           android:id="@+id/firstName"/>
       <TextView android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{user.lastName}"
           android:id="@+id/lastName"/>
    </LinearLayout>
    </layout>
    

    生成的Binding类中会有:

    public final TextView firstName;
    public final TextView lastName;
    

    ViewStubs

    布局文件中的ViewStub经过binding之后会转换成ViewStubProxy

    简单用代码说明一下,xml 文件与之前的代码一样,根节点改为 layout,在 LinearLayout 中添加一个 ViewStub,添加 ID。

    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
        <RelativeLayout
             android:layout_width="match_parent"
             android:layout_height="match_parent">
             <ViewStub
                android:id="@+id/view_stub_test"
                android:layout="@layout/stub"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
         </RelativeLayout>
    </layout>
    

    在 Java 代码中获取 binding 实例,为 ViewStubProy 注册 ViewStub.OnInflateListener 事件:

    binding.viewStubTest.setOnInflateListener(new ViewStub.OnInflateListener() {
        @Override
        public void onInflate(ViewStub stub, View inflated) {
            StubBinding stubBinding = DataBindingUtil.bind(inflated);
            ViewStubViewModel model = new ViewStubViewModel();
            model.setName("boco");
            stubBinding.setStubModel(model);
        }
     });
     binding.viewStubTest.getViewStub().inflate();
    

    Advanced Binding(高级绑定)

    Dynamic Variables(动态设置Variables)

    有时候,我们并不知道具体的Binding类型,如在RecyclerView.Adapter中可能会有多个layout,我们必须在onBindViewHolder(VH, int)进行赋值。

    public static class BindingHolder extends RecyclerView.ViewHolder {
        private ViewDataBinding binding;
        public BindingHolder(View itemView) {
            super(itemView);
        }
        public ViewDataBinding getBinding() {
            return binding;
        }
        public void setBinding(ViewDataBinding binding) {
            this.binding = binding;
        }
    }
    @Override
    public BindingHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        ViewDataBinding binding = DataBindingUtil.inflate(
                LayoutInflater.from(viewGroup.getContext()),
                R.layout.list_item,
                viewGroup,
                false);
        BindingHolder holder = new BindingHolder(binding.getRoot());
        holder.setBinding(binding);
        return holder;
    }
    @Override
    public void onBindViewHolder(BindingHolder holder, int position) {
        User user = users.get(position);
        holder.getBinding().setVariable(BR.user, user);
        holder.getBinding().executePendingBindings();
    }
    
    Immediate Binding(立即绑定)

    当一个variable或者observable发生了改变,数据绑定会在下一帧之前发生但不是立刻发生,如果你希望数据绑定立刻发生需要使用executePendingBindings() 方法,eg.

    public void onBindViewHolder(BindingHolder holder, int position) {
       final T item = mItems.get(position);
       holder.getBinding().setVariable(BR.item, item);
       holder.getBinding().executePendingBindings();
    }
    

    Background Thread

    你可以在后台线程中改变不是集合的数据,Data binding will localize each variable / field while evaluating to avoid any concurrency issues.

    Attribute Setters (属性赋值)


    自动赋值

    有了 Data Binding,即使属性没有在 declare-styleable 中定义,我们也可以通过 xml 进行赋值操作。

    <android.support.v4.widget.DrawerLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:drawerListener="@{fragment.drawerListener}"/>
    

    在上面的例子中app:drawerListener="@{fragment.drawerListener}会调用DrawerLayout的setDrawerListener(DrawerListener listener)方法具体的方法参数类型和@{}中的参数类型一致。

    重命名

      <Button
           android:id="@+id/email_sign_in_button"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           app:myText="@{login.name}"
           android:onClick="@{loginHandler.onClick}"
           />
    

    在上面的xml对应的ViewModel中加入如下注解,app:myText="@{login.name}便会调用Button的setText方法。

    @BindingMethods({
            @BindingMethod(type = android.widget.Button.class,
                    attribute = "myText",
                    method = "setText"),
    })
    

    自定义

    ViewModel中的方法
    @BindingAdapter({"app:myText"})
     public static void showText(Button button,String newV,String oldV){
         button.setText(newV);
         Log.w(TAG,"new="+newV+"--oldV="+oldV);
     }
     or
     @BindingAdapter({"app:myText"})
     public static void showText(Button button,String newV){
         button.setText(newV);
         Log.w(TAG,"new="+newV);
     } 
    

    对应得xml代码:

      <Button
           android:id="@+id/email_sign_in_button"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           app:myText="@{login.name}"
           android:onClick="@{loginHandler.onClick}"
           />
    

    转换器 (Converters)

    在 xml 中为属性赋值时,如果变量的类型与属性不一致,通过 DataBinding 可以进行转换。
    例如,下面代码中如果要为属性 android:background 赋值一个 int 型的 color 变量:

    <View
        android:background="@{isError.get() ? @color/red : @color/white}"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_height="@{height}" />
    

    只需要定义一个标记了 @BindingConversion 的静态方法即可(方法的定义位置可以随意):

    @BindingConversion
    public static ColorDrawable convertColorToDrawable(int color) {
        return new ColorDrawable(color);
    }
    

    相关文章

      网友评论

      本文标题:Android MVVM之Data Binding

      本文链接:https://www.haomeiwen.com/subject/ddsvettx.html