JetPack 组件之 DataBinding 详解

47 篇文章 0 订阅
28 篇文章 0 订阅


DataBinding 是谷歌官方推出的一个库,用于扩展 layout.xml 的功能,给 laytout 增加属性, layout 中的 View 可以使用这些属性,java 代码中可以设置这些属性,从而将 layout 和数据绑定起来。

一、基础用法

1.1 开启 DataBinding 配置

修改 build.gradle 文件:

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

当引用一个开启了 DataBinding 配置的 Module 时,主 Module 也要开启 DataBinding 配置。

1.2 布局中的属性

<?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>

LinearLayout 标签内的是原始的 xml 布局,使用 DataBinding 时,需要在它外面再套一层 layout 标签,再给 layout 标签内添加一个 data 标签。在 data 标签内就可以通过 variable 标签声明 view 可引用的对象,即 layout 的属性。

如上述代码声明了 user 对象,在 TextView 中就可以通过 @{} 引用它的属性 @{user.firstName}

快速转化:在根 ViewGroup 上,按 alt + 回车,就可以将该布局 convert to data binding layout
在这里插入图片描述

1.2.1 数据对象

variable 标签中声明的对象需要添加 name、type 属性。name 就是名字,type 是类名,java.lang 中的类可以直接写类名称,如 String,其他类需要写类全名,如 com.example.User。

要访问到一个对象的属性,可以使用以下三种方式。DataBinding 会按 firstName、getFirstName()、firstName() 的顺序去寻找值。

// public
public class User {
  public String firstName;
  public String lastName;
}
// get属性名() 方法
public class User {
  private String firstName;
  private String lastName;
  
  public String getFirstName() {
    return firstName;
  }
  public String getLastName() {
    return lastName;
  }  
}
// 属性名() 方法
public class User {
  private String firstName;
  private String lastName;
  
  public String firstName() {
    return firstName;
  }
  public String lastName() {
    return lastName;
  }  
}

1.3 代码中的绑定

每个布局文件都会产生一个 binding 类,默认类名是布局文件名去除下划线再加上 Binding 后缀,如 activity_main.xml 的 binding 类是 ActivityMainBinding。存放于 /app/build/generated/source/apt/debug/.../databinding 目录下。(修改类名见 三)

推荐的方式是在 inflating layout 的时候去创建 binding,如下所示:

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

当在 Fragment、ListView/RecyclerView Adapter 中使用时,可以使用 inflate 方法:

ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

1.4 binding 表达式语法

1.4.1 基础语法

binding 表达式可以使用 java 中的几乎所有语法(除了 this、super、new、泛型调用等)。

另有一些特殊语法,如下所示:

  1. 非空判断符号 ??

    android:text="@{user.displayName ?? user.lastName}"
    

    相当于:

    android:text="@{user.displayName != null ? user.displayName : user.lastName}"
    
  2. 非空检查

    binding 表达式取值时会进行非空判断,如果为空,则不进行下一步取值。

    如:

    android:text="@{String.valueOf(user.name)},如果 user 为 null,显示 null
    android:text="@{String.valueOf(user.age)},如果 user 为 null,显示 0

  3. 集合

    <data>
    	<import type="android.util.SparseArray"/>
        <import type="java.util.Map"/>
        <import type="java.util.List"/>
        <variable name="list" type="List&lt;String>"/>
        <variable name="sparse" type="SparseArray&lt;String>"/>
        <variable name="map" type="Map&lt;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]}"
    

    为了使 xml 的语法保持正确,不能使用左尖括号,如 List<String> 要写作 List&lt;String>

    另外,@{map[key]} 可写作 @{map.key}

  4. 字面值

    在 @{} 中使用字面值,取决于 @{} 外部是什么。
    @{} 外部是 ‘’,则用 “” 来包围字面值。
    @{} 外部是 “”,则用 `` 来包围字面值。

    android:text='@{"firstName"}'android:text="@{`firstName`}"

  5. 资源

    android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"
    android:text="@{@string/nameFormat(firstName, lastName)}"
    

1.4.2 import、variables 和 include

import

import 可以使得在 binding 表达式、variable 标签中,可以引用某个类中的类方法、类变量。

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

<TextView
   android:text="@{user.lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

在 visibility 的 binding 表达式中,使用了 View.VISIBLE 和 View.GONE。如果不添加 import,是无法编译成功的。

<data>
    <import type="com.example.User"/>
    <import type="java.util.List"/>
    <variable name="user" type="User"/>
    <variable name="userList" type="List&lt;User>"/>
</data>

在 variable 的 type 中也可以直接使用 import 的类,而不用再引用全名。

别名

当 import 两个同名的类时,可以使用别名对它们进行区分。

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

Variables

variable 标签可以声明 binding 表达式中可使用的变量。

<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 会被合并,所以不能有冲突。

每个 variable 都会默认生成 getter、setter 方法。

每个 layout 文件都会自带一个 context 对象,它来自根 View 的 getContext() 方法。

Includes

binding 支持 include 标签,可以使用 bind 给 include 的布局传递 variable,如下所示:

<?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>

但是不支持给 merge 标签加 layout 外壳。

<?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><!-- Doesn't work -->
       <include layout="@layout/name"
           bind:user="@{user}"/>
       <include layout="@layout/contact"
           bind:user="@{user}"/>
   </merge>
</layout>

1.4.3 事件处理

有两种方式可以进行事件处理:方法引用和监听器绑定

方法引用

事件可以直接绑定到处理器的方法上。

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.MyHandlers"/>
       <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>

这和原生的 android:onClick="..." 的区别在于,原生的 onClick 方法只能在 Activity 中创建,且没有编译期的检查。binding 表达式 的 onClick 可以指定任意对象,并有编译期的检查。

监听器绑定

方法绑定只能把 View 传给某个方法,这个方法的参数、返回值都必须与事件监听器所需的一样。

监听器绑定可以使用任何的 binding 表达式,即它实现的是方法的内容。所以参数不一定跟事件监听器所需的一样,只需返回值一样即可。

如:

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>

这里的 () -> presenter.onSaveClick(task) 相当于监听器方法,而 presenter.onSaveClick(task) 已经是监听器方法里的实现。

所以也可以直接这样:

<TextView
	...
	android:onClick="@{(v) -> v.setVisibility(View.GONE)}"
	.../>

但是尽量不要把复杂的逻辑放在 binding 表达式中,这会带来阅读和维护的成本。

二、使用响应式数据对象

响应式对象,指的是,对象数据变化后,会通知其他对象自己的修改,从而执行一些特定逻辑,如 UI 的变更。

2.1 响应式字段

要实现响应式的效果,数据对象必须实现 Observable 接口(DataBinding 库中的接口),但对于一个简单的类这么做有点过于复杂,所以 DataBinding 库提供了一系列 ObservableXXX 类,如 ObservableBoolean、ObservableByte、ObservableChar、ObservableShort、ObservableInt、ObservableLong、ObservableFloat、ObservableDouble、ObservableParcelable、ObservableField<> 等。

它们使用 get()、set() 来取值和赋值。

实例:

private static class User {
    public final ObservableField<String> firstName = new ObservableField<>();
    public final ObservableField<String> lastName = new ObservableField<>();
    public final ObservableInt age = new ObservableInt();
}
<TextView
	...
	android:text="@{user.firstName}"
	.../>

现在,当我们调用 user.firstName.set("Google"),TextView 就会显示 Google。而不用再去调一遍 setText()。

2.2 响应式集合

ObservableArrayMap:

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:

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"/>

2.3 响应式对象

实现了 Observable 接口的类就拥有了响应式的能力。为了方便开发,DataBinding 库提供了一个 BaseObservable,可继承该类来实现。

实现方式:给 getter 方法加上 Bindable 注解,给 setter 方法加上 notifyPropertyChanged() 语句。

实例:

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);
    }
}

BR 类位于 /app/build/generated/source/apt/debug/.../BR.java,里面包含 DataBinding 所需的资源 id。Bindable 注解在编译时,会往 BR 文件中添加一个条目。

如果一个类的基类不能变动的话,可以使用 PropertyChangeRegistry 来实现响应式的功能。

三、生成绑定类

binding 类用来访问 View 和 Variable,每个布局文件都对应一个 binding 类。binding 类都继承于 ViewDataBinding 类。

生成绑定对象

对于 Activity:

// 第一种方式
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
	MyLayoutBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_my);
}

// 第二种方式
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    MyLayoutBinding binding = MyLayoutBinding.inflate(getLayoutInflater());
    setContentView(binding.getRoot());
}

// 第三种方式
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
	ViewGroup contentView = findViewById(android.R.id.content);
	MyLayoutBinding binding = MyLayoutBinding.inflate(getLayoutInflater(), contentView, true);
}

对于 Fragment、ListView/RecyclerView Adapter :

ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
// or
ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);
// or
View viewRoot = LayoutInflater.from(this).inflate(layoutId, parent, attachToParent);
ListItemBinding binding = DataBindingUtil.bind(viewRoot);

带 ID 的 View

bindind 类中会自动生成所有带 id 的 View 对象,并将其设为 final。这比每次都通过 findViewById() 来找 View 要快多了。

<TextView
    android:id="@+id/tv_hello"
    .../>

生成的 binding 类:

public class ActivityMvvmHelloBinding {
	public final android.widget.TextView tvHello;
	...
}

访问:

binding.tvHello.setText("...");

ViewStubs

在 binding 类中,布局文件中的 ViewStub 会用 ViewStubProxy 来表示。ViewStubProxy 可以代表 ViewStub inflate 前和后的两种状态,inflate 之前能访问到 ViewStub 对象,inflate 之后能访问到 inflate 的 View,如果这个 View 有 binding 配置,那 ViewStubProxy 能获取到相应的 binding 类。

布局中的 ViewStub:

<ViewStub
    android:id="@+id/view_stub"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

在生成的 binding 类中:

public final android.databinding.ViewStubProxy viewStub;

立即绑定

当一个 variable 或 observable 对象改变后,数据绑定会在下一帧进行,如果需要立即执行的话,可以使用 executePendingBindings() 方法。

动态变量

有时,binding 类的类别是未知的,比如在 RecyclerView.Adapter 中有多种 ViewHolder 时。这时可以使 ViewHolder 返回一个 ViewDataBinding,然后调用 setVariable() 方法进行数据绑定。

setVariable 的第一个参数是 variableId,它保存在 BR 文件中,编译时生成。

public void onBindViewHolder(BindingHolder holder, int position) {
    final T item = items.get(position);
    holder.getBinding().setVariable(BR.item, item);
    holder.getBinding().executePendingBindings();
}
...
class BindingHolder extends RecyclerView.ViewHolder {
    ViewDataBinding binding;
    public HelloViewHolder(View itemView) {
        super(itemView);
        binding = ItemRcvHelloBinding.bind(itemView);
    }
    public ViewDataBinding getBinding() {
        return binding;
    }
}

自定义 binding 类名

默认类名是布局文件名去除下划线再加上 Binding 后缀,如 activity_main.xml 的 binding 类是 ActivityMainBinding

通过 data 标签的的 class 属性可以自定义文件名,同时放置到不同的文件夹。

<data class="com.example.ContactItem"></data>

但这样的做法在 AndroidStudio 3.4 上好像有问题,会报 Attribute class is not allowed here 的错。

四、绑定适配器

4.1 设置属性值

自动方法选择

<data>
	<variable 
		name="contentText"
        type="String" />
</data>
<TextView
	android:text="@[contentText]"
	.../>

当调用 binding.setContentText("hello") 时,binding 内部会找到引用它的 View(这里是 TextView),和引用它的属性(这里是 text),然后去找该属性的 setter 方法(这里是 setText(String) 方法),如果没找到就会报错。

如果一个 View 的标签不支持某个属性,但是有 setter 方法,也可以这么设置。

如一个自定义

public class MyTextView extends TextView {
	...
	public void setContentText(String text) {
		System.out.println("exec setContentText with " + text);
	}
}
<MyTextView
	android:text="@[contentText]"
	app:contentText="@[contentText]"
	.../>

现在调用 binding.setContentText("hello"),那 MyTextView 的 setText()、setContentText() 方法都会被调用。

指定一个自定义方法

有时候一些属性和方法名不是一一对应的,这时候可以使用 BindingMethodsBindingMethod 注解来指定对应关系。

一些系统原生的方法不用这样设置,DataBinding 内部已经做了处理,如 android:tint 属性和 setImageTintList(ColorStateList) 方法。

@BindingMethods({
        @BindingMethod(type = MyTextView.class,
                attribute = "contentText",
                method = "setContent")
})
public class MyTextView extends TextView {
	...
    public void setContent(String text) {
        content = text;
    }
}

或者放入一个单独的 Adapter 类:

@BindingMethods({
        @BindingMethod(type = MyTextView.class,
                attribute = "contentText",
                method = "setContent")
})
public class MyTextViewBindingAdapter {
}

提供自定义逻辑

上面的效果页可以使用另一种方式实现:

public class MyTextView extends TextView {
	...
    @BindingAdapter("contentText")
    public static void setContent(View view, String text) {
    	content = text;
    }
}

或者放入一个单独的 Adapter 类:

public class MyTextView extends TextView {
	...
    public void setContent(String text) {
        content = text;
    }
}

public class MyTextViewBindingAdapter {
    @BindingAdapter("contentText")
    public static void setContent(MyTextView view, String text) {
        view.setContent(text);
    }
}

BindingAdapter 注解相比 BindingMethods 的好处在于可以支持多个参数的同时设置:

@BindingAdapter({"imageUrl", "error"})
public static void loadImage(ImageView view, String url, Drawable error) {
  Picasso.get().load(url).error(error).into(view);
}
<ImageView 
	app:imageUrl="@{venue.imageUrl}" 
	app:error="@{@drawable/venueError}" />

这样,在包含的所有属性都改变时,该方法都会被调用。

如果需要任一属性改变,就调用方法,可以加上 requireAll=false

@BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)
public static void setImageUrl(ImageView imageView, String url, Drawable placeHolder) {
  if (url == null) {
    imageView.setImageDrawable(placeholder);
  } else {
    MyImageLoader.loadInto(imageView, url, placeholder);
  }
}

当我们自定义的 BindingAdapter 与 DataBinding 库默认生成的 BindingAdapter 有冲突时,会采用我们自定义的 BindingAdapter。

如 View 的 setPaddingLeft 方法,默认 BindingAdapter 的实现是这样:

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int padding) {
  view.setPadding(padding,
                  view.getPaddingTop(),
                  view.getPaddingRight(),
                  view.getPaddingBottom());
}

如果我们定义了自己的 setPaddingLeft,那就会覆盖默认的。

方法的旧值和新值

如果要在设置新值的时候同时使用旧值,可以在 BindingAdapter 方法中依次传入所有旧值,再依次传入所有新值。

如:

@BindingAdapter("android:paddingLeft")
public static void setPaddingLeft(View view, int oldPadding, int newPadding) {
  if (oldPadding != newPadding) {
      view.setPadding(newPadding,
                      view.getPaddingTop(),
                      view.getPaddingRight(),
                      view.getPaddingBottom());
   }
}

又如:

@BindingAdapter("android:onLayoutChange")
public static void setOnLayoutChangeListener(View view, View.OnLayoutChangeListener oldValue,
       View.OnLayoutChangeListener newValue) {
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
    if (oldValue != null) {
      view.removeOnLayoutChangeListener(oldValue);
    }
    if (newValue != null) {
      view.addOnLayoutChangeListener(newValue);
    }
  }
}
<View android:onLayoutChange="@{() -> handler.layoutChanged()}"/>

当 handler 改变时,setOnLayoutChangeListener 方法就会被调用,移除旧 listener,添加新 listener。

4.2 对象转换

自动对象转换

当 binding 表达式返回的值是一个 Object 类型时,DataBinding 库会将其转化为属性的 setter 方法需要的参数类型。

如 userMap[“lastName”] 返回一个 Object:

<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content" />

DataBinding 库会找到 setText() 方法,setText() 方法需要一个 CharSequence 类型的参数,所以 userMap[“lastName”] 会被转化为 CharSequence。

当然这需要该 Object 本来就是一个 CharSequence,否则运行时会抛出强转异常。

如果 binding 表达式返回的不是一个 Object,而是一个特定类型的值,那如果类型不符合,编译就不会通过。

自定义转换

要解决这种编译不通过的问题,可以设置 BindingConversion 方法。

<View
   android:background="@{isError ? @color/red : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
    return new ColorDrawable(color);
}

但 binding 表达式返回值的值必须是一致的,不能像这样,可能返回 drawable,也可能发挥 color:

<View
   android:background="@{isError ? @drawable/error : @color/white}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

五、绑定到 JetPack 组件

5.1 使用 LiveData 通知 UI 数据改变

相比于 Observable 对象,LiveData 的好处在于它知道订阅者的生命周期。在 Android Studio 3.1 及以后,你可以把 Observable Fields 替换为 LiveData。

要使用 LiveData,需要指定 LiveData 的 Lifecycle Owner。

class ViewModelActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Inflate view and obtain an instance of the binding class.
        UserBinding binding = DataBindingUtil.setContentView(this, R.layout.user);

        // Specify the current activity as the lifecycle owner.
        binding.setLifecycleOwner(this);
    }
}

5.2 使用 ViewModel 去管理 UI 相关数据

DataBinding 库和 ViewModel 库可以无缝衔接。

要使用 ViewModel 组件,需要先创建 ViewModel,然后将 ViewModel 对象作为 binding 类的一个属性。

class ViewModelActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Obtain the ViewModel component.
        UserModel userModel = ViewModelProviders.of(getActivity())
                                                  .get(UserModel.class);

        // Inflate view and obtain an instance of the binding class.
        UserBinding binding = DataBindingUtil.setContentView(this, R.layout.user);

        // Assign the component to a property in the binding class.
        binding.viewmodel = userModel;
    }
}

在 layout 文件中可以使用 ViewModel 对象。

<CheckBox
    android:id="@+id/rememberMeCheckBox"
    android:checked="@{viewmodel.rememberMe}"
    android:onCheckedChanged="@{() -> viewmodel.rememberMeChanged()}" />

5.3 使用 Observable ViewModel 进行更多控制

有时更适合使用实现了Observable 接口的 ViewModel,而不是 LiveData。这样能够提供更多的控制性。

我们可以实现自己的订阅、通知逻辑,通过 addOnPropertyChangedCallback 添加订阅者,通过 removeOnPropertyChangedCallback 移除订阅者,通过 notifyChangenotifyPropertyChanged 通知订阅者。

/**
 * A ViewModel that is also an Observable,
 * to be used with the Data Binding Library.
 */
class ObservableViewModel extends ViewModel implements Observable {
    private PropertyChangeRegistry callbacks = new PropertyChangeRegistry();

    @Override
    protected void addOnPropertyChangedCallback(
            Observable.OnPropertyChangedCallback callback) {
        callbacks.add(callback);
    }

    @Override
    protected void removeOnPropertyChangedCallback(
            Observable.OnPropertyChangedCallback callback) {
        callbacks.remove(callback);
    }

    /**
     * Notifies observers that all properties of this instance have changed.
     */
    void notifyChange() {
        callbacks.notifyCallbacks(this, 0, null);
    }

    /**
     * Notifies observers that a specific property has changed. The getter for the
     * property that changes should be marked with the @Bindable annotation to
     * generate a field in the BR class to be used as the fieldId parameter.
     *
     * @param fieldId The generated BR id for the Bindable field.
     */
    void notifyPropertyChanged(int fieldId) {
        callbacks.notifyCallbacks(this, fieldId, null);
    }
}

六、双向数据绑定

6.1 View 的常规属性与数据双向绑定

使用单向数据绑定,可以通过数据设置 View 的内容。

使用双向数据绑定,View 的属性也可以反过来更新数据的值。

双向数据绑定的 binding 表达式使用 @={}。同时数据类要实现 Observable 接口,或继承 BaseObservable 类,也可以直接使用 ObservableInt、ObservableField<> 等类。

如:

<CheckBox
    android:id="@+id/rememberMeCheckBox"
    android:checked="@={viewmodel.rememberMe}"
/>
public class LoginViewModel extends BaseObservable {
    // private Model data = ...

    @Bindable
    public Boolean getRememberMe() {
        return data.rememberMe;
    }

    public void setRememberMe(Boolean value) {
        // Avoids infinite loops.
        if (data.rememberMe != value) {
            data.rememberMe = value;

            // React to the change.
            saveData();

            // Notify observers of a new value.
            notifyPropertyChanged(BR.remember_me);
        }
    }
}

数据-View:
数据类通过 setRememberMe 更新数据,并通知 View(通过 notifyPropertyChanged 通知),View 通过 getRememberMe 获取数据的值。

View-数据:
View 通过 setChecked() 更新内容,并通知数据类(通过 InverseBindingListener.onChange() 通知),数据类通过通过 View 的 isChecked() 方法获取数据的值。

CheckBox 内容的更新由 CheckBox 自己的监听器 OnCheckedChangeListener 来监听,在 OnCheckedChangeListener 的回调里调用 InverseBindingListener.onChange()。

6.2 View 的自定义属性与数据双向绑定

如 MyView 有一个 自定义属性 time。

用数据给 View 设置内容:

@BindingAdapter("time")
public static void setTime(MyView view, Time newValue) {
    // Important to break potential infinite loops.
    if (view.time != newValue) {
        view.time = newValue;
    }
}

从 View 的内容获取数据:

@InverseBindingAdapter("time")
public static Time getTime(MyView view) {
    return view.getTime();
}

当数据改变时,会调用 BindingAdapter 注解的方法,当 View 内容改变时,需要一个 InverseBindingListener 来调 onChange() 方法,从而通知 Model 进行修改。

InverseBindingListener 由 DataBinding 库自动生成,但 onChange() 方法需要我们自己调用。

获取 InverseBindingListener 对象,需要声明一个 @BindingAdapter 方法,参数为 app/android:属性名AttrChanged。(app: 可省略,android: 不确定,还未验证)

@BindingAdapter("app:timeAttrChanged")
public static void setListeners(
        MyView view, final InverseBindingListener attrChange) {
    // Set a listener for click, focus, touch, etc.
}

获取 InverseBindingListener 对象后,就可以在 View 自己的内容变化监听器里去调 InverseBindingListener.onChange() 方法,然后从 InverseBindingAdapter 注解方法中获取 View 内容代表的数据,从而更新数据。

6.3 转换器

如果传递给 View 的数据需要做一些处理,可以编写一个 Converter 类来处理。

<EditText
    android:id="@+id/birth_date"
    android:text="@={Converter.dateToString(viewmodel.birthDate)}"
/>

import Converter 类后,就可以使用 dataToString 方法。

public class Converter {
    public static String dateToString(EditText view, long oldValue,
            long value) {
        // Converts long to String.
    }
}

但是因为使用的是双向绑定 @={},还需要提供一个逆绑定方法,通过 @InverseMethod 标识。

public class Converter {
    @InverseMethod("stringToDate")
    public static String dateToString(EditText view, long oldValue,
            long value) {
        // Converts long to String.
    }

    public static long stringToDate(EditText view, String oldValue,
            String value) {
        // Converts String to long.
    }
}

6.4 无限循环

因为双向绑定的存在,可能会出现无限循环的情况,要注意新值和旧值的判断,相等时不再往下执行。

一些库中预设的双向绑定属性
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android Jetpack是Google提供的一套用于加速Android应用开发的工具包,其中包括了许多架构组件,其中之一就是ViewModel。 ViewModel是一种设计模式,用于保存和管理与UI相关的数据。在传统的Android开发中,当屏幕旋转或者因为其他原因导致Activity或Fragment重建时,之前保存的临时数据就会丢失。而ViewModel的出现解决了这个问题。 ViewModel的主要作用是将数据与UI组件分离。它的工作方式是创建一个ViewModel类,并在其中保存需要与UI组件交互的数据。这样,当屏幕旋转或重建时,ViewModel实例不会销毁,数据也会得到保留。然后,在Activity或Fragment中,通过获取ViewModel实例,可以轻松地访问这些数据。 使用ViewModel的好处有很多。首先,它可以避免内存泄漏,因为ViewModel的生命周期与Activity或Fragment无关。其次,它可以节省资源,因为当Activity或Fragment销毁时,ViewModel实例可以被系统缓存起来,下次再创建时可以直接返回该实例。另外,由于ViewModel保存了与UI相关的数据,可以减少因为屏幕旋转导致的数据重复加载的问题。 在使用ViewModel时,你可以选择使用Android Jetpack中的其他架构组件来进一步提高开发效率,比如通过LiveData实现数据的观察和通知,或者通过DataBinding来实现UI与数据的自动绑定。 总之,ViewModel是Android Jetpack中非常重要的一个架构组件,它的出现实现了数据与UI的解耦,提高了开发效率,并且解决了数据丢失的问题。希望通过这篇文档的详解,你对ViewModel有了更深入的理解。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值