-
-
Save gpeal/9925b6333220dcdd3ad29d7c5081c5ea to your computer and use it in GitHub Desktop.
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 |
@zeusalmighty717 updated both gists
Thanks for sharing this, very helpful.
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?
I faced the similar issue with @sjaramillo10. Could anyone find a solution ?
@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.
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( ** );
}
Any demo code for activity ?
Could you provide the cast method? Also, how do you handle this for
Activity
?