DataBinding——使用Kotlin 委托優(yōu)化

簡(jiǎn)介

DataBinding 是 Google 在 Jetpack 中推出的一款數(shù)據(jù)綁定的支持庫(kù)变骡,利用該庫(kù)可以實(shí)現(xiàn)在頁(yè)面組件中直接綁定應(yīng)用程序的數(shù)據(jù)源比原。使其維護(hù)起來(lái)更加方便讨惩,架構(gòu)更明確簡(jiǎn)潔睁壁。

啟用DataBinding

DataBinding庫(kù)與 Android Gradle 插件捆綁在一起为狸。無(wú)需聲明對(duì)此庫(kù)的依賴項(xiàng)歼郭,但必須啟用它。

android {
    ...
    buildFeatures {
        dataBinding true
    }
}

基本使用 DataBinding—官方文檔

常規(guī)用法

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中使用病曾,我們直接通過(guò)inflate(@NonNull LayoutInflater inflater)創(chuàng)建binding對(duì)象,然后通過(guò)setContentView(View view)把根部局(binding.root)設(shè)置進(jìn)去

或者我們可以通過(guò)懶加載的方式

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"
    }

}

我們通過(guò)by lazy{},在首次訪問(wèn)的時(shí)候會(huì)調(diào)用lazy中的代碼塊進(jìn)行初始化漾根;這里我們會(huì)發(fā)現(xiàn)泰涂,在onCreate()中,我們并沒(méi)有調(diào)用setContentView()設(shè)置布局辐怕;這是因?yàn)槲覀冊(cè)谑状卧L問(wèn)binding的時(shí)候逼蒙,會(huì)執(zhí)行l(wèi)azy中的DataBindingUtil.setContentView(),其中就調(diào)用了activity.setContentView()并創(chuàng)建binding對(duì)象返回寄疏;由于我們首次訪問(wèn)是在onCreate()中是牢,自然就會(huì)在此處設(shè)置布局了。

2陕截、在Fragment中使用

注意內(nèi)存泄漏:

在Activity中使無(wú)需考慮此問(wèn)題

在Fragment中使用時(shí)需要注意在onDestroyView()的時(shí)候把binding對(duì)象置空驳棱,因?yàn)?strong>Fragment的生命周期和FragmentView的生命周期是不同步的;而binding綁定的是視圖农曲,當(dāng)視圖被銷毀時(shí)社搅,binding就不應(yīng)該再被訪問(wèn)且能夠被回收,因此乳规,我們需要在onDestroyView()中將binding對(duì)象置空罚渐; 否則,當(dāng)視圖被銷毀時(shí)驯妄,F(xiàn)ragment繼續(xù)持有binding的引用荷并,就會(huì)導(dǎo)致binding無(wú)法被回收,造成內(nèi)存泄漏青扔。

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版中使用了兩個(gè)binding對(duì)象源织?

因?yàn)樵贙otlin語(yǔ)言的特性中

  • 當(dāng)某個(gè)變量的值可以為 null 的時(shí)候翩伪,必須在聲明處的類型后添加 ? 來(lái)標(biāo)識(shí)該引用可為空。
  • 可重新賦值的變量使用 var 關(guān)鍵字

因此我們需要將Binding對(duì)象聲明為可變的且可為空的谈息;又因?yàn)樵贙otlin中有null 檢測(cè)缘屹,會(huì)導(dǎo)致我們每次使用時(shí)都需要判空或使用安全調(diào)用操作符?. 這樣又會(huì)造成代碼可讀性較差、不必要的判空侠仇、不夠優(yōu)雅轻姿,用起來(lái)也麻煩。

然后這里就引出了我們的第二個(gè)對(duì)象逻炊,使用Kotlin的非空斷言運(yùn)算符將它轉(zhuǎn)為非空類型來(lái)使用互亮。

非空斷言運(yùn)算符(!!)將任何值轉(zhuǎn)換為非空類型,若該值為空則拋出異常

即解決了判空問(wèn)題余素,又可以將binding對(duì)象用val聲明為不可變的豹休。

使用Kotlin屬性委托來(lái)優(yōu)化

像上文中創(chuàng)建和銷毀binding對(duì)象,如果每次使用都要寫一遍這樣的模板代碼桨吊,就會(huì)變得很繁瑣威根,我們通知將之封裝到Activity / Fragment的基類(Base)中,在對(duì)應(yīng)的生命周期中創(chuàng)建或銷毀视乐;但是會(huì)依賴于基類洛搀,往往項(xiàng)目中基類做的事情太多了;如果我們只是需要這個(gè)binding佑淀,就會(huì)繼承到一些不需要的功能姥卢。

像這樣的情況我們希望將它進(jìn)一步優(yōu)化,將之解耦出來(lái)作為一個(gè)頁(yè)面的組件存在渣聚,可以理解為做成一個(gè)支持熱插拔的組件独榴,這里就需要用到委托來(lái)實(shí)現(xiàn)。

關(guān)于Kotlin委托機(jī)制請(qǐng)看:委托屬性 - Kotlin 語(yǔ)言中文站 (kotlincn.net)

1奕枝、Activity中的委托

ContentViewBindingDelegate.kt

/**
 * 懶加載DataBinding的委托棺榔,
 * 調(diào)用 [Activity.setContentView],設(shè)置[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拓展函數(shù)來(lái)使用
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通過(guò)by關(guān)鍵字委托給了其中定義的Activity的拓展函數(shù)contentView(),此函數(shù)返回我們的委托類ContentViewBindingDelegate,每次訪問(wèn)binding時(shí)谭梗,會(huì)執(zhí)行委托類中的getValue()忘晤;當(dāng)我們?cè)?code>onCreate()中首次訪問(wèn)時(shí),委托中的binding為空激捏,會(huì)去創(chuàng)建binding對(duì)象设塔,并調(diào)用了Activity.setContentView();此后每次訪問(wèn),binding不再為空远舅,直接返回了binding闰蛔。

2痕钢、Fragment中的委托

避坑:Fragment的viewLifecycleOwner 會(huì)在 Fragment的onDestroyView() 之前執(zhí)行onDestroy()

也就是說(shuō)如果我這樣寫:

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 {
            //會(huì)在Fragment的`onDestroyView()` 之前執(zhí)行
            override fun onDestroy(owner: LifecycleOwner) {  
                binding = null
            }
        })
        return binding!!
    }

}

那么binding會(huì)在Fragment的onDestroyView()之前置空任连,當(dāng)我們onDestroyView()訪問(wèn)了binding,會(huì)再給binding賦值例诀。

因此我們需要實(shí)現(xiàn)在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随抠,設(shè)置生命周期所有者并返回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
    }

}

這種方式通過(guò)注冊(cè)FragmentManager.FragmentLifecycleCallbacks來(lái)監(jiān)聽(tīng)Fragment的生命周期變化繁涂,其中的onFragmentViewDestroyed()會(huì)在Fragment從 FragmentManager 對(duì)Fragment.onDestroyView()的調(diào)用返回之后調(diào)用拱她。

方式二

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,設(shè)置生命周期所有者并返回binding爆土。
 */
fun <F : Fragment, T : ViewDataBinding> Fragment.binding(): FragmentViewBindingDelegate<F, T> =
    FragmentViewBindingDelegate()

這種方式通過(guò)在viewLifecycleOwneronDestroy()時(shí)使用主線程Handler.post將binding置空的任務(wù)添加到消息隊(duì)列中,而viewLifecycleOwneronDestroy()和Fragment的onDestroyView()方法是在同一個(gè)消息中被處理的:

performDestroyView()中:

因此诸蚕,我們post的Runnable自然會(huì)在onDestroyView()之后

相比方式二步势,方式一的生命周期回調(diào)會(huì)得更穩(wěn)定。

拓展

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末背犯,一起剝皮案震驚了整個(gè)濱河市坏瘩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌漠魏,老刑警劉巖倔矾,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異柱锹,居然都是意外死亡哪自,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門禁熏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)壤巷,“玉大人,你說(shuō)我怎么就攤上這事瞧毙‰驶” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵宙彪,是天一觀的道長(zhǎng)矩动。 經(jīng)常有香客問(wèn)我,道長(zhǎng)释漆,這世上最難降的妖魔是什么悲没? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮男图,結(jié)果婚禮上檀训,老公的妹妹穿的比我還像新娘柑潦。我一直安慰自己,他們只是感情好峻凫,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布渗鬼。 她就那樣靜靜地躺著,像睡著了一般荧琼。 火紅的嫁衣襯著肌膚如雪譬胎。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天命锄,我揣著相機(jī)與錄音堰乔,去河邊找鬼。 笑死脐恩,一個(gè)胖子當(dāng)著我的面吹牛镐侯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播驶冒,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼苟翻,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了骗污?” 一聲冷哼從身側(cè)響起崇猫,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎需忿,沒(méi)想到半個(gè)月后诅炉,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡屋厘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年涕烧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片汗洒。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡澈魄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出仲翎,到底是詐尸還是另有隱情痹扇,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布溯香,位于F島的核電站鲫构,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏玫坛。R本人自食惡果不足惜结笨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧炕吸,春花似錦伐憾、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至瀑罗,卻和暖如春胸嘴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背斩祭。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工劣像, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人摧玫。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓耳奕,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親诬像。 傳聞我的和親對(duì)象是個(gè)殘疾皇子屋群,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容