Skip to content

Instantly share code, notes, and snippets.

@Zhuinden
Last active February 24, 2024 20:13
  • Star 71 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
Star You must be signed in to star a gist
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)
@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