-
概述
之前我们在DataBinding原理已经大概了解了DataBinding的基本原理,根据之前的源码分析,我们知道,由build generated的继承自ViewDataBinding的XxxBindingImpl的executeBindings方法中实现了数据绑定的逻辑,结合这个我们来看这么点:
- LiveData和StateFlow的自动刷新UI的细节;
- 内置的双向绑定语法;
- 自定义属性的绑定和其双向绑定。
-
Demo
/** * 数据改变自动响应UI变化的三种内置方式。 * 注意,LiveData和StateFlow二者都需要设置LifecycleOwner才行 * */ //1.LiveData val ageLive = MutableLiveData(0) //2.StateFlow private val _ageFlow = MutableStateFlow(0) val ageFlow: StateFlow<Int> = _ageFlow //3.ObservableField val ageObs = ObservableInt() //val ageObs = ObservableField<Int>()
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="viewModel" type="com.mph.bpp.viewmodel.ScoreFragmentViewModel" /> </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView ... android:text='@{"StateFlow: "+viewModel.ageFlow}' /> <TextView ... android:text='@{"LiveData: "+viewModel.ageLive}'/> <TextView ... android:text='@{"ObservableXxx: "+viewModel.ageObs}'/> <CheckBox ... android:checked="@={viewModel.checkState}"/> </androidx.constraintlayout.widget.ConstraintLayout> </layout>
对于这三种方式,DataBinding内部在build时都会生成对应的数据更新注册方法,根据之前的博文我们知道,也就是updateRegistration方法。其中ObservableXxx系列字段是不自动支持生命周期感知特性的,而其他两种都需要设置才能生效,下面我们会从源码上找到答案。
-
LiveData和StateFlow的生命感知特性
ViewDataBinding类的updateRegistration方法中的第三个参数是CreateWeakListener,对于LiveData来说,注册时传入的是CREATE_LIVE_DATA_LISTENER:
private static final CreateWeakListener CREATE_LIVE_DATA_LISTENER = new CreateWeakListener() { @Override public WeakListener create( ViewDataBinding viewDataBinding, int localFieldId, ReferenceQueue<ViewDataBinding> referenceQueue ) { return new LiveDataListener(viewDataBinding, localFieldId, referenceQueue) .getListener(); } };
对于StateFlow来说,传入的是CREATE_STATE_FLOW_LISTENER(这个在ViewDataBindingKtx.kt中):
private val CREATE_STATE_FLOW_LISTENER = CreateWeakListener { viewDataBinding, localFieldId, referenceQueue -> StateFlowListener(viewDataBinding, localFieldId, referenceQueue) .listener }
根据原理,绑定流程中都会调用addListener方法,它们的这个方法中都判断了如果没设置lifecycleOwner的话就不添加数据监听。
以上是为什么要设置lifecycleOwner的原因,那么lifecycleOwner用在哪呢?
LiveData是根据liveData.observe(lifecycleOwner, LiveDataListener)来设置监听的,关于这个方法可以移步至ViewModel之LiveData查看原理,这里就不赘述了。
StateFlow会在addListener方法中确认设置了lifecycleOwner之后调用startCollection方法:
private fun startCollection(owner: LifecycleOwner, flow: Flow<Any?>) { observerJob?.cancel() observerJob = owner.lifecycleScope.launch { owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { flow.collect { listener.binder?.handleFieldChange( listener.mLocalFieldId, listener.target, 0 ) } } } }
repeatOnLifecycle扩展方法里就不展开了,就是给LifecycleOwner设置了一个Observer,在ON_START到ON_START之间会用协程调用StateFlow的collect方法(suspend方法),也就是StateFlowImpl的collect方法,其会更新_state的value值,并会调用collector.emit方法回调到上面的handleFieldChange方法,从而触发executeBindings方法。
这就是它们的生命周期感知特性的实现原理。
-
关于数据刷新
自动生成的executeBindings方法中的部分实现如下:
@Override protected void executeBindings() { long dirtyFlags = 0; synchronized(this) { dirtyFlags = mDirtyFlags; mDirtyFlags = 0; } androidx.lifecycle.MutableLiveData<java.lang.Integer> viewModelAgeLive = null; java.lang.Integer viewModelAgeLiveGetValue = null; java.lang.String javaLangStringObservableXxxViewModelAgeObs = null; kotlinx.coroutines.flow.StateFlow<java.lang.Integer> viewModelAgeFlow = null; java.lang.Integer viewModelAgeFlowGetValue = null; int viewModelAgeObsGet = 0; java.lang.String javaLangStringLiveDataViewModelAgeLive = null; androidx.databinding.ObservableInt viewModelAgeObs = null; java.lang.String javaLangStringStateFlowViewModelAgeFlow = null; com.mph.bpp.viewmodel.ScoreFragmentViewModel viewModel = mViewModel; if ((dirtyFlags & 0x37L) != 0) { //如果LiveData值改变了的话 if ((dirtyFlags & 0x31L) != 0) { if (viewModel != null) { viewModelAgeLive = viewModel.getAgeLive(); } updateLiveDataRegistration(0, viewModelAgeLive); if (viewModelAgeLive != null) { viewModelAgeLiveGetValue = viewModelAgeLive.getValue(); } javaLangStringLiveDataViewModelAgeLive = ("LiveData: ") + (viewModelAgeLiveGetValue); } //如果StateFlow值改变了的话 if ((dirtyFlags & 0x32L) != 0) { if (viewModel != null) { viewModelAgeFlow = viewModel.getAgeFlow(); } androidx.databinding.ViewDataBindingKtx.updateStateFlowRegistration(this, 1, viewModelAgeFlow); if (viewModelAgeFlow != null) { viewModelAgeFlowGetValue = viewModelAgeFlow.getValue(); } javaLangStringStateFlowViewModelAgeFlow = ("StateFlow: ") + (viewModelAgeFlowGetValue); } //如果ObservableXxx值改变了的话 if ((dirtyFlags & 0x34L) != 0) { if (viewModel != null) { viewModelAgeObs = viewModel.getAgeObs(); } updateRegistration(2, viewModelAgeObs); if (viewModelAgeObs != null) { viewModelAgeObsGet = viewModelAgeObs.get(); } javaLangStringObservableXxxViewModelAgeObs = ("ObservableXxx: ") + (viewModelAgeObsGet); } } ... //设置StateFlow的值 if ((dirtyFlags & 0x32L) != 0) { androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvLabel, javaLangStringStateFlowViewModelAgeFlow); } //设置LiveData的值 if ((dirtyFlags & 0x31L) != 0) { androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvLabel2, javaLangStringLiveDataViewModelAgeLive); } //设置ObservableXxx的值 if ((dirtyFlags & 0x34L) != 0) { androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvLabel3, javaLangStringObservableXxxViewModelAgeObs); } }
这里的if语句都是根据dirtyFlags来判定的,这个值在初始化完成后(即第一次调用executeBindings后)就变成0了,这代表着目前没有数据改动,那么0x3..L这些值是什么意思呢?
当上述的可监听数据变化后,根据原理,会回调到onFieldChange方法中:
@Override protected boolean onFieldChange(int localFieldId, Object object, int fieldId) { switch (localFieldId) { case 0 : return onChangeViewModelAgeLive((androidx.lifecycle.MutableLiveData<java.lang.Integer>) object, fieldId); case 1 : return onChangeViewModelAgeFlow((kotlinx.coroutines.flow.StateFlow<java.lang.Integer>) object, fieldId); case 2 : return onChangeViewModelAgeObs((androidx.databinding.ObservableInt) object, fieldId); } return false; }
localFieldId是调用updateRegistration方法时传入的,根据这个执行不同数据的更新操作(三个方法合成一个伪代码方法):
//方法名根据变量名生成 private boolean onChangeViewModelXxx(androidx.lifecycle.MutableLiveData<java.lang.Integer> ViewModelAgeLive, int fieldId) { //自动生成的数据绑定传入的fieldId都是BR._all if (fieldId == BR._all) { synchronized(this) { //LiveData mDirtyFlags |= 0x1L; //StateFlow mDirtyFlags |= 0x2L; //ObservableXxx mDirtyFlags |= 0x4L; } return true; } return false; }
因为mDirtyFlags初始化后变成了0,因此这里mDirtyFlags进行或操作后会变成0x1L、0x2L或者0x4L。
回到executeBindings方法中我们就能理解,0x3这个前缀我们不知道什么意义,也不需要知道,我们只需要知道在这里进行与操作二进制比较后会进入对应的属性设置逻辑,updateXxxRegistration方法里都有防重复设置监听判断,所以不用担心性能浪费,然后会调用LiveData和StateFlow的getValue方法、ObservableXxx的get方法获取最新值,最后调用TextViewBindingAdapter的setText方法设置新值:
@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); }
这里用到了BindingAdapter注解,是为了防止陷入死循环,下面会讲到。
-
自定义属性
系统内置View属性会查找对应的setXxx方法来赋值,而对于自定义的属性或者是系统内置的但是没有实现对应set方法的属性来说,就需要使用@BindingAdapter注解来定义“set”方法,比如:
class CustomDataAdapter { companion object { //必须是静态方法 @JvmStatic @BindingAdapter("app:name") fun aaa(v: TextView, oldName: String, newName: String) { v.text = name } //一个方法可以设置多个属性 @JvmStatic @BindingAdapter("app:name", "sex") //or //@BindingAdapter(value = ["app:name", "sex"], requireAll = false) //@BindingAdapter("app:name", "sex", requireAll = false) fun aaa(v: TextView, name: String?, sex: Int?) { v.text = name + sex } } } <Button ... app:name='@{@string/add_one}' sex="@{@integer/sex}" />
关于它的使用有如下规则:
- 必须是静态方法;
- @BindingAdapter的value值必须和xml中使用的一致;
- 方法名可以随便写,但是建议写成相关易理解的名字;
- 方法至少需要两个参数:第一个是组件的类型(可以使用父类型接收),第二个是xml中设置的值;
- 如果@BindingAdapter中有多个value,则方法参数个数必须与其一致且一一对应,requireAll表示是否所有属性都需要设置,默认为true;
- 如果@BindingAdapter中有一个value但是有三个方法参数,则第二个表示上次的旧值,最后一个表示新值;
- xml中必须用@{}引用数据;
- @{}引用中直接写成死数据非英文字符串的话会出现乱码,比如'@{"你好"}',所以要使用@{@string/name}这样引用配置数据或者动态变量;
- xml中的自定义属性可以不加namespace前缀,但必须在resources中定义好。
-
没有和属性名对应的setter方法时
当有一个属性app:demo应用于一个CustomView自定义组件时,若是CustomView类中没有定义setDemo方法的话则就无法自动匹配到合适的方法,这个时候就需要BindingMethods注解了(当然也有BindingMethod注解用于单个方法)。
-
@BindingMethods可以放在任何类上。
@BindingMethods(value = [BindingMethod(type = CustomView::class, attribute = "app:age", method = "customColor")]) class CustomDataAdapter {}
-
customColor方法必须在type指定的类上实现,且方法有且只能有一个参数,且参数类型必须和属性定义时指定的一致。
<attr name="age" format="integer" />
class CustomView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : androidx.appcompat.widget.AppCompatTextView(context, attrs) { fun customColor(color: Int) { setTextColor(Color.BLUE) Log.d("MPH", "哈哈哈哈哈哈") } }
-
xml中使用必须用@{}引用。
<com.mph.bpp.data_bind.CustomView ... app:age='@{777}'/>
最后,需要指出的是,因为你无法在原生View里添加方法,因此理论上无法通过这种方式重改View中相关方法的实现,所以BindingMethods注解只能用在自定义View类中。
-
-
类型转换方法
当xml中引用的资源类型和属性本身的类型不一致时,可以通过BindingConversion设置方法来自动进行转换。
<!--app:age的定义类型是integer,这里传递的是String,本来是会报错的,因为下面设置了对应类型的BindingConversion,因此可以安全转换--> <com.mph.bpp.data_bind.CustomView ... app:age='@{"777"}'/>
object CustomDataAdapter { /** * 设置了这个后,xml中使用app:age指定成String后也能通过编译,否则只能接收app:age本来所属的Int类型 * 经测试,BindConversion使用只能在object中的静态方法上,使用class的companion object不能通过编译 */ @JvmStatic @BindingConversion fun string2Int(value: String) = value.toIntOrNull() }
需要注意的是,BindConversion使用只能在object中的静态方法上,使用class的companion object不能通过编译。
-
双向绑定
上面一开始我们介绍了使用三种数据改变可以自动触发UI更新,双向绑定意味着UI更新时其所引用的数据也跟着改变。
xml使用@={variant}语法可以指定双向绑定。
系统内部已经内置了一些属性可以支持双向绑定:
image-20230214170403191
比如:
<CheckBox ... android:checked="@={viewModel.checkState}"/>
//双向绑定不需要设置LifecycleOwner var checkState: Boolean = false set(value) { //双向绑定必须要判断旧值,否则会掉入循环 if (field != value) { field = value Log.d("AAA", "设置成了$field") } }
这源于CompoundButtonBindingAdapter中定义了:
@InverseBindingMethods({ @InverseBindingMethod(type = CompoundButton.class, attribute = "android:checked"), }) public class CompoundButtonBindingAdapter { @BindingAdapter("android:checked") public static void setChecked(CompoundButton view, boolean checked) { if (view.isChecked() != checked) { view.setChecked(checked); } } ... }
View根据数据更新UI需要设置BindingAdapter,而根据UI变化更新数据则需要InverseBindingMethod,这里没有定义method属性是因为CompoundButton中有对应的getter方法,是不是很熟悉,就好像BindingAdapter对应着setter方法一样,之所以要定义BindingAdapter不是因为CompoundButton中没有setChecked方法,而是因为这里要加一个旧值判断,否则会循环双向设置,同样数据那边的setter方法也需要判断,就像上面的checkState的set方法里的做法。
所以我们也就知道了如何定义自定义的双向绑定属性,就是定义一组BindingAdapter和InverseBindingAdapter,然后在BindingAdapter的方法中加入旧值判断,在引用的数据的set方法中加入旧值判断即可。
网友评论