DataBinding——使用Kotlin 委托优化
简介
DataBinding 是 Google 在 Jetpack 中推出的一款数据绑定的支持库,利用该库可以实现在页面组件中直接绑定应用程序的数据源。使其维护起来更加方便,架构更明确简洁。
启用DataBinding
DataBinding库与 Android Gradle 插件捆绑在一起。无需声明对此库的依赖项,但必须启用它。
android {
...
buildFeatures {
dataBinding true
}
}
基本使用 DataBinding—官方文档
常规用法
1、在Activity中使用
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.tvName.text = "ak"
}
}
在Activity中使用,我们直接通过inflate(@NonNull LayoutInflater inflater)
创建binding对象,然后通过setContentView(View view)
把根部局(binding.root)设置进去
或者我们可以通过懒加载的方式
class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by lazy { DataBindingUtil.setContentView(this,R.layout.activity_main) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.tvName.text = "ak"
}
}
我们通过by lazy{}
,在首次访问的时候会调用lazy中的代码块进行初始化;这里我们会发现,在onCreate()
中,我们并没有调用setContentView()
设置布局;这是因为我们在首次访问binding的时候,会执行lazy中的DataBindingUtil.setContentView()
,其中就调用了activity.setContentView()并创建binding对象返回;由于我们首次访问是在onCreate()
中,自然就会在此处设置布局了。
2、在Fragment中使用
注意内存泄漏:
在Activity中使无需考虑此问题
在Fragment中使用时需要注意在onDestroyView()
的时候把binding对象置空,因为Fragment的生命周期和Fragment中View的生命周期是不同步的;而binding绑定的是视图,当视图被销毁时,binding就不应该再被访问且能够被回收,因此,我们需要在onDestroyView()
中将binding对象置空; 否则,当视图被销毁时,Fragment继续持有binding的引用,就会导致binding无法被回收,造成内存泄漏。
Java版
public class BlankFragmentOfJava extends Fragment {
private FragmentBlankBinding binding;
public BlankFragmentOfJava() {
super(R.layout.fragment_blank);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
binding = FragmentBlankBinding.bind(view);
binding.tvName.setText("ak");
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
Kotlin版
class BlankFragment : Fragment(R.layout.fragment_blank) {
private var _binding: FragmentBlankBinding? = null
private val binding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.tvName
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
为什么Kotlin版中使用了两个binding对象?
因为在Kotlin语言的特性中
- 当某个变量的值可以为 null 的时候,必须在声明处的类型后添加
?
来标识该引用可为空。 - 可重新赋值的变量使用
var
关键字
因此我们需要将Binding对象声明为可变的且可为空的;又因为在Kotlin中有null
检测,会导致我们每次使用时都需要判空或使用安全调用操作符?.
这样又会造成代码可读性较差、不必要的判空、不够优雅,用起来也麻烦。
然后这里就引出了我们的第二个对象,使用Kotlin的非空断言运算符将它转为非空类型来使用。
非空断言运算符(
!!
)将任何值转换为非空类型,若该值为空则抛出异常
即解决了判空问题,又可以将binding对象用val
声明为不可变的。
使用Kotlin属性委托来优化
像上文中创建和销毁binding对象,如果每次使用都要写一遍这样的模板代码,就会变得很繁琐,我们通知将之封装到Activity / Fragment的基类(Base)中,在对应的生命周期中创建或销毁;但是会依赖于基类,往往项目中基类做的事情太多了;如果我们只是需要这个binding,就会继承到一些不需要的功能。
像这样的情况我们希望将它进一步优化,将之解耦出来作为一个页面的组件存在,可以理解为做成一个支持热插拔的组件,这里就需要用到委托来实现。
关于Kotlin委托机制请看:委托属性 - Kotlin 语言中文站 (kotlincn.net)
1、Activity中的委托
ContentViewBindingDelegate.kt
/**
* 懒加载DataBinding的委托,
* 调用 [Activity.setContentView],设置[androidx.lifecycle.LifecycleOwner]并返回绑定。
*/
class ContentViewBindingDelegate<in A : AppCompatActivity, out T : ViewDataBinding>(
@LayoutRes private val layoutRes: Int
) {
private var binding: T? = null
operator fun getValue(activity: A, property: KProperty<*>): T {
binding?.let { return it } //不为空,直接返回
binding = DataBindingUtil.setContentView<T>(activity, layoutRes).apply {
lifecycleOwner = activity
}
return binding!!
}
}
//作为Activity拓展函数来使用
fun <A : AppCompatActivity, T : ViewDataBinding> AppCompatActivity.contentView(
@LayoutRes layoutRes: Int
): ContentViewBindingDelegate<A, T> = ContentViewBindingDelegate(layoutRes)
使用示例
class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by contentView(R.layout.activity_main)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.tvName.text = "ak"
}
}
首先我们Activity中的binding通过by
关键字委托给了其中定义的Activity的拓展函数contentView()
,此函数返回我们的委托类ContentViewBindingDelegate
,每次访问binding时,会执行委托类中的getValue()
;当我们在onCreate()
中首次访问时,委托中的binding为空,会去创建binding对象,并调用了Activity.setContentView()
;此后每次访问,binding不再为空,直接返回了binding。
2、Fragment中的委托
避坑:Fragment的viewLifecycleOwner 会在 Fragment的
onDestroyView()
之前执行onDestroy()
。
也就是说如果我这样写:
class FragmentViewBindingDelegate<in R : Fragment, out T : ViewDataBinding> {
private var binding: T? = null
operator fun getValue(fragment: R, property: KProperty<*>): T {
binding?.let { return it } //不为空,直接返回
binding = DataBindingUtil.bind<T>(fragment.requireView())?.also {
it.lifecycleOwner = fragment.viewLifecycleOwner
}
fragment.viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
//会在Fragment的`onDestroyView()` 之前执行
override fun onDestroy(owner: LifecycleOwner) {
binding = null
}
})
return binding!!
}
}
那么binding会在Fragment的onDestroyView()
之前置空,当我们onDestroyView()
访问了binding,会再给binding赋值。
因此我们需要实现在onDestroyView()
之后再将binding置空
方式一(推荐)
class FragmentViewBindingDelegate<in F : Fragment, out T : ViewDataBinding> {
private var binding: T? = null
operator fun getValue(fragment: F, property: KProperty<*>): T {
binding?.let { return it }
fragment.view ?: throw IllegalArgumentException("The fragment view is empty or has been destroyed")
binding = DataBindingUtil.bind<T>(fragment.requireView())?.also {
it.lifecycleOwner = fragment.viewLifecycleOwner
}
fragment.parentFragmentManager.registerFragmentLifecycleCallbacks(Clear(fragment), false)
return binding!!
}
inner class Clear(private val thisRef: F) : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentViewDestroyed(fm: FragmentManager, f: Fragment) {
if (thisRef === f) {
binding = null
fm.unregisterFragmentLifecycleCallbacks(this)
}
}
}
}
/**
* 绑定fragment布局View,设置生命周期所有者并返回binding。
*/
fun <F : Fragment, T : ViewDataBinding> Fragment.binding(): FragmentViewBindingDelegate<F, T> =
FragmentViewBindingDelegate()
使用示例
class BlankFragment : Fragment(R.layout.fragment_blank) {
private val binding: FragmentBlankBinding by binding()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.tvName
}
}
这种方式通过注册FragmentManager.FragmentLifecycleCallbacks
来监听Fragment的生命周期变化,其中的onFragmentViewDestroyed()
会在Fragment从 FragmentManager 对Fragment.onDestroyView()
的调用返回之后调用。
方式二
class FragmentViewBindingDelegate<in F : Fragment, out T : ViewDataBinding>() {
private var binding: T? = null
operator fun getValue(fragment: F, property: KProperty<*>): T {
binding?.let { return it }
fragment.view ?: throw IllegalArgumentException("The fragment view is empty or has been destroyed")
binding = DataBindingUtil.bind<T>(fragment.requireView())?.apply {
lifecycleOwner = fragment.viewLifecycleOwner
}
fragment.viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
private val mainHandler = Handler(Looper.getMainLooper())
override fun onDestroy(owner: LifecycleOwner) {
mainHandler.post { binding = null }
}
})
return binding!!
}
}
/**
* 绑定fragment布局View,设置生命周期所有者并返回binding。
*/
fun <F : Fragment, T : ViewDataBinding> Fragment.binding(): FragmentViewBindingDelegate<F, T> =
FragmentViewBindingDelegate()
这种方式通过在viewLifecycleOwner
的onDestroy()
时使用主线程Handler.post
将binding置空的任务添加到消息队列中,而viewLifecycleOwner
的onDestroy()
和Fragment的onDestroyView()
方法是在同一个消息中被处理的:
在performDestroyView()
中:
因此,我们
post
的Runnable自然会在onDestroyView()
之后
相比方式二,方式一的生命周期回调会得更稳定。