Skip to content

Instantly share code, notes, and snippets.

@Zhuinden
Last active February 24, 2024 20:13
Show Gist options
  • Star 71 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save Zhuinden/ea3189198938bd16c03db628e084a4fa to your computer and use it in GitHub Desktop.
Save Zhuinden/ea3189198938bd16c03db628e084a4fa to your computer and use it in GitHub Desktop.
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 Jun 15, 2021

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

@Uiasdnmb
Copy link

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

@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

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

@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

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

@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

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

@AndroidDeveloperLB
Copy link

How would I use this in DialogFragment?

@Zhuinden
Copy link
Author

I just don't use this in a DialogFragment.

@AndroidDeveloperLB
Copy link

@Zhuinden Isn't it possible to add support for it, or have a similar solution for it?

@gmk57
Copy link

gmk57 commented Dec 23, 2022

@AndroidDeveloperLB There are two "flavors" of DialogFragment, they have different view lifecycle. You can find examples for both cases here.

@AndroidDeveloperLB
Copy link

AndroidDeveloperLB commented Dec 24, 2022

@gmk57 Oh these make sense. Thank you!

@Zhuinden
Copy link
Author

Zhuinden commented Feb 16, 2023

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.

Btw there is an updated version of this gist as a library in https://github.com/Zhuinden/fragmentviewbindingdelegate-kt

@manju23reddy
Copy link

manju23reddy commented Apr 13, 2023

@Zhuinden what is the real reason behind val binding = binding in most cases we directly access binding. is this required ? .

@Zhuinden
Copy link
Author

Zhuinden commented Apr 13, 2023

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

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