ViewBinding 與 Kotlin 委託雙劍合璧
請點贊關注,你的支援對我意義重大。
🔥 Hi,我是小彭。本文已收錄到 GitHub · Android-NoteBook 中。這裡有 Android 進階成長知識體系,有志同道合的朋友,關注公眾號 [彭旭銳] 帶你建立核心競爭力。
前言
大家好,我是小彭。
過去兩年,我們在掘金平台上發表過一些文章,小彭也收到了大家的意見和鼓勵。最近,我會陸續搬運到公眾號上。
ViewBinding 是 Android Gradle Plugin 3.6 中新增的特性,用於更加輕量地實現視圖綁定(即視圖與變數的綁定),可以理解為輕量版本的 DataBinding。 在這篇文章里,我將總結 ViewBinding 使用方法 & 原理,示常式序 AndroidFamilyDemo · KotlinDelegate 有用請記得給 Star ,給小彭一點創作的動力。
前置知識:
- Kotlin | 委託機制 & 原理 & 應用
- Kotlin | 擴展函數(終於知道為什麼 with 用 this,let 用 it)
- Java | 關於泛型能問的都在這裡了(含Kotlin)
- Android | Fragment 核心原理 & 面試題 (AndroidX 版本)
學習路線圖
1. 認識 ViewBinding
1.1 ViewBinding 用於解決什麼問題?
ViewBinding 是 Android Gradle Plugin 3.6 中新增的特性,用於更加輕量地實現視圖綁定(即視圖與變數的綁定),可以理解為輕量版本的 DataBinding。
1.2 ViewBinding 與其他視圖綁定方案對比
在 ViewBinding 之前,業界已經有過幾種視圖綁定方案了,想必你也用過。那麼,ViewBinding 作為後起之秀就一定比前者香嗎?我從多個維度對比它們的區別:
角度 | findViewById | ButterKnife | Kotlin Synthetics | DataBinding | ViewBinding | ❓ |
---|---|---|---|---|---|---|
簡潔性 | ✖ | ✖ | ✔ | ✔ | ✔ | ❓ |
編譯期檢查 | ✖ | ✖ | ✖ | ✔ | ✔ | ❓ |
編譯速度 | ✔ | ✖ | ✔ | ✖ | ✔ | ❓ |
支援 Kotlin & Java | ✔ | ✔ | ✖ | ✔ | ✔ | ❓ |
收斂模板程式碼 | ✖ | ✖ | ✔ | ✖ | ✖ | ❓ |
- 1、簡潔性: findViewById 和 ButterKnife 需要在程式碼中聲明很多變數,其他幾種方案程式碼簡潔讀較好;
- 2、編譯檢查: 編譯期間主要有兩個方面的檢查:類型檢查 + 只能訪問當前布局中的 id。findViewById、ButterKnife 和 Kotlin Synthetics 在這方面表現較差;
- 3、編譯速度: findViewById 的編譯速度是最快的,而 ButterKnife 和 DataBinding 中存在註解處理,編譯速度略遜色於 Kotlin Synthetics 和 ViewBinding;
- 4、支援 Kotlin & Java: Kotlin Synthetics 只支援 Kotlin 語言;
- 5、收斂模板程式碼: 基本上每種方案都帶有一定量的模板程式碼,只有 Kotlin Synthetics 的模板程式碼是較少的。
可以看到,並沒有一種絕對優勢的方法,但越往後整體的效果是有提升的。另外,❓是什麼呢?
1.3 ViewBinding 的實現原理
AGP 插件會為每個 XML 布局文件創建一個綁定類文件 xxxBinding
,綁定類中會持有布局文件中所有帶 android:id
屬性的 View 引用。例如,有布局文件為 fragment_test.xml
,則插件會生成綁定類 FragmentTestBinding.java
。
那麼,所有 XML 布局文件都生成 Java 類,會不會導致包體積瞬間增大?不會的, 未使用的類會在混淆時被壓縮。
2. ViewBinding 的基本用法
這一節我們來介紹 ViewBinding 的使用方法,內容不多。
提示: ViewBinding 要求在 Android Gradle Plugin 版本在至少在 3.6 以上。
2.1 添加配置
視圖綁定功能按模組級別啟用,啟用的模組需要在模組級 build.gralde 中添加配置。例如:
build.gradle
android {
...
viewBinding {
enabled = true
}
}
對於不需要生成綁定類的布局文件,可以在根節點聲明 tools:viewBindingIgnore="true"
。例如:
<LinearLayout
...
tools:viewBindingIgnore="true" >
...
</LinearLayout>
2.2 視圖綁定
綁定類中提供了 3 個視圖綁定 API:
// 綁定到視圖 view 上
fun <T> bind(view : View) : T
// 使用 inflater 解析布局,再綁定到 View 上
fun <T> inflate(inflater : LayoutInflater) : T
// 使用 inflater 解析布局,再綁定到 View 上
fun <T> inflate(inflater : LayoutInflater, parent : ViewGroup?, attachToParent : Boolean) : T
- 1、在 Activity 中使用
MainActivity.kt
class TestActivity: AppCompatActivity(R.layout.activity_test) {
private lateinit var binding: ActivityTestBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityTestBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.tvDisplay.text = "Hello World."
}
}
- 2、在 Fragment 中使用
TestFragment.kt
class TestFragment : Fragment(R.layout.fragment_test) {
private var _binding: FragmentTestBinding? = null
private val binding get() = _binding!!
override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
_binding = FragmentTestBinding.bind(root)
binding.tvDisplay.text = "Hello World."
}
override fun onDestroyView() {
super.onDestroyView()
// 置空
_binding = null
}
}
2.3 避免記憶體泄露
這裡有一個隱藏的記憶體泄露問題,你需要理解清楚(嚴格來說這並不是 ViewBinding 的問題,即使你採用其它視圖綁定方案也要考慮這個問題)。
問題:為什麼 Fragment#onDestroyView() 里需要置空綁定類對象,而 Activity 里不需要?
答:Activity 實例和 Activity 視圖的生命周期是同步的,而 Fragment 實例和 Fragment 視圖的生命周期並不是完全同步的,因此需要在 Fragment 視圖銷毀時,手動回收綁定類對象,否則造成記憶體泄露。例如:detach Fragment,或者 remove Fragment 並且事務進入返回棧,此時 Fragment 視圖銷毀但 Fragment 實例存在。關於 Fragment 生命周期和事務在我之前的一篇文章里討論過:Android | Fragment 核心原理 & 面試題 (AndroidX 版本)
總之,在視圖銷毀但是控制類對象實例還存活的時機,你就需要手動回收綁定類對象,否則造成記憶體泄露。
2.4 ViewBinding 綁定類源碼
反編譯如下:
ActivityTestBinding.java
public final class ActivityTestBinding implements ViewBinding {
private final ConstraintLayout rootView;
public final TextView tvDisplay;
private ActivityTestBinding (ConstraintLayout paramConstraintLayout1, TextView paramTextView)
this.rootView = paramConstraintLayout1;
this.tvDisplay = paramTextView;
}
public static ActivityTestBinding bind(View paramView) {
TextView localTextView = (TextView)paramView.findViewById(2131165363);
if (localTextView != null) {
return new ActivityMainBinding((ConstraintLayout)paramView, localTextView);
}else {
paramView = "tvDisplay";
}
throw new NullPointerException("Missing required view with ID: ".concat(paramView));
}
public static ActivityMainBinding inflate(LayoutInflater paramLayoutInflater) {
return inflate(paramLayoutInflater, null, false);
}
public static ActivityMainBinding inflate(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup, boolean paramBoolean) {
paramLayoutInflater = paramLayoutInflater.inflate(2131361821, paramViewGroup, false);
if (paramBoolean) {
paramViewGroup.addView(paramLayoutInflater);
}
return bind(paramLayoutInflater);
}
public ConstraintLayout getRoot() {
return this.rootView;
}
}
3. ViewBinding 與 Kotlin 委託雙劍合璧
到這裡,ViewBinding 的使用教程已經說完了。但是回過頭看,有沒有發現一些局限性呢?
- 1、創建和回收 ViewBinding 對象需要重複編寫樣板程式碼,特別是在 Fragment 中使用的案例;
- 2、binding 屬性是可空的,也是可變的,使用起來不方便。
那麼,有沒有可優化的方案呢?我們想起了 Kotlin 屬性委託,關於 Kotlin 委託機制在我之前的一篇文章里討論過:Kotlin | 委託機制 & 原理。如果你還不太了解 Kotlin 委託,下面的內容對你會有些難度。下面,我將帶你一步步封裝 ViewBinding 屬性委託工具。首先,我們梳理一下我們要委託的內容與需求,以及相應的解決辦法:
需求 | 解決辦法 |
---|---|
需要委託 ViewBinding#bind() 的調用 | 反射 |
需要委託 binding = null 的調用 | 監聽 Fragment 視圖生命周期 |
期望 binding 屬性聲明為非空不可變變數 | ReadOnlyProperty<F, V> |
3.1 ViewBinding + Kotlin 委託 1.0
我們現在較複雜的 Fragment 中嘗試使用 Kotlin 委託優化:
FragmentViewBindingPropertyV1.kt
private const val TAG = "ViewBindingProperty"
public inline fun <reified V : ViewBinding> viewBindingV1() = viewBindingV1(V::class.java)
public inline fun <reified T : ViewBinding> viewBindingV1(clazz: Class<T>): FragmentViewBindingPropertyV1<Fragment, T> {
val bindMethod = clazz.getMethod("bind", View::class.java)
return FragmentViewBindingPropertyV1 {
bindMethod(null, it.requireView()) as T
}
}
/**
* @param viewBinder 創建綁定類對象
*/
class FragmentViewBindingPropertyV1<in F : Fragment, out V : ViewBinding>(
private val viewBinder: (F) -> V
) : ReadOnlyProperty<F, V> {
private var viewBinding: V? = null
@MainThread
override fun getValue(thisRef: F, property: KProperty<*>): V {
// 已經綁定,直接返回
viewBinding?.let { return it }
// Use viewLifecycleOwner.lifecycle other than lifecycle
val lifecycle = thisRef.viewLifecycleOwner.lifecycle
val viewBinding = viewBinder(thisRef)
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
Log.w(
TAG, "Access to viewBinding after Lifecycle is destroyed or hasn't created yet. " +
"The instance of viewBinding will be not cached."
)
// We can access to ViewBinding after Fragment.onDestroyView(), but don't save it to prevent memory leak
} else {
lifecycle.addObserver(ClearOnDestroyLifecycleObserver())
this.viewBinding = viewBinding
}
return viewBinding
}
@MainThread
fun clear() {
viewBinding = null
}
private inner class ClearOnDestroyLifecycleObserver : LifecycleObserver {
private val mainHandler = Handler(Looper.getMainLooper())
@MainThread
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy(owner: LifecycleOwner) {
owner.lifecycle.removeObserver(this)
mainHandler.post { clear() }
}
}
}
使用示例:
class TestFragment : Fragment(R.layout.fragment_test) {
private val binding : FragmentTestBinding by viewBindingV1()
override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
binding.tvDisplay.text = "Hello World."
}
}
乾淨清爽!前面提出的三個需求也都實現了,現在我為你解答細節:
- 問題 1、為什麼可以使用 V::class.java,不是泛型擦除了嗎? 利用了 Kotlin 內斂函數 + 實化類型參數,編譯後函數體整體被複制到調用處,V::class.java 其實是 FragmentTestBinding::class.java。具體分析見:Java | 關於泛型能問的都在這裡了(含Kotlin)
- 問題 2、ReadOnlyProperty<F, V> 是什麼? ReadOnlyProperty 是不可變屬性代理,通過 getValue(…) 方法實現委託行為。第一個類型參數 F 是屬性所有者,第二個參數 V 是屬性類型,因為我們在 Fragment 中定義屬性,屬性類型為 ViewBinding,所謂定義類型參數為 <in F : Fragment, out V : ViewBinding>;
- 問題 3、解釋下 getValue(…) 方法? 直接看注釋:
FragmentViewBindingPropertyV1.kt
@MainThread
override fun getValue(thisRef: F, property: KProperty<*>): V {
// 1、viewBinding 不為空說明已經綁定,直接返回
viewBinding?.let { return it }
// 2、Fragment 視圖的生命周期
val lifecycle = thisRef.viewLifecycleOwner.lifecycle
// 3、實例化綁定類對象
val viewBinding = viewBinder(thisRef)
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
// 4.1 如果視圖生命周期為 DESTROYED,說明視圖被銷毀,此時不快取綁定類對象(避免記憶體泄漏)
} else {
// 4.2 定義視圖生命周期監聽者
lifecycle.addObserver(ClearOnDestroyLifecycleObserver())
// 4.3 快取綁定類對象
this.viewBinding = viewBinding
}
return viewBinding
}
- 問題 4、為什麼 onDestroy() 要採用 Handler#post(Message) 完成? 因為 Fragment#viewLifecycleOwner 通知生命周期事件 ON_DESTROY 的時機在 Fragment#onDestroyView 之前。如果不使用 post 的方式,那麼業務方要是在 onDestroyView 中訪問了 binding,則會二次執行 getValue() 這是不必要的。
3.2 ViewBinding + Kotlin 委託 2.0
V1.0 版本使用了反射,真的一定要反射嗎?反射調用 bind 函數的目的就是獲得一個 ViewBinding 綁定類對象,或許我們可以試試把創建對象的行為交給外部去定義,類似這樣用一個 lambda 表達式實現工廠函數:
FragmentViewBindingPropertyV2.kt
inline fun <F : Fragment, V : ViewBinding> viewBindingV2(
crossinline viewBinder: (View) -> V,
// 類似於創建工廠
crossinline viewProvider: (F) -> View = Fragment::requireView
) = FragmentViewBindingPropertyV2 { fragment: F ->
viewBinder(viewProvider(fragment))
}
class FragmentViewBindingPropertyV2<in F : Fragment, out V : ViewBinding>(
private val viewBinder: (F) -> V
) : ReadOnlyProperty<F, V> {
// 以下源碼相同 ...
}
使用示例:
class TestFragment : Fragment(R.layout.fragment_test) {
private val binding by viewBindingV2(FragmentTestBinding::bind)
override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
binding.tvDisplay.text = "Hello World."
}
}
乾淨清爽!不使用反射也可以實現,現在我為你解答細節:
- 問題 5、(View) -> V 是什麼? Kotlin 高階函數,可以把 lambda 表達式直接作為參數傳遞,其中 View 是函數參數,而 T 是函數返回值。lambda 表達式本質上是 「可以作為值傳遞的程式碼塊」。在老版本 Java 中,傳遞程式碼塊需要使用匿名內部類實現,而使用 lambda 表達式甚至連函數聲明都不需要,可以直接傳遞程式碼塊作為函數值;
- 問題 6、Fragment::requireView 是什麼? 把函數 requireView() 作為參數傳遞。Fragment#requireView() 會返回 Fragment 的根節點,但要注意在 onCreateView() 之前調用 requireView() 會拋出異常;
- 問題 7、FragmentTestBinding::bind 是什麼? 把函數 bind() 作為參數傳遞,bind 函數的參數為 View,返回值為 ViewBinding,與函數聲明 (View) -> V 匹配。
3.3 ViewBinding + Kotlin 委託最終版
V2.0 版本已經完成了針對 Fragment 的屬性代理,但是實際場景中只會在 Fragment 中使用 ViewBinding 嗎?顯然並不是,我們還有其他一些場景:
- Activity
- Fragment
- DialogFragment
- ViewGroup
- RecyclerView.ViewHolder
所以,我們有必要將委託工具適當封裝得更通用些,完整程式碼和演示工程你可以直接下載查看: AndroidFamilyDemo · KotlinDelegate
ViewBindingProperty.kt
// -------------------------------------------------------
// ViewBindingProperty for Activity
// -------------------------------------------------------
@JvmName("viewBindingActivity")
inline fun <V : ViewBinding> ComponentActivity.viewBinding(
crossinline viewBinder: (View) -> V,
crossinline viewProvider: (ComponentActivity) -> View = ::findRootView
): ViewBindingProperty<ComponentActivity, V> = ActivityViewBindingProperty { activity: ComponentActivity ->
viewBinder(viewProvider(activity))
}
@JvmName("viewBindingActivity")
inline fun <V : ViewBinding> ComponentActivity.viewBinding(
crossinline viewBinder: (View) -> V,
@IdRes viewBindingRootId: Int
): ViewBindingProperty<ComponentActivity, V> = ActivityViewBindingProperty { activity: ComponentActivity ->
viewBinder(activity.requireViewByIdCompat(viewBindingRootId))
}
// -------------------------------------------------------
// ViewBindingProperty for Fragment / DialogFragment
// -------------------------------------------------------
@Suppress("UNCHECKED_CAST")
@JvmName("viewBindingFragment")
inline fun <F : Fragment, V : ViewBinding> Fragment.viewBinding(
crossinline viewBinder: (View) -> V,
crossinline viewProvider: (F) -> View = Fragment::requireView
): ViewBindingProperty<F, V> = when (this) {
is DialogFragment -> DialogFragmentViewBindingProperty { fragment: F ->
viewBinder(viewProvider(fragment))
} as ViewBindingProperty<F, V>
else -> FragmentViewBindingProperty { fragment: F ->
viewBinder(viewProvider(fragment))
}
}
@Suppress("UNCHECKED_CAST")
@JvmName("viewBindingFragment")
inline fun <F : Fragment, V : ViewBinding> Fragment.viewBinding(
crossinline viewBinder: (View) -> V,
@IdRes viewBindingRootId: Int
): ViewBindingProperty<F, V> = when (this) {
is DialogFragment -> viewBinding(viewBinder) { fragment: DialogFragment ->
fragment.getRootView(viewBindingRootId)
} as ViewBindingProperty<F, V>
else -> viewBinding(viewBinder) { fragment: F ->
fragment.requireView().requireViewByIdCompat(viewBindingRootId)
}
}
// -------------------------------------------------------
// ViewBindingProperty for ViewGroup
// -------------------------------------------------------
@JvmName("viewBindingViewGroup")
inline fun <V : ViewBinding> ViewGroup.viewBinding(
crossinline viewBinder: (View) -> V,
crossinline viewProvider: (ViewGroup) -> View = { this }
): ViewBindingProperty<ViewGroup, V> = LazyViewBindingProperty { viewGroup: ViewGroup ->
viewBinder(viewProvider(viewGroup))
}
@JvmName("viewBindingViewGroup")
inline fun <V : ViewBinding> ViewGroup.viewBinding(
crossinline viewBinder: (View) -> V,
@IdRes viewBindingRootId: Int
): ViewBindingProperty<ViewGroup, V> = LazyViewBindingProperty { viewGroup: ViewGroup ->
viewBinder(viewGroup.requireViewByIdCompat(viewBindingRootId))
}
// -------------------------------------------------------
// ViewBindingProperty for RecyclerView#ViewHolder
// -------------------------------------------------------
@JvmName("viewBindingViewHolder")
inline fun <V : ViewBinding> RecyclerView.ViewHolder.viewBinding(
crossinline viewBinder: (View) -> V,
crossinline viewProvider: (RecyclerView.ViewHolder) -> View = RecyclerView.ViewHolder::itemView
): ViewBindingProperty<RecyclerView.ViewHolder, V> = LazyViewBindingProperty { holder: RecyclerView.ViewHolder ->
viewBinder(viewProvider(holder))
}
@JvmName("viewBindingViewHolder")
inline fun <V : ViewBinding> RecyclerView.ViewHolder.viewBinding(
crossinline viewBinder: (View) -> V,
@IdRes viewBindingRootId: Int
): ViewBindingProperty<RecyclerView.ViewHolder, V> = LazyViewBindingProperty { holder: RecyclerView.ViewHolder ->
viewBinder(holder.itemView.requireViewByIdCompat(viewBindingRootId))
}
// -------------------------------------------------------
// ViewBindingProperty
// -------------------------------------------------------
private const val TAG = "ViewBindingProperty"
interface ViewBindingProperty<in R : Any, out V : ViewBinding> : ReadOnlyProperty<R, V> {
@MainThread
fun clear()
}
class LazyViewBindingProperty<in R : Any, out V : ViewBinding>(
private val viewBinder: (R) -> V
) : ViewBindingProperty<R, V> {
private var viewBinding: V? = null
@Suppress("UNCHECKED_CAST")
@MainThread
override fun getValue(thisRef: R, property: KProperty<*>): V {
// Already bound
viewBinding?.let { return it }
return viewBinder(thisRef).also {
this.viewBinding = it
}
}
@MainThread
override fun clear() {
viewBinding = null
}
}
abstract class LifecycleViewBindingProperty<in R : Any, out V : ViewBinding>(
private val viewBinder: (R) -> V
) : ViewBindingProperty<R, V> {
private var viewBinding: V? = null
protected abstract fun getLifecycleOwner(thisRef: R): LifecycleOwner
@MainThread
override fun getValue(thisRef: R, property: KProperty<*>): V {
// Already bound
viewBinding?.let { return it }
val lifecycle = getLifecycleOwner(thisRef).lifecycle
val viewBinding = viewBinder(thisRef)
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
Log.w(
TAG, "Access to viewBinding after Lifecycle is destroyed or hasn't created yet. " +
"The instance of viewBinding will be not cached."
)
// We can access to ViewBinding after Fragment.onDestroyView(), but don't save it to prevent memory leak
} else {
lifecycle.addObserver(ClearOnDestroyLifecycleObserver(this))
this.viewBinding = viewBinding
}
return viewBinding
}
@MainThread
override fun clear() {
viewBinding = null
}
private class ClearOnDestroyLifecycleObserver(
private val property: LifecycleViewBindingProperty<*, *>
) : LifecycleObserver {
private companion object {
private val mainHandler = Handler(Looper.getMainLooper())
}
@MainThread
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy(owner: LifecycleOwner) {
mainHandler.post { property.clear() }
}
}
}
class FragmentViewBindingProperty<in F : Fragment, out V : ViewBinding>(
viewBinder: (F) -> V
) : LifecycleViewBindingProperty<F, V>(viewBinder) {
override fun getLifecycleOwner(thisRef: F): LifecycleOwner {
try {
return thisRef.viewLifecycleOwner
} catch (ignored: IllegalStateException) {
error("Fragment doesn't have view associated with it or the view has been destroyed")
}
}
}
class DialogFragmentViewBindingProperty<in F : DialogFragment, out V : ViewBinding>(
viewBinder: (F) -> V
) : LifecycleViewBindingProperty<F, V>(viewBinder) {
override fun getLifecycleOwner(thisRef: F): LifecycleOwner {
return if (thisRef.showsDialog) {
thisRef
} else {
try {
thisRef.viewLifecycleOwner
} catch (ignored: IllegalStateException) {
error("Fragment doesn't have view associated with it or the view has been destroyed")
}
}
}
}
// -------------------------------------------------------
// Utils
// -------------------------------------------------------
@RestrictTo(RestrictTo.Scope.LIBRARY)
class ActivityViewBindingProperty<in A : ComponentActivity, out V : ViewBinding>(
viewBinder: (A) -> V
) : LifecycleViewBindingProperty<A, V>(viewBinder) {
override fun getLifecycleOwner(thisRef: A): LifecycleOwner {
return thisRef
}
}
fun <V : View> View.requireViewByIdCompat(@IdRes id: Int): V {
return ViewCompat.requireViewById(this, id)
}
fun <V : View> Activity.requireViewByIdCompat(@IdRes id: Int): V {
return ActivityCompat.requireViewById(this, id)
}
/**
* Utility to find root view for ViewBinding in Activity
*/
fun findRootView(activity: Activity): View {
val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
checkNotNull(contentView) { "Activity has no content view" }
return when (contentView.childCount) {
1 -> contentView.getChildAt(0)
0 -> error("Content view has no children. Provide root view explicitly")
else -> error("More than one child view found in Activity content view")
}
}
fun DialogFragment.getRootView(viewBindingRootId: Int): View {
val dialog = checkNotNull(dialog) {
"DialogFragment doesn't have dialog. Use viewBinding delegate after onCreateDialog"
}
val window = checkNotNull(dialog.window) { "Fragment's Dialog has no window" }
return with(window.decorView) {
if (viewBindingRootId != 0) requireViewByIdCompat(
viewBindingRootId
) else this
}
}
4. 總結
ViewBinding 是一個輕量級的視圖綁定方案,Android Gradle 插件會為每個 XML 布局文件創建一個綁定類。在 Fragment 中使用 ViewBinding 需要注意在 Fragment#onDestroyView() 里置空綁定類對象避免記憶體泄漏。但這會帶來很多重複編寫樣板程式碼,使用屬性委託可以收斂模板程式碼,保證調用方程式碼乾淨清爽。
角度 | findViewById | ButterKnife | Kotlin Synthetics | DataBinding | ViewBinding | ViewBindingProperty |
---|---|---|---|---|---|---|
簡潔性 | ✖ | ✖ | ✔ | ✔ | ✔ | ✔ |
編譯期檢查 | ✖ | ✖ | ✖ | ✔ | ✔ | ✔ |
編譯速度 | ✔ | ✖ | ✔ | ✖ | ✔ | ✔ |
支援 Kotlin & Java | ✔ | ✔ | ✖ | ✔ | ✔ | ✔ |
收斂模板程式碼 | ✖ | ✖ | ✔ | ✖ | ✖ | ✔ |
參考資料
- View Binding 視圖綁定 —— 官方文檔
- View Binding 與 Kotlin 委託屬性的巧妙結合,告別垃圾程式碼! —— Kirill Rozov 著,依然范特稀西 譯
- 誰才是 ButterKnife 的終結者? —— fundroid 著
- 深入研究 ViewBinding 在 include, merge, adapter, fragment, activity 中使用 —— Flywith24 著