DataBinding
说到DataBinding
,大家就会想到双向绑定。那究竟什么是双向绑定,其实对于刚接触的人来说是需要去理解一下的。
在MVVM
中,View
和Model
是互相隔离的。假设有以下的EditText
布局:
android:text="@{echo.text}"
那么显而易见,当echo.text
发生变化时,我们希望EditText
中的android:text
属性可以自动变化。这是Model
层->View
层的绑定。
反之,当用户通过View
层在EditText
中进行输入时,我们希望echo.text
字段也可以同步更新到最新的输入值。这是View
到Model
层的绑定。这一点很多人都会忽略。
那么首先我们看是Model
层->View
层的绑定是怎么实现的,在DataBinding
中大家都知道有以下两种方式:
-
Model
继承BaseObservable
,get
带上@Bindable
注解 - 相应字段使用
Observablexxx
变量,如下text
字段(其实Observablexxx
就是继承BaseObservable
)
可以简单看下BaseObservable
的源码,后面会用到:
public class BaseObservable implements Observable {
@Override
public void addOnPropertyChangedCallback(@NonNullOnPropertyChangedCallback callback) {
...
mCallbacks.add(callback);
}
public void notifyPropertyChanged(int fieldId) {
...
mCallbacks.notifyCallbacks(this, fieldId, null);
}
就是一个回调模式。
public class Echo {
public ObservableField<String> text = new ObservableField<>();
}
当使用ObservableField
后,就真正使用了观察者的模式。也就是说当调用setEcho
方法后,一个监听器就被注册了,这个监听器会在每次text
字段被更新后去更新视图。
先来一段小小的源码分析,每个layout
生成的xxxBinding
都是关键的类,里面有一个executeBinding
方法。
我们来看,一个简单的
android:text="@{model.str}"
会生成什么模板代码?
@Override
protected void executeBindings() {
long dirtyFlags = 0;
synchronized(this) {
dirtyFlags = mDirtyFlags;
mDirtyFlags = 0;
}
java.lang.String modelStr = null;
me.lizz0y.myapplication.VM model = mModel;
if ((dirtyFlags & 0x3L) != 0) {
if (model != null) {
// read model.str
modelStr = model.str;
}
}
// batch finished
if ((dirtyFlags & 0x3L) != 0) {
// api target 1
android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, modelStr);
}
}
关键代码:
modelStr = model.str;
android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, modelStr);//这个`xxxAdapter`后面再说,这里就看出简单的赋值就好了
如果变成
public ObservableField<String> str = new ObservableField<String>("sss");
我们会发现,在executeBinding
中多了一句:
updateRegistration(0, modelStr);//localFieldId
将这个变量的fieldId
与model.str
这个Observable
绑定在一起,同时使用前面的addOnPropertyChangedCallback
把ViewBinding
类作为回调传进去。
最终,当我们对mode.str
进行set
操作时,一系列回调最终走到ViewDataBinding
的
@Override
protected boolean onFieldChange(int localFieldId, Object object, int fieldId) {
switch (localFieldId) {
case 0 :
return onChangeModelStr((android.databinding.ObservableField<java.lang.String>) object, fieldId);
}
return false;
}
其实就是对str
这个变量赋予脏位,让下次屏幕刷新时更新这个变量对应的View
。
介绍一些常用的运算符:
运算符
@BindingConversion
如果在xml
里我这么写:android:background="@{@color/blue}"
会报错,因为background
应该是drawable
。所以要进行自动转化,所以需要进行如下定义:
//转化@color/blue为drawable
@BindingConversion
public static ColorDrawable convertColorToDrawable(int color) {
return new ColorDrawable(color);
}
经过前面分析也很简单:
android.databinding.adapters.ViewBindingAdapter.setBackground(
this.mboundView0,me.lizz0y.myapplication.VM.convertColorToDrawable(
mboundView0.getResources().getColor(R.color.blue)));
当然这里显然有人会问,假设我定义了多个怎么破,结论就是后面的覆盖前面的。。。
@BindAdapter
举个栗子就明白了:
@BindingAdapter({"imageUrl"})
public static void loadImage(ImageView view, String u) {
RequestOptions options = new RequestOptions()
.centerCrop()
.placeholder(R.mipmap.ic_launcher_round)
.error(R.mipmap.ic_launcher)
.priority(Priority.HIGH)
.diskCacheStrategy(DiskCacheStrategy.NONE);
Glide.with(view.getContext()).applyDefaultRequestOptions(options).load(u).transition(new DrawableTransitionOptions().crossFade(1000)).into(view);
}
xml
里这么写:
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:imageUrl="@{user.url}" />
这就每次更新user.url
时就会自动重新设置图片。
DataBinding
在set
属性attr
时会先看View
有没有setXXX
方法。如果没有就去找有没有BindingAdapter
注解设置对应的方法。
前面分析源码的时候提到过
if ((dirtyFlags & 0x3L) != 0) {
// api target 1
android.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, modelStr);
}
我们看这里的BindingAdapter
:
@BindingAdapter("android:text")
public static void setText(TextView view, CharSequence text) {
final CharSequence oldText = view.getText();
if (text == oldText || (text == null && oldText.length() == 0)) {
return;
}
if (text instanceof Spanned) {
if (text.equals(oldText)) {
return; // No change in the spans, so don't set anything.
}
} else if (!haveContentsChanged(text, oldText)) {
return; // No content changes, so don't set anything.
}
view.setText(text);
}
可以看到会比较oldText&newText
,以防无限循环。
Component
我们可以定义多个BindingAdapter
,但究竟想要哪个发挥作用呢? 就可以使用这个Component
BindingMethod
该注解可以帮助我们重新命名view
属性对应的setter
方法名称。
@BindingMethods({@BindingMethod(type = NestedScrollView.class, attribute = "custom", method = "setMyCustomAttr")})
注解在类上。
个人觉得他与BindingAdapter
的区别在于:
- 参数,
BindingAdapter
可以拿到view
引用 component
监听属性变更
databinding
让我们省去了各种监听函数,但有的时候我们需要在属性变化时做一些额外的事情:
mModel.addOnPropertyChangedCallback(new Observable.OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable observable, int i) {
if (i == BR.name) {
Toast.makeText(TwoWayActivity.this, "name changed",
Toast.LENGTH_SHORT).show();
} else if (i == BR.password) {
Toast.makeText(TwoWayActivity.this, "password changed",
Toast.LENGTH_SHORT).show();
}
}
});
双重绑定
下面我们来说说如果从View
->Model
的绑定。我们看在前面已有的基础上,我们如何自己实现双重绑定
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Text 1"
android:text="@{echo.text}"
android:addTextChangedListener="@{echo.watcher}"/>
假设有两个EditText
都使用了android:text
,可以看到一个改变并不能连带带动另一个EditText
,因为EditText
的输入并没有手动去调用setField
方法。所以显而易见需要再使用DataBinding
赋予textChangedListener
public TextWatcher watcher = new TextWatcherAdapter() {
@Override public void afterTextChanged(Editable s) {
if (!Objects.equals(text.get(), s.toString())) { //防止无限循环
text.set(s.toString());
}
}
};
customBinding
public class BindableString extends BaseObservable {
private String value;
public String get() {
return value != null ? value : “”;
}
public void set(String value) {
if (!Objects.equals(this.value, value)) {
this.value = value;
notifyChange();
}
}
public boolean isEmpty() {
return value == null || value.isEmpty();
}
}
@BindingConversion
public static String convertBindableToString(
BindableString bindableString) {
return bindableString.get();
}
当主动调用BindableString.set
时会通过notifyChange
去触发UI更新,UI更新时调用convertBindableToString
取出string
绑定
或者BindAdapter
:
@BindingAdapter({“app:binding”})
public static void bindEditText(EditText view,
final BindableString bindableString) {
Pair<BindableString, TextWatcherAdapter> pair =
(Pair) view.getTag(R.id.bound_observable);
if (pair == null || pair.first != bindableString) {
if (pair != null) {
view.removeTextChangedListener(pair.second);
}
TextWatcherAdapter watcher = new TextWatcherAdapter() {
public void onTextChanged(CharSequence s,
int start, int before, int count) {
bindableString.set(s.toString());
}
};
view.setTag(R.id.bound_observable,
new Pair<>(bindableString, watcher));
view.addTextChangedListener(watcher);
}
String newValue = bindableString.get();
if (!view.getText().toString().equals(newValue)) {
view.setText(newValue);
}
}
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Text 1"
app:binding="@{echo.text}"/>
或者可以使用BindAdapter
:
@BindingAdapter({“app:binding”})
public static void bindEditText(EditText view,
final BindableString bindableString) {
Pair<BindableString, TextWatcherAdapter> pair =
(Pair) view.getTag(R.id.bound_observable);
if (pair == null || pair.first != bindableString) {
if (pair != null) {
view.removeTextChangedListener(pair.second);
}
TextWatcherAdapter watcher = new TextWatcherAdapter() {
public void onTextChanged(CharSequence s,
int start, int before, int count) {
bindableString.set(s.toString());
}
};
view.setTag(R.id.bound_observable,
new Pair<>(bindableString, watcher));
view.addTextChangedListener(watcher);
}
String newValue = bindableString.get();
if (!view.getText().toString().equals(newValue)) {
view.setText(newValue);
}
}
当然,google
不可能真的这么蠢。。让我们自己去实现这一套,它在xml
里给我们提供了很简单的运算符@=
@=
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textNoSuggestions"
android:text="@={model.name}"/>
这么搞完当EditText
更新时就自动更新model.name
字段。当然,肯定很好奇背后的实现(这一part看了我半天。。) 它的实现是由多个注解完成的,先看其中两个:
- @InverseBindingAdapter
- @InverseBindingListener
TextViewBindingAdapter.java:
@BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
"android:afterTextChanged", "android:textAttrChanged"}, requireAll = false)
public static void setTextWatcher(TextView view, final BeforeTextChanged before,
final OnTextChanged on, final AfterTextChanged after,
final InverseBindingListener textAttrChanged) {
final TextWatcher newValue;
if (before == null && after == null && on == null && textAttrChanged == null) {
newValue = null;
} else {
newValue = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
if (before != null) {
before.beforeTextChanged(s, start, count, after);
}
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (on != null) {
on.onTextChanged(s, start, before, count);
}
if (textAttrChanged != null) {
textAttrChanged.onChange();
}
}
@Override
public void afterTextChanged(Editable s) {
if (after != null) {
after.afterTextChanged(s);
}
}
};
}
final TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
if (oldValue != null) {
view.removeTextChangedListener(oldValue);
}
if (newValue != null) {
view.addTextChangedListener(newValue);
}
}
@InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
public static String getTextString(TextView view) {
return view.getText().toString();
}
xxxViewBinding.java
private android.databinding.InverseBindingListener mboundView2androidTextAttrChanged = new android.databinding.InverseBindingListener() {
@Override
public void onChange() {
// Inverse of model.str.get()
// is model.str.set((java.lang.String) callbackArg_0)
java.lang.String callbackArg_0 = android.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView2);
// localize variables for thread safety
// model
me.lizz0y.myapplication.VM model = mModel;
// model.str != null
boolean modelStrJavaLangObjectNull = false;
// model != null
boolean modelJavaLangObjectNull = false;
// model.str.get()
java.lang.String modelStrGet = null;
// model.str
android.databinding.ObservableField<java.lang.String> modelStr = null;
modelJavaLangObjectNull = (model) != (null);
if (modelJavaLangObjectNull) {
modelStr = model.str;
modelStrJavaLangObjectNull = (modelStr) != (null);
if (modelStrJavaLangObjectNull) {
modelStr.set(((java.lang.String) (callbackArg_0)));
}
}
}
};
也很简单,当text
发生变化时,触发onTextChange
,然后调用mboundView2androidTextAttrChanged.onChange
,里面调用了由@InverseBindingAdapter
注解的getTextString
去获取值赋给model
总结一下,假设你要给一个自定义属性双向绑定,写上@=
时:你需要写以下函数:
@InverseBindingAdapter(attribute = "refreshing", event = "refreshingAttrChanged")
public static boolean getRefreshing(PhilView view) { //赋值时来这里取
return isRefreshing;
}
@BindingAdapter(value = {"refreshingAttrChanged"}, requireAll = false)
public static void setRefreshingAttrChanged(PhilView view, final InverseBindingListener inverseBindingListener) {
Log.d(TAG, "setRefreshingAttrChanged");
if (inverseBindingListener == null) {
view.setRefreshingListener(null);
} else {
mInverseBindingListener = inverseBindingListener;
view.setRefreshingListener(mOnRefreshingListener);
}
}
@InverseMethod & @InverseBindingMethod[s]
只是简化了一下@InverseBindingAdapter
的注解。
与RecyclerView
public class MyViewHolder extends RecyclerView.ViewHolder {
private final ItemBinding binding;
public MyViewHolder(ItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(Item item) {
binding.setItem(item);
binding.executePendingBindings();
}
}
强刷executePendingBinding
强制绑定操作马上执行,而不是推迟到下一帧刷新时。RecyclerView 会在 onBindViewHolder 之后立即测量 View。如果因为绑定推迟到下一帧绘制时导致错误的数据被绑定到 View 中, View 会被不正确地测量,因此这个 executePendingBindings() 方法非常重要!
todoApp结构
1.png现在先让我们忘记前面说的一切有关双向绑定的事情。。。来看看google
推出的架构LiveData
&ViewModel
我们先看平时开发时会有哪些问题:
通常Android系统来管理UI controllers(如Activity、Fragment)的生命周期,由系统响应用户交互或者重建组件,用户无法操控。当组件被销毁并重建后,原来组件相关的数据也会丢失,如果数据类型比较简单,同时数据量也不大,可以通过onSaveInstanceState()存储数据,组件重建之后通过onCreate(),从中读取Bundle恢复数据。但如果是大量数据,不方便序列化及反序列化,则上述方法将不适用。
UI controllers经常会发送很多异步请求,有可能会出现UI组件已销毁,而请求还未返回的情况,因此UI controllers需要做额外的工作以防止内存泄露。
当Activity因为配置变化而销毁重建时,一般数据会重新请求,其实这是一种浪费,最好就是能够保留上次的数据。
解决
fragment
和fragment
之间通信的问题
LiveData
LiveData
,顾名思义和生命周期绑定,解决上面的第二个异步问题:
-
能够感知组件(Fragment、Activity、Service)的生命周期;
-
只有在组件出于激活状态(STARTED、RESUMED)才会通知观察者有数据更新;
public class NameViewModel extends ViewModel{
// Create a LiveData with a String
private MutableLiveData<String> mCurrentName;
// Create a LiveData with a String list
private MutableLiveData<List<String>> mNameListData;
public MutableLiveData<String> getCurrentName() {
if (mCurrentName == null) {
mCurrentName = new MutableLiveData<>();
}
return mCurrentName;
}
public MutableLiveData<List<String>> getNameList(){
if (mNameListData == null) {
mNameListData = new MutableLiveData<>();
}
return mNameListData;
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mNameViewModel = ViewModelProviders.of(this).get(NameViewModel.class);
mNameViewModel.getCurrentName().observe(this,(String name) -> {
mTvName.setText(name);
Log.d(TAG, "currentName: " + name);
}); // 订阅LiveData中当前Name数据变化,以lambda形式定义Observer
mNameViewModel.getNameList().observe(this, (List<String> nameList) -> {
for (String item : nameList) {
Log.d(TAG, "name: " + item);
}
}); // 订阅LiveData中Name列表数据变化,以lambda形式定义Observer
}
当组件处于激活状态,并且mCurrentName
变量发生变化时,fragment
观察者就会收到监听
ViewModel
[站外图片上传中...(image-93bfcd-1526989876647)]
说实话一开始看到这个我总以为跟MVVM
的ViewModel
有什么异曲同工之妙,事实证明我想多了。此ViewModel
是用来存储和管理UI相关的数据。
ViewModel
是生存在整个生命周期内的,所以在这个类中不能存在android.content.Context;
简单来说不能有view
或context
的引用,所以一般都会传ApplicationContext
进去。
用法很简单:
public class MyActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
// Create a ViewModel the first time the system calls an activity's onCreate() method.
// Re-created activities receive the same MyViewModel instance created by the first activity.
MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
model.getUsers().observe(this, users -> {
// update UI
});
}
}
public class MyViewModel extends ViewModel {
private MutableLiveData<List<User>> users;
public LiveData<List<User>> getUsers() {
if (users == null) {
users = new MutableLiveData<List<Users>>();
loadUsers();
}
return users;
}
private void loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
很容易看出它为什么可以解决以上问题, ViewModelProviders.of(this).get(MyViewModel.class);
调用这个时,内部代码会帮我们做存储。至于怎么做存储,也很常见,加了一个fragment
并setRetain(true)
就可以了。这样重建恢复后拿的还是同一个ViewModel
,因此显然数据也都还在。同时,一个Activity
对应的两个fragemt
也可以通信:
public class SharedViewModel extends ViewModel {
private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
public void select(Item item) {
selected.setValue(item);
}
public LiveData<Item> getSelected() {
return selected;
}
}
public class MasterFragment extends Fragment {
private SharedViewModel model;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}
public class DetailFragment extends Fragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.getSelected().observe(this, { item ->
// Update the UI.
});
}
}
使用getActivity
拿到同一份ViewModel
,就可以拿到同一份数据。也很容易看到,其实LiveData
是需要与ViewModel
结合在一起用的
todoApp-mvvm-live
从todoApp
可以看出最明显的区别:
@NonNull
public static TaskDetailViewModel obtainViewModel(FragmentActivity activity) {
// Use a Factory to inject dependencies into the ViewModel
ViewModelFactory factory = ViewModelFactory.getInstance(activity.getApplication());
return ViewModelProviders.of(activity, factory).get(TaskDetailViewModel.class);
}
其次,其实这个例子还是跟dataBinding
搞在一起了,否则拿回数据更新UI时需要用到大量LiveData
的observe
函数。
最后所以用了LiveData
的作用在哪,我们看一个例子,点击某个按钮后跳转Activity
:
before
public class TasksActivity extends AppCompatActivity implements TaskItemNavigator, TasksNavigator {
....
}
@Nullable
private WeakReference<TaskItemNavigator> mNavigator;
在ViewModel
中:
public void taskClicked() {
String taskId = getTaskId();
if (taskId == null) {
// Click happened before task was loaded, no-op.
return;
}
if (mNavigator != null && mNavigator.get() != null) { //烦躁
mNavigator.get().openTaskDetails(taskId);
}
}
after
TasksActivity.java
// Subscribe to "open task" event
mViewModel.getOpenTaskEvent().observe(this, new Observer<String>() {
@Override
public void onChanged(@Nullable String taskId) {
if (taskId != null) {
openTaskDetails(taskId);
}
}
});
// mTasksViewModel.getOpenTaskEvent().setValue(task.getId());
利用liveData
的生命周期特性,就不用管activity
是否已经消失。
调试
Databinding
想调试自动生成的代码,需要在setting
里选择Reference code generated by the compiler
一些额外的技巧
imageimage
image
网友评论