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的生命周期和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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,302评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,232评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,337评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,977评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,920评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,194评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,638评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,319评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,455评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,379评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,426评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,106评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,696评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,786评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,996评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,467评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,043评论 2 341

推荐阅读更多精彩内容