Skip to content

Instantly share code, notes, and snippets.

@gpeal
Last active October 2, 2024 04:10
Show Gist options
  • Save gpeal/9925b6333220dcdd3ad29d7c5081c5ea to your computer and use it in GitHub Desktop.
Save gpeal/9925b6333220dcdd3ad29d7c5081c5ea to your computer and use it in GitHub Desktop.
View Binding Delegates
class WifiNetworksFragment : TonalFragment(R.layout.wifi_networks_fragment) {
// This automatically creates and clears the binding in a lifecycle-aware way.
private val binding: WifiNetworksFragmentBinding by viewBinding()
...
}
class WifiNetworkView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
// This does the inflate too.
private val binding: WifiNetworkViewBinding by viewBinding()
...
}
import android.os.Handler
import android.os.Looper
import android.view.View
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.viewbinding.ViewBinding
import com.tonal.trainer.utils.cast
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
/**
* Create bindings for a view similar to bindView.
*
* To use, just call
* private val binding: FHomeWorkoutDetailsBinding by viewBinding()
* with your binding class and access it as you normally would.
*/
inline fun <reified T : ViewBinding> Fragment.viewBinding() = FragmentViewBindingDelegate(T::class.java, this)
class FragmentViewBindingDelegate<T : ViewBinding>(
private val bindingClass: Class<T>,
val fragment: Fragment
) : ReadOnlyProperty<Fragment, T> {
private val clearBindingHandler by lazy(LazyThreadSafetyMode.NONE) { Handler(Looper.getMainLooper()) }
private var binding: T? = null
private val bindMethod = bindingClass.getMethod("bind", View::class.java)
init {
fragment.viewLifecycleOwnerLiveData.observe(fragment) { viewLifecycleOwner ->
viewLifecycleOwner.lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
// Lifecycle listeners are called before onDestroyView in a Fragment.
// However, we want views to be able to use bindings in onDestroyView
// to do cleanup so we clear the reference one frame later.
clearBindingHandler.post { binding = null }
}
})
}
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
// onCreateView may be called between onDestroyView and next Main thread cycle.
// In this case [binding] refers to the previous fragment view. Check that binding's root view matches current fragment view
if (binding != null && binding?.root !== thisRef.view) {
binding = null
}
binding?.let { return it }
val lifecycle = fragment.viewLifecycleOwner.lifecycle
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
error("Cannot access view bindings. View lifecycle is ${lifecycle.currentState}!")
}
binding = bindMethod.invoke(null, thisRef.requireView()).cast<T>()
return binding!!
}
}
@Suppress("UNCHECKED_CAST")
private fun <T> Any.cast(): T = this as T
/**
* Create bindings for a view similar to bindView.
*
* To use, just call:
* private val binding: FHomeWorkoutDetailsBinding by viewBinding()
* with your binding class and access it as you normally would.
*/
inline fun <reified T : ViewBinding> ViewGroup.viewBinding() = ViewBindingDelegate(T::class.java, this)
class ViewBindingDelegate<T : ViewBinding>(
private val bindingClass: Class<T>,
private val view: ViewGroup
) : ReadOnlyProperty<ViewGroup, T> {
private val binding: T = try {
val inflateMethod = bindingClass.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.javaPrimitiveType)
inflateMethod.invoke(null, LayoutInflater.from(view.context), view, true).cast<T>()
} catch (e: NoSuchMethodException) {
// <merge> tags don't have the boolean parameter.
val inflateMethod = bindingClass.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java)
inflateMethod.invoke(null, LayoutInflater.from(view.context), view).cast<T>()
}
override fun getValue(thisRef: ViewGroup, property: KProperty<*>): T = binding
}
@Suppress("UNCHECKED_CAST")
private fun <T> Any.cast(): T = this as T
@gpeal
Copy link
Author

gpeal commented Apr 1, 2021

@zeusalmighty717 updated both gists

@mirokolodii
Copy link

Thanks for sharing this, very helpful.

@sjaramillo10
Copy link

Hey! I love this, however I tried to implement it on my project and I am having issues with ProGuard/R8:
NoSuchMethodException: ... .bind [class android.view.View]

Did you @gpeal add any special ProGuard rules for this?

@oiyio
Copy link

oiyio commented Jun 27, 2021

I faced the similar issue with @sjaramillo10. Could anyone find a solution ?

@sjaramillo10
Copy link

@oiyio I guess you could add a ProGuard keep rule to keep the bind() method of all classes that extend ViewBinding. We ended up using a different approach that does not require reflection.

@oiyio
Copy link

oiyio commented Jun 27, 2021

Ok i solved the issue by adding the following code to proguard file :
-keep class com.example.mypackage.databinding.* {
public static ** inflate( ** );
public static ** bind( ** );
}

@rahat14
Copy link

rahat14 commented Jan 5, 2022

Any demo code for activity ?

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