Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Fragment view binding delegate
// 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)
@Zhuinden
Copy link
Author

Zhuinden commented Feb 16, 2021

I would need an actual project to debug the timing of things, and to see if val binding = binding is called at the start of onViewCreated

@ziselos
Copy link

ziselos commented Feb 17, 2021

@Zhuinden
I just send you an email with access on project repo that reproduce the crash

@Zhuinden
Copy link
Author

Zhuinden commented Feb 17, 2021

I could not reproduce the crash, but ViewPager's FragmentPagerAdapter/FragmentStatePagerAdapter should NEVER contain a List<Fragment>. And you should NEVER initialize a Fragment with new SomeFragment() WITHOUT first checking for findFragmentByTag if that given fragment exists.

https://stackoverflow.com/questions/54279509/how-to-get-elements-of-fragments-created-by-viewpager-in-mainactivity

So that could easily be a cause of your problem.

@JBlaz
Copy link

JBlaz commented Feb 22, 2021

@Zhuinden

throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.")

The string in this exception seems a little misleading since if you access a binding in onDestoryView() it crashes but in onDestoryView() getView() is not null. This comment implies that you should be able to safely do this

getView()?.let {
    binding.map.onDestroy()
}

But this causes that crash when called from onDestroyView() as has been mentioned by others.

@hvsimon
Copy link

hvsimon commented Feb 25, 2021

@Zhuinden

What if I need to assess binding to call some view release method when onDestoryView(). How can I do?

For example

fun onDestoryView() {
    // Below code will throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.")
    binding.recyclerView.adapter = null
    binding.myView.onDestroy()
    super.onDestroyView()
}

@Drjacky
Copy link

Drjacky commented Feb 25, 2021

What if I need to assess binding to call some view release method when onDestoryView(). How can I do?

For example

fun onDestoryView() {
    // Below code will throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.")
    binding.recyclerView.adapter = null
    binding.myView.onDestroy()
    super.onDestroyView()
}

@hvsimon

https://gist.github.com/Zhuinden/ea3189198938bd16c03db628e084a4fa#gistcomment-3619653
https://gist.github.com/Zhuinden/ea3189198938bd16c03db628e084a4fa#gistcomment-3622614

@gmk57
Copy link

gmk57 commented Feb 25, 2021

What if I need to assess binding to call some view release method when onDestoryView()

You can also use this or this approach.

@Cochi
Copy link

Cochi commented Apr 8, 2021

I replace the way i used for binding views with FragmentViewBindingDelegate and everything worked great until i came across with the below scenario:
I have a parent fragment with a tabLayout of 4 tabs (inner fragments). If i click 4th tab and then press back(it should go back to the first tab), on onBackPressed handling on root activity i am getting

java.lang.IllegalStateException: Can't access the Fragment View's LifecycleOwner when getView() is null i.e., before onCreateView() or after onDestroyView()

If i understand correctly this is because i am trying to access views from 1st fragment (in order to set viewpager current position to 0) but these views no longer exists as the 1st tab fragment has been destroyed. The only way to make it work for now is to recreate the parent fragment.

Same issue that @ziselos

I decided to make the viewBinding nullable and add a null safety check on my binding object binding?.run{...}:

class CustomViewBindingDelegate<out T : ViewBinding>(
    val view: View,
    val viewBindingFactory: (LayoutInflater, ViewGroup) -> T
) : ReadOnlyProperty<View, T?> {
   ...
}



override fun getValue(thisRef: Fragment, property: KProperty<*>): T? {
    val binding = binding
    if (binding != null) {
        return binding
    }

     try {
         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.")
         }
    } catch (e : Exception) {
        Timber.e(e)
    }
     
    return thisRef.view?.let { viewBindingFactory(it).also { this.binding = it }}
}

@Gryzor
Copy link

Gryzor commented Apr 12, 2021

This "Fancy" one-liner is simply replacing this code:

private var _binding: SomeBinding?
private val binding = _binding!!
onDestroyView() {
   _binding = null
}
onCreateView() {
    _binding = SomeBinding.inflate(...)
    return _binding.root
}

Another bad news about the "delegate":

If you open your screen, all is nice and shinny, and then you go to settings -> accessibility and change the font size, DefaultLifecycleObserver does not trigger appropriately. So your "binding" is now outdated and you can see what the leak (and side-effect) is.

The reason is buried somewhere in Android, but long story short, is the fragment is not really destroyed, but its view is.

@gmk57
Copy link

gmk57 commented Apr 12, 2021

This "Fancy" one-liner is simply replacing this code:

Exactly, because we don't want this ugly and error-prone code repeated all over our fragments.

DefaultLifecycleObserver does not trigger appropriately

It is triggered, if you attach it to a correct lifecycle (viewLifecycleOwner.lifecycle).

the fragment is not really destroyed, but its view is

Yes, because fragments have two separate lifecycles. The same happens when you put a fragment in a back stack.

@Zhuinden
Copy link
Author

Zhuinden commented Apr 12, 2021

This "Fancy" one-liner is simply replacing this code:

private var _binding: SomeBinding?
private val binding = _binding!!
onDestroyView() {
   _binding = null
}
onCreateView() {
    _binding = SomeBinding.inflate(...)
    return _binding.root
}

Another bad news about the "delegate":

If you open your screen, all is nice and shinny, and then you go to settings -> accessibility and change the font size, DefaultLifecycleObserver does not trigger appropriately. So your "binding" is now outdated and you can see what the leak (and side-effect) is.

The reason is buried somewhere in Android, but long story short, is the fragment is not really destroyed, but its view is.

Interesting, I would expect that if Android kills the view, then onDestroyView should be called. 🤔 I might have to look at it later, although I would expect the lifecycle to be symmetric...

@Gryzor
Copy link

Gryzor commented Apr 13, 2021

Interesting, I would expect that if Android kills the view, then onDestroyView should be called. 🤔 I might have to look at it later, although I would expect the lifecycle to be symmetric...

We did too, but I found out they were using setRetainInstance(true) and this alters the lifecycle of a Fragment in Fragment Manager during these configuration changes; this confuses DefaultLifecycleObserver's owner and the callback is not called.

Exactly, because we don't want this ugly and error-prone code repeated all over our fragments.

I don't think this is "too error prone", but then again, I don't know your coding practices, style, and the environment you have to work on. Not all teams work the same way, so I understand the motivation behind something like this.

It is triggered, if you attach it to a correct lifecycle (viewLifecycleOwner.lifecycle).

It is not triggered if a fragment has `setRetainInstance(true). This may or may not surprise you, but a lot of apps, companies, devs, etc. enabled that in the past (maybe still do) because it's a "cheap way to get the state saved". It's a thing, and while I've never personally used it, I've faced many places where the side-effects of that are everywhere.

This is an unexpected side-effect. In a mid-large size project, it's very easy for things like this to slip through the cracks.

Yes, because fragments have two separate lifecycles. The same happens when you put a fragment in a back stack.

Thank you, I've been doing Android since 2010, I kind of knew about this; I was there when What The Fragment happened... :/

@Zhuinden
Copy link
Author

Zhuinden commented Apr 13, 2021

setRetainFragment() is technically deprecated, but I admit I didn't think about that. 🤔

At that case, that just means the AutoClearedValue I based this code on is conceptually flawed.

@Gryzor
Copy link

Gryzor commented Apr 13, 2021

setRetainFragment() is technically deprecated, but I admit I didn't think about that. thinking

Agreed, me neither. It took me a few days to make the link. The project I am working on, started using ViewBinding, and they made a "clever" property delegate like this... and during a regression test, someone from the QA dept. detected that after going to accessibility and changing the font size, the screens became unresponsive (not ANR style, but clicks did nothing).

It took me a few days/hours of debugging to realize their fragments inherited from a lame BaseFragment that did "setRetainInstance = true". It was buried in an abstract method. Anyway, a spaghetti disaster. I am merely trying to raise awareness of how something so "simple" can have unintended consequences.

Should they be doing retain? NO
But they do and until it can be changed, one has to be aware of cases not considered.

Regarding the setRetainInstance deprecation, also to consider is that this was a relatively recent deprecation:

Version 1.3.0
February 10, 2021

setRetainInstance() deprecation: The setRetainInstance() method on Fragments has been deprecated. With the introduction of ViewModels, developers have a specific API for retaining state that can be associated with Activities, Fragments, and Navigation graphs. This allows developers to use a normal, not retained Fragment and keep the specific state they want retained separate, avoiding a common source of leaks while maintaining the useful properties of a single creation and destruction of the retained state (namely, the constructor of the ViewModel and the onCleared() callback it receives).

source: https://developer.android.com/jetpack/androidx/releases/fragment

At that case, that just means the AutoClearedValue I based this code on is conceptually flawed.

It could be or, in Google's words: "Working as intended" :p

@benjdero
Copy link

benjdero commented Apr 23, 2021

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

@Drjacky
Copy link

Drjacky commented Apr 23, 2021

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

@VahidGarousi
Copy link

VahidGarousi commented Jun 15, 2021

Hello, I hope you are well.
I found a problem that I will explain step by step.

  1. Create a fragment called CruiseFilterFragment and then add a button to the page to click on it to go to the CruiseResultFragment fragment.
  2. 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)

@Zhuinden
Copy link
Author

Zhuinden commented Jun 15, 2021

@VahidGarousi it sounds like you aren't using viewLifecycleOwner.lifecycleScope.launchWhenStarted {.

@Uiasdnmb
Copy link

Uiasdnmb commented Jun 19, 2021

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).

@marlonlom
Copy link

marlonlom commented Jun 22, 2021

@Uiasdnmb

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

@Uiasdnmb
Copy link

Uiasdnmb commented Jun 22, 2021

You can see in fragment manager source how fragment view destruction is performed:

  1. performDestroyView - which is the place where lifecycle becomes destroyed and onDestroyView is called
  2. fragments view reference is set to null
  3. viewLifecycleOwnerLiveData value is set to null

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.

@MasterCluster
Copy link

MasterCluster commented Jul 8, 2021

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
    }

@Uiasdnmb
Copy link

Uiasdnmb commented Jul 10, 2021

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.

@Drjacky
Copy link

Drjacky commented Oct 21, 2021

@ZacSweers

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

@Zhuinden
Copy link
Author

Zhuinden commented Oct 21, 2021

@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.

@Drjacky
Copy link

Drjacky commented Oct 21, 2021

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

?

@Zhuinden
Copy link
Author

Zhuinden commented Oct 21, 2021

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.

@flamesoft
Copy link

flamesoft commented Jan 11, 2022

@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).

@Drjacky
Copy link

Drjacky commented Jan 12, 2022

@flamesoft But we have setContentView(binding.root) in onCreate for Activity and = binding.root in onCreateView

@flamesoft
Copy link

flamesoft commented Jan 12, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment