-
-
Save Zhuinden/ea3189198938bd16c03db628e084a4fa to your computer and use it in GitHub Desktop.
// https://github.com/Zhuinden/fragmentviewbindingdelegate-kt | |
import android.view.View | |
import androidx.fragment.app.Fragment | |
import androidx.lifecycle.DefaultLifecycleObserver | |
import androidx.lifecycle.Lifecycle | |
import androidx.lifecycle.LifecycleOwner | |
import androidx.lifecycle.Observer | |
import androidx.viewbinding.ViewBinding | |
import kotlin.properties.ReadOnlyProperty | |
import kotlin.reflect.KProperty | |
class FragmentViewBindingDelegate<T : ViewBinding>( | |
val fragment: Fragment, | |
val viewBindingFactory: (View) -> T | |
) : ReadOnlyProperty<Fragment, T> { | |
private var binding: 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 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.") | |
} | |
return viewBindingFactory(thisRef.requireView()).also { this.binding = it } | |
} | |
} | |
fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) = | |
FragmentViewBindingDelegate(this, viewBindingFactory) |
Why have you removed the
AppCompatActivity.viewBinding
extension function?I've read both your blogs posts "Simple one-liner ViewBinding in Fragments and Activities with Kotlin" and "An update to the FragmentViewBindingDelegate: the bug we’ve inherited from AutoClearedValue" but haven't found any information about this removal
It wasn't there in first place: Zhuinden/fragmentviewbindingdelegate-kt@a7c1c94#diff-8e05f917a7f5d8b22b1f311a7bf9c2a9e1a35a08244356353f9b56a525cdf9b0
Use this:
inline fun <T : ViewBinding> AppCompatActivity.viewBinding(
crossinline bindingInflater: (LayoutInflater) -> T
) =
lazy(LazyThreadSafetyMode.NONE) {
bindingInflater.invoke(layoutInflater)
}
Hello, I hope you are well.
I found a problem that I will explain step by step.
- Create a fragment called CruiseFilterFragment and then add a button to the page to click on it to go to the CruiseResultFragment fragment.
- Create a fragment called CruiseResultFragment. No special work is required in this fragment
3 - Click the button on the CruiseFilterFragment page to go to the next page.
4. On the CruiseResultFragment page, return to the CruiseFilterFragment page.
5 - Click the button again
You will encounter the following error:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.pixelco.hitrav.dev, PID: 13616
java.lang.IllegalStateException: Can't access the Fragment View's LifecycleOwner when getView() is null i.e., before onCreateView() or after onDestroyView()
at androidx.fragment.app.Fragment.getViewLifecycleOwner(Fragment.java:361)
at com.pixelco.hitrav.base.FragmentViewBindingDelegate.getValue(FragmentViewBindingDelegate.kt:49)
at com.pixelco.hitrav.feature.product.cruise.result.CruiseResultFragment.getBinding(CruiseResultFragment.kt)
at com.pixelco.hitrav.feature.product.cruise.result.CruiseResultFragment.access$getBinding$p(CruiseResultFragment.kt:20)
at com.pixelco.hitrav.feature.product.cruise.result.CruiseResultFragment$setUpViews$2.invoke(CruiseResultFragment.kt:39)
at com.pixelco.hitrav.feature.product.cruise.result.CruiseResultFragment$setUpViews$2.invoke(CruiseResultFragment.kt:20)
at androidx.paging.PagingDataDiffer$processPageEventCallback$1.onStateUpdate(PagingDataDiffer.kt:92)
at androidx.paging.PagePresenter.processEvent(PagePresenter.kt:100)
at androidx.paging.PagingDataDiffer$collectFrom$2$invokeSuspend$$inlined$collect$1$lambda$1.invokeSuspend(PagingDataDiffer.kt:184)
at androidx.paging.PagingDataDiffer$collectFrom$2$invokeSuspend$$inlined$collect$1$lambda$1.invoke(PagingDataDiffer.kt)
at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:91)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:165)
at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
at androidx.paging.PagingDataDiffer$collectFrom$2$invokeSuspend$$inlined$collect$1.emit(Collect.kt:133)
at kotlinx.coroutines.flow.FlowKt__ChannelsKt.emitAllImpl$FlowKt__ChannelsKt(Channels.kt:61)
at kotlinx.coroutines.flow.FlowKt__ChannelsKt$emitAllImpl$1.invokeSuspend(Channels.kt)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.EventLoop.processUnconfinedEvent(EventLoop.common.kt:69)
at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:357)
at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:30)
at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable$default(Cancellable.kt:27)
at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:110)
at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:158)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:56)
at kotlinx.coroutines.BuildersKt.launch(Unknown Source)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:49)
at kotlinx.coroutines.BuildersKt.launch$default(Unknown Source)
at com.pixelco.hitrav.feature.product.cruise.result.CruiseResultFragment.observeData(CruiseResultFragment.kt:51)
at com.pixelco.hitrav.base.BaseFragment.onViewCreated(BaseFragment.kt:74)
at androidx.fragment.app.Fragment.performViewCreated(Fragment.java:2987)
at androidx.fragment.app.FragmentStateManager.createView(FragmentStateManager.java:546)
at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:282)
at androidx.fragment.app.FragmentManager.executeOpsTogether(FragmentManager.java:2189)
at androidx.fragment.app.FragmentManager.removeRedundantOperationsAndExecute(FragmentManager.java:2106)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:2002)
at androidx.fragment.app.FragmentManager$5.run(FragmentManager.java:524)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
@VahidGarousi it sounds like you aren't using viewLifecycleOwner.lifecycleScope.launchWhenStarted {
.
I was actually writing this delegate myself and hit onDestroyView
issue.
To access binding there I altered my delegate to use viewLifecycleOwnerLiveData.observeForever
and releasing the binding when it becomes null instead of observing lifecycle events.
Observing forever looks bad but there's no lifecycleOwner to latch onto as both fragments lifecycle and views lifecycle get destroyed before onDestroyView
so they'd invalidate the observer itself. However viewLifecycleOwnerLiveData
is guaranteed to become null
right after onDestroyView
call so I won't worry about it.
I'm using fragment 1.3.5 (so new internal implementation).
I was actually writing this delegate myself and hit onDestroyView issue.
... i think this onDestroyView kind-.of thing could cause a memory leak, i read about that while adding the leakcanary library in an app im working :S
You can see in fragment manager source how fragment view destruction is performed:
performDestroyView
- which is the place where lifecycle becomes destroyed andonDestroyView
is called- fragments view reference is set to
null
viewLifecycleOwnerLiveData
value is set tonull
I'd conclude that its impossible not to trigger the observer (as long as view itself was set) and say that Leakcanary is just wrong in this case as it's probably just hooking into lifecycle while being unaware of additional observer that'll clear the reference right after lifecycle is destroyed.
How about such a getValue
version?
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
return binding ?: run {
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.")
}
viewBindingFactory(thisRef.requireView()).also {
this.binding = it
}
}
}
to get rid of
val binding = binding
if (binding != null) {
return binding
}
I just had an idea while checking how findViewTreeLifecycleOwner
method works - it just tags root view with LifecycleOwner
.
I think that's an interesting alternative for this use as well:
class VBDelegate<T>(private val bindingFactory: (View) -> T) : ReadOnlyProperty<Fragment, T> {
companion object {
private val key = R.id.content
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T =
thisRef.requireView().run {
getTag(key)?.let { it as T } ?: bindingFactory(this).also { setTag(key, it) }
}
}
Now there are no listeners, no potential leaks (binding reference is cleared alongside view itself) and you can access it during onDestroyView
safely.
Only caveat is (if you can call it that) you need to use project specific resource ID for tagging purpose.
Fatal Exception: java.lang.IllegalStateException: Can't access the Fragment View's LifecycleOwner when getView() is null i.e., before onCreateView() or after onDestroyView()
at androidx.fragment.app.Fragment.getViewLifecycleOwner(Fragment.java:361)
at net.example.company.utils.views.FragmentViewBindingDelegate.getValue(FragmentViewBindingDelegate.java:61)
at net.example.company.views.smartdogtrainer.common.FragBlah.getBinding(FragBlah.java:80)
at net.example.company.views.smartdogtrainer.common.FragBlah.visibilityConnectingContainer(FragBlah.java:295)
at net.example.company.views.smartdogtrainer.common.FragBlah.resetVisibility(FragBlah.java:289)
at net.example.company.views.smartdogtrainer.common.FragBlah.showErrorConnection(FragBlah.java:311)
at net.example.company.views.smartdogtrainer.common.FragBlah.performErrorConnection$lambda-8$lambda-7(FragBlah.java:332)
at net.example.company.views.smartdogtrainer.common.FragBlah$$InternalSyntheticLambda$0$deef3459a1d7d120afdfd9de73e823d74825ffa0a9101b2fc6484805f55a8632$0.run$bridge(FragBlah.java:13)
at android.app.Activity.runOnUiThread(Activity.java:7154)
at net.example.company.views.smartdogtrainer.common.FragBlah.performErrorConnection(FragBlah.java:331)
at net.example.company.views.smartdogtrainer.common.FragBlah.access$performErrorConnection(FragBlah.java:63)
at net.example.company.views.smartdogtrainer.common.FragBlah$sdtCollarScannerCallback$1.onStopScanning(FragBlah.java:120)
at net.example.company.scanner.BleScanner.stopScanningForDevices(BleScanner.java:128)
at net.example.company.scanner.BleScanner.durationTimeoutScanning(BleScanner.java:116)
at net.example.company.scanner.BleScanner.access$durationTimeoutScanning(BleScanner.java:15)
at net.example.company.scanner.BleScanner$durationScanningRunnable$1.run(BleScanner.java:52)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:246)
at android.app.ActivityThread.main(ActivityThread.java:8595)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)
And my delegate:
class FragmentViewBindingDelegate<T : ViewBinding>(
val fragment: Fragment,
val viewBindingFactory: (Fragment) -> T,
val cleanUp: ((T?) -> Unit)?
) : ReadOnlyProperty<Fragment, T> {
// A backing property to hold our value
private var binding: 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) {
cleanUp?.invoke(binding)
binding = null
}
})
}
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observeForever(
viewLifecycleOwnerLiveDataObserver
)
}
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).not()) {
throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.")
}
return viewBindingFactory(thisRef).also { this.binding = it }
}
}
inline fun <T : ViewBinding> Fragment.viewBinding(
crossinline viewBindingFactory: (View) -> T,
noinline cleanUp: ((T?) -> Unit)? = null
): FragmentViewBindingDelegate<T> =
FragmentViewBindingDelegate(this, { f -> viewBindingFactory(f.requireView()) }, cleanUp)
inline fun <T : ViewBinding> Fragment.viewInflateBinding(
crossinline bindingInflater: (LayoutInflater) -> T,
noinline cleanUp: ((T?) -> Unit)? = null,
): FragmentViewBindingDelegate<T> =
FragmentViewBindingDelegate(this, { f -> bindingInflater(f.layoutInflater) }, cleanUp)
inline fun <T : ViewBinding> AppCompatActivity.viewInflateBinding(
crossinline bindingInflater: (LayoutInflater) -> T
) =
lazy(LazyThreadSafetyMode.NONE) {
bindingInflater.invoke(layoutInflater)
}
@Drjacky sounds like you are missing val binding = binding
in your onViewCreated
OR you are actually accessing the view after onDestroyView
and potentially have a memory leak in your app anyway.
@Zhuinden You mean just this:
override fun onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
val binding = binding
}
But still using the binding
from the top variable in lines that are outside of the onViewCreated
:
class FragBlah : Fragment() {
private val binding by viewInflateBinding(FragBlahBinding::inflate)
...
private fun visibilityConnectingContainer() {
binding.txtTitle.visibilityOff() //this binding is the one from the top class variable
}
?
In that case, the problem is that you have some listener that is called even after onDestroyView.
That's not a bug in this code.
@Drjacky I see that you forgot to add the layout name in the class FragBlah : Fragment() . It should be class FragBlah : Fragment(R.layout.your_layout_name).
@flamesoft But we have setContentView(binding.root)
in onCreate
for Activity and = binding.root
in onCreateView
I haven't tried this. I have just tested it with fragments and that works. Try to use fragment and see if it works. @Drjacky
How would I use this in DialogFragment?
I just don't use this in a DialogFragment.
@Zhuinden Isn't it possible to add support for it, or have a similar solution for it?
@AndroidDeveloperLB There are two "flavors" of DialogFragment
, they have different view lifecycle. You can find examples for both cases here.
@gmk57 Oh these make sense. Thank you!
sounds like you are missing
val binding = binding
in youronViewCreated
OR you are actually accessing the view afteronDestroyView
and potentially have a memory leak in your app anyway.
Btw there is an updated version of this gist as a library in https://github.com/Zhuinden/fragmentviewbindingdelegate-kt
@Zhuinden what is the real reason behind val binding = binding
in most cases we directly access binding. is this required ? .
@manju23reddy I debugged a guy's code who had trouble with callbacks of a WebView running on a different thread and this was the fix, so I do do it in my code personally.
I've had to do a modification on this idea that allows us to use the binding right before it gets nullified.
An example scenario is when we need to null the adapter of a recyclerView when a fragment is destroyed. Using this gist will lead to a crash because the life cycle state DESTROYED
is set before any of the onDestroy*
methods are called inside the fragment, which causes the throw
in line 50 of this gist.
I've added an optional onDestroyListener
parameter to the class, ex:
class FragmentViewBindingDelegate<T : ViewBinding>(
val fragment: Fragment,
val viewBindingFactory: (View) -> T,
val onDestroyListener: () -> Unit = {}
) : ReadOnlyProperty<Fragment, T> {
Which is then used inside the observer's onDestroy
, ex:
viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
onDestroyListener()
binding = null
}
})
This guarantees that any sync code inside that listener has access to the binding before it is null, which allows me to, for example, remove the adapter of a recyclerView when the fragment is destroyed.
@ParticleCore i did eventually move this code to a library in https://github.com/Zhuinden/fragmentviewbindingdelegate-kt because these oddities kept coming up inherited from Google's code so it made sense to version it instead.
Why have you removed the
AppCompatActivity.viewBinding
extension function?I've read both your blogs posts "Simple one-liner ViewBinding in Fragments and Activities with Kotlin" and "An update to the FragmentViewBindingDelegate: the bug we’ve inherited from AutoClearedValue" but haven't found any information about this removal