Simplify and reduce overhead of lazy view binding in Fragments (#4269)

This reduces complexity of view binding inflation in Fragments, and also
reduces overhead (no `KProperty` objects need to be generated by the
compiler) by implementing `Lazy` instead of `ReadOnlyProperty`.

For a full explanation, see this [detailed blog
post](https://medium.com/@bladecoder/viewlifecyclelazy-and-other-ways-to-avoid-view-memory-leaks-in-android-fragments-4aa982e6e579).
This commit is contained in:
Christophe Beyls 2024-02-23 20:10:33 +01:00 committed by GitHub
parent 7e5eef4060
commit a19540f0e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 24 additions and 49 deletions

View File

@ -4,16 +4,14 @@ import android.view.LayoutInflater
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.viewbinding.ViewBinding
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
/**
* https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c
* Original code: https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c
* Refactor: https://bladecoder.medium.com/viewlifecyclelazy-and-other-ways-to-avoid-view-memory-leaks-in-android-fragments-4aa982e6e579
*/
inline fun <T : ViewBinding> AppCompatActivity.viewBinding(
@ -22,55 +20,32 @@ inline fun <T : ViewBinding> AppCompatActivity.viewBinding(
bindingInflater(layoutInflater)
}
class FragmentViewBindingDelegate<T : ViewBinding>(
val fragment: Fragment,
val viewBindingFactory: (View) -> T
) : ReadOnlyProperty<Fragment, T> {
private var binding: T? = null
private class ViewLifecycleLazy<out T : Any>(
private val fragment: Fragment,
private val initializer: (View) -> T
) : Lazy<T>, LifecycleEventObserver {
private var cached: T? = null
init {
fragment.lifecycle.addObserver(object : DefaultLifecycleObserver {
val viewLifecycleOwnerLiveDataObserver =
Observer<LifecycleOwner?> {
val viewLifecycleOwner = it ?: return@Observer
viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
binding = null
}
})
}
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observeForever(
viewLifecycleOwnerLiveDataObserver
)
override val value: T
get() {
return cached ?: run {
val newValue = initializer(fragment.requireView())
cached = newValue
fragment.viewLifecycleOwner.lifecycle.addObserver(this)
newValue
}
override fun onDestroy(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.removeObserver(
viewLifecycleOwnerLiveDataObserver
)
}
})
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
val binding = binding
if (binding != null) {
return binding
}
val lifecycle = fragment.viewLifecycleOwner.lifecycle
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
throw IllegalStateException(
"Should not attempt to get bindings when Fragment views are destroyed."
)
}
override fun isInitialized() = cached != null
return viewBindingFactory(thisRef.requireView()).also { this.binding = it }
override fun toString() = cached.toString()
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
cached = null
}
}
}
fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
FragmentViewBindingDelegate(this, viewBindingFactory)
fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T): Lazy<T> =
ViewLifecycleLazy(this, viewBindingFactory)