美文网首页Android深入
DataBinding——使用Kotlin 委托优化

DataBinding——使用Kotlin 委托优化

作者: 我爱田Hebe | 来源:发表于2023-01-29 14:39 被阅读0次

    简介

    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的生命周期和FragmentView的生命周期是不同步的;而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()
    

    这种方式通过在viewLifecycleOwneronDestroy()时使用主线程Handler.post将binding置空的任务添加到消息队列中,而viewLifecycleOwneronDestroy()和Fragment的onDestroyView()方法是在同一个消息中被处理的:

    performDestroyView()中:

    因此,我们post的Runnable自然会在onDestroyView()之后

    相比方式二,方式一的生命周期回调会得更稳定。

    拓展

    作者:ak
    链接:https://juejin.cn/post/7194024942650785852

    相关文章

      网友评论

        本文标题:DataBinding——使用Kotlin 委托优化

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