Like and follow, no longer get lost, your support means a lot to me!

🔥 Hi, I am Ugly. This article has been collected by GitHub · Android-Notebook. Welcome to grow with Peng Chouchou. (Contact info on GitHub)


preface

  • ViewBinding is a new feature in Android Gradle Plugin 3.6. It is used to implement ViewBinding (binding views to variables) more lightly. It can be thought of as a lightweight version of DataBinding.
  • In this post, I’m going to summarize how to use ViewBinding. Please be sure to like and follow if it helps, it’s really important to me.
  • The code for this article can be downloaded from DemoHall·KotlinDelegate.

directory


Front knowledge

The content of this article will cover the following preconditions/related knowledge, thoughtful I have helped you to prepare, please enjoy ~

  • Kotlin | entrusted mechanism & principle & application
  • Kotlin | extension functions (finally know why with this, let use it)
  • Java | about generic can ask it’s all here (including Kotlin)
  • Android | fragments core principles & interview questions (AndroidX version)

1. An overview of the

  • What problem does ViewBinding solve? Instead of findViewById, ViewBinding makes view interaction more elegant.

  • The Android Gradle plugin creates a binding class for each XML layout file. The binding class contains references to each View in the layout file that defines the Android: ID property. Assuming the layout file is fragment_test.xml, the binding class FragmentTestBinding.

  • Does generating Java classes for all XML layout files result in a sudden increase in package size? Unused classes are compressed in case of confusion.


2. Comparison of ViewBinding with other ViewBinding schemes

Before ViewBinding, there were several ViewBinding solutions, which you’ve probably used. So, ViewBinding as a rising star must be more fragrant than the former, I take you to analyze.

The Angle findViewById ButterKnife Kotlin Synthetics DataBinding ViewBinding
simplicity
Compile-time check
Compilation speed
Support for Kotlin & Java
Convergence template code
  • Simplicity: findViewById and ButterKnife require many variables to be declared in the code. Several other scenarios have code that is simple and readable;

  • Compile check: There are two main aspects of check during compile: type check + only access ids in the current layout. FindViewById, ButterKnife and Kotlin Synfooting were poor in this aspect.

  • Compilation speed: findViewById has the fastest compilation speed, while ButterKnife and DataBinding have annotation processing, which is slightly slower than Kotlin Synfooting and ViewBinding.

  • Kotlin & Java support: Kotlin Synfooting only supports Kotlin language;

  • Convergent template code: Basically every scenario has a certain amount of template code in it, and only Kotlin Synslab has a small amount.

As you can see, there is no single dominant approach, but the overall effect does improve. By the way, what is ❓?


3. How to use ViewBinding?

In this section, we’ll show you how to use ViewBinding.

Note: ViewBinding requires Android Gradle Plugin version at least 3.6.

3.1 Adding Configuration

View binding is enabled at the module level. Enabled modules need to be configured in build.gralde at the module level. Such as:

build.gradle

android {
    ...
    viewBinding {
        enabled = true
    }
}
Copy the code

For layout files that do not need to generate binding classes, you can declare tools:viewBindingIgnore=”true” in the root node. Such as:

<LinearLayout
    ...
    tools:viewBindingIgnore="true" >
    ...
</LinearLayout>
Copy the code

3.2 Creating a Binding Class

There are three apis for creating binding classes:

fun <T> bind(view : View) : T fun <T> inflate(inflater : LayoutInflater) : T fun <T> inflate(inflater : LayoutInflater, parent : ViewGroup? , attachToParent : Boolean) : TCopy the code
  • 1. Use in an Activity

MainActivity.kt

class TestActivity: AppCompatActivity(R.layout.activity_test) {

    private lateinit var binding: ActivityTestBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityTestBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.tvDisplay.text = "Hello World."
    }
}   
Copy the code
  • 2. Use it in fragments

TestFragment.kt

class TestFragment : Fragment(R.layout.fragment_test) {

    private var _binding: FragmentTestBinding? = null
    private val binding get() = _binding!!

    override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
        _binding = FragmentTestBinding.bind(root)

        binding.tvDisplay.text = "Hello World."
    }

    override fun onDestroyView() {
        super.onDestroyView()

        _binding = null
    }
}
Copy the code

3.3 Avoiding memory Leaks

There is a hidden memory leak that you need to understand (this is not strictly a ViewBinding issue, even if you use other ViewBinding solutions).

Question: Why does Fragment#onDestroyView() need an empty bound class object, but not an Activity? A: The life cycles of Activity instances and Activity views are synchronized, while the life cycles of Fragment instances and Fragment views are not completely synchronized. Therefore, you need to manually recycle bound objects when destroying Fragment views. Otherwise, memory leaks may occur. For example, detach Fragment or remove Fragment and the transaction enters the return stack, the Fragment view is destroyed but the Fragment instance exists. Of fragments and transaction life cycle before I discussed in an article: Android | fragments core principles & interview questions (AndroidX version)

In general, when the view is destroyed but the instance of the control class object is still alive, you need to manually reclaim the bound class object, otherwise you will cause a memory leak.

3.4 ViewBinding binding class source

Decompiled as follows:

public final class ActivityTestBinding implements ViewBinding {
    private final ConstraintLayout rootView;
    public final TextView tvDisplay;
  
    private ActivityTestBinding (ConstraintLayout paramConstraintLayout1, TextView paramTextView)
        this.rootView = paramConstraintLayout1;
        this.tvDisplay = paramTextView;
    }
  
    public static ActivityTestBinding bind(View paramView) {
        TextView localTextView = (TextView)paramView.findViewById(2131165363);
        if (localTextView != null) {
            return new ActivityMainBinding((ConstraintLayout)paramView, localTextView);
        }else {
          paramView = "tvDisplay";
        }
        throw new NullPointerException("Missing required view with ID: ".concat(paramView));
    }
  
    public static ActivityMainBinding inflate(LayoutInflater paramLayoutInflater) {
        return inflate(paramLayoutInflater, null, false);
    }
  
    public static ActivityMainBinding inflate(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup, boolean paramBoolean) {
        paramLayoutInflater = paramLayoutInflater.inflate(2131361821, paramViewGroup, false);
        if (paramBoolean) {
            paramViewGroup.addView(paramLayoutInflater);
        }
        return bind(paramLayoutInflater);
    }
  
    public ConstraintLayout getRoot() {
        return this.rootView;
    }
}
Copy the code

4. ViewBinding and Kotlin delegate

At this point, the tutorial for using ViewBinding is over. But looking back, are there any limitations?

  • Creating and recycling ViewBinding objects requires repeated boilerplate code, especially in the case of fragments.
  • The binding attribute is null and mutable, making it inconvenient to use.

So, is there an optimized solution? We remember the Kotlin property entrusted on Kotlin entrust mechanism in my previous article discussed: Kotlin | entrusted mechanism & principle. If you’re not familiar with Kotlin delegates, this is going to be a little difficult for you.

Next, I’ll walk you through the steps of wrapping the ViewBinding property delegate tool:

4.1 ViewBinding + Kotlin Delegate 1.0

First, let’s sort out what we want to delegate, what we need, and how to solve it:

demand The solution
Call to delegate ViewBinding#bind() is required reflection
Calls that require the delegate binding = null Listen for the lifecycle of the Fragment view
The binding attribute is expected to be declared as a non-empty immutable variable ReadOnlyProperty<F, V>

FragmentViewBindingPropertyV1.kt

private const val TAG = "ViewBindingProperty" public inline fun <reified V : ViewBinding> viewBindingV1() = viewBindingV1(V::class.java) public inline fun <reified T : ViewBinding> viewBindingV1(clazz: Class<T>): FragmentViewBindingPropertyV1<Fragment, T> { val bindMethod = clazz.getMethod("bind", View::class.java) return FragmentViewBindingPropertyV1 { bindMethod(null, It. RequireView ()) as T}} / * * * @ param viewBinder create binding class object * / class FragmentViewBindingPropertyV1 < in F: Fragment, out V : ViewBinding>( private val viewBinder: (F) -> V ) : ReadOnlyProperty<F, V> { private var viewBinding: V? = null @mainThread Override fun getValue(thisRef: F, property: KProperty<*>): V { .let { return it } // Use viewLifecycleOwner.lifecycle other than lifecycle val lifecycle = thisRef.viewLifecycleOwner.lifecycle val viewBinding = viewBinder(thisRef) if (lifecycle.currentState == Lifecycle.State.DESTROYED) { Log.w( TAG, "Access to viewBinding after Lifecycle is destroyed or hasn't created yet. " + "The instance of viewBinding will be not cached." ) // We can access to ViewBinding after Fragment.onDestroyView(), but don't save it to prevent memory leak } else { lifecycle.addObserver(ClearOnDestroyLifecycleObserver()) this.viewBinding = viewBinding } return viewBinding } @MainThread fun clear() { viewBinding = null } private inner class  ClearOnDestroyLifecycleObserver : LifecycleObserver { private val mainHandler = Handler(Looper.getMainLooper()) @MainThread @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun onDestroy(owner: LifecycleOwner) { owner.lifecycle.removeObserver(this) mainHandler.post { clear() } } } }Copy the code

Usage:

class TestFragment : Fragment(R.layout.fragment_test) {

    private val binding : FragmentTestBinding by viewBindingV1()

    override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
        binding.tvDisplay.text = "Hello World."
    }
}
Copy the code

Clean and fresh! The three requirements mentioned above have been fulfilled, and now I will answer the details for you:

  • Why can we use V::class.java, but not generic erasers? Using the Kotlin restraint function + real type parameters, the compiled function body as a whole are copied to the call to V: : class. Java is actually FragmentTestBinding: : class. Java. Concrete analysis see: Java | about generic can ask it’s all here (including Kotlin)

  • What is ReadOnlyProperty

    ? ReadOnlyProperty is an immutable property proxy, which is passed by getValue(…). Method to implement the delegate behavior. In F: Fragment, out V: ViewBinding>
    ,>

  • Question 3: Explain getValue(…) Methods? Read the comments directly:

@mainThread Override fun getValue(thisRef: F, property: KProperty<*>): V { . Let it} {return 2, fragments, view the life cycle of val lifecycle. = thisRef viewLifecycleOwner. Lifecycle 3, instantiate binding class object val viewBinding = ViewBinder (thisRef) if (lifecycle. The currentState = = lifecycle. State. The DESTROYED) {4.1 if view life cycle is DESTROYED, the view is DESTROYED, At this point do not cache the binding class object (to avoid memory leaks)} else {4.2 defined view lifecycle listener lifecycle. The addObserver (ClearOnDestroyLifecycleObserver ()) 4.3 binding class object cache this.viewBinding = viewBinding } return viewBinding }Copy the code
  • Why should onDestroy() be done with Handler#post(Message)? Because Fragment#viewLifecycleOwner notifies the lifecycle event ON_DESTROY before Fragment#onDestryoView.

4.2 ViewBinding + Kotlin Delegate 2.0

Version 1.0 uses reflection. Is reflection really necessary? The purpose of the bind function is to get a ViewBinding class object. Maybe we can try to define the behavior of creating the object externally, something like this:

inline fun <F : Fragment, V : ViewBinding> viewBindingV2( crossinline viewBinder: (View) -> V, crossinline viewProvider: (F) -> View = Fragment::requireView ) = FragmentViewBindingPropertyV2 { fragment: F -> viewBinder(viewProvider(fragment)) } class FragmentViewBindingPropertyV2<in F : Fragment, out V : ViewBinding>(private val viewBinder: (F) -> V) : ReadOnlyProperty<F, V> {same... }Copy the code

Usage:

class TestFragment : Fragment(R.layout.fragment_test) {

    private val binding by viewBindingV2(FragmentTestBinding::bind)

    override fun onViewCreated(root: View, savedInstanceState: Bundle?) {
        binding.tvDisplay.text = "Hello World."
    }
}
Copy the code

Clean and fresh! You can do this without reflection, but here are the details:

  • Question 4. What is (View) -> V? Kotlin higher-order functions can pass lambda expressions directly as arguments, where View is the function argument and T is the function return value. Lambda expressions are essentially “blocks of code that can be passed as values.” In older versions of Java, passing code blocks required anonymous inner class implementations, while passing code blocks as function values did not even require function declarations.

  • Question 5. Fragment::requireView? Pass the function requireView() as a parameter. Fragment#requireView() returns the Fragment root view. Calling requireView() before onCreateView() will raise an exception;

  • Question 6, FragmentTestBinding: : what is the bind? Bind (); bind(); bind(); bind(); bind(); bind();

4.3 ViewBinding + Kotlin Delegate Final version

Version 2.0 has completed property proxying for fragments, but will the actual scenario only use ViewBinding for fragments? Obviously not. Here are some other scenarios:

  • Activity
  • Fragment
  • DialogFragment
  • ViewGroup
  • RecyclerView.ViewHolder

Therefore, it is necessary to wrap the delegate tool in a more general way. You can download the complete code and demo project directly:

@JvmName("viewBindingActivity") inline fun <A : ComponentActivity, V : ViewBinding> viewBinding( crossinline viewBinder: (View) -> V, crossinline viewProvider: (A) -> View = ::findRootView ): ViewBindingProperty<A, V> = ActivityViewBindingProperty { activity: A -> viewBinder(viewProvider(activity)) } @JvmName("viewBindingActivity") inline fun <A : ComponentActivity, V : ViewBinding> viewBinding( crossinline viewBinder: (View) -> V, @IdRes viewBindingRootId: Int ): ViewBindingProperty<A, V> = ActivityViewBindingProperty { activity: A -> viewBinder(activity.requireViewByIdCompat(viewBindingRootId)) } // ------------------------------------------------------------------------------------- // ViewBindingProperty for Fragment // ------------------------------------------------------------------------------------- @Suppress("UNCHECKED_CAST") @JvmName("viewBindingFragment") inline fun <F : Fragment, V : ViewBinding> Fragment.viewBinding( crossinline viewBinder: (View) -> V, crossinline viewProvider: (F) -> View = Fragment::requireView ): ViewBindingProperty<F, V> = when (this) { is DialogFragment -> DialogFragmentViewBindingProperty { fragment: F -> viewBinder(viewProvider(fragment)) } as ViewBindingProperty<F, V> else -> FragmentViewBindingProperty { fragment: F -> viewBinder(viewProvider(fragment)) } } @Suppress("UNCHECKED_CAST") @JvmName("viewBindingFragment") inline fun <F : Fragment, V : ViewBinding> Fragment.viewBinding( crossinline viewBinder: (View) -> V, @IdRes viewBindingRootId: Int ): ViewBindingProperty<F, V> = when (this) { is DialogFragment -> viewBinding(viewBinder) { fragment: DialogFragment -> fragment.getRootView(viewBindingRootId) } as ViewBindingProperty<F, V> else -> viewBinding(viewBinder) { fragment: F -> fragment.requireView().requireViewByIdCompat(viewBindingRootId) } } // ------------------------------------------------------------------------------------- // ViewBindingProperty // ------------------------------------------------------------------------------------- private const val TAG = "ViewBindingProperty" interface ViewBindingProperty<in R : Any, out V : ViewBinding> : ReadOnlyProperty<R, V> { @MainThread fun clear() } abstract class LifecycleViewBindingProperty<in R : Any, out V : ViewBinding>( private val viewBinder: (R) -> V ) : ViewBindingProperty<R, V> { private var viewBinding: V? = null protected abstract fun getLifecycleOwner(thisRef: R): LifecycleOwner @MainThread override fun getValue(thisRef: R, property: KProperty<*>): V { // Already bound viewBinding? .let { return it } val lifecycle = getLifecycleOwner(thisRef).lifecycle val viewBinding = viewBinder(thisRef) if (lifecycle.currentState == Lifecycle.State.DESTROYED) { Log.w( TAG, "Access to viewBinding after Lifecycle is destroyed or hasn'V created yet. " + "The instance of viewBinding will be not cached." ) // We can access to ViewBinding after Fragment.onDestroyView(), but don'V save it to prevent memory leak } else { lifecycle.addObserver(ClearOnDestroyLifecycleObserver(this)) this.viewBinding = viewBinding } return viewBinding } @MainThread override fun clear() { viewBinding = null } private class ClearOnDestroyLifecycleObserver( private val property: LifecycleViewBindingProperty<*, *> ) : LifecycleObserver { private companion object { private val mainHandler = Handler(Looper.getMainLooper()) } @MainThread @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun onDestroy(owner: LifecycleOwner) { mainHandler.post { property.clear() } } } } class FragmentViewBindingProperty<in F : Fragment, out V : ViewBinding>( viewBinder: (F) -> V ) : LifecycleViewBindingProperty<F, V>(viewBinder) { override fun getLifecycleOwner(thisRef: F): LifecycleOwner { try { return thisRef.viewLifecycleOwner } catch (ignored: IllegalStateException) { error("Fragment doesn't have view associated with it or the view has been destroyed") } } } class DialogFragmentViewBindingProperty<in F : DialogFragment, out V : ViewBinding>( viewBinder: (F) -> V ) : LifecycleViewBindingProperty<F, V>(viewBinder) { override fun getLifecycleOwner(thisRef: F): LifecycleOwner { return if (thisRef.showsDialog) { thisRef } else { try { thisRef.viewLifecycleOwner } catch (ignored: IllegalStateException) { error("Fragment doesn't have view associated with it or the view has been destroyed") } } } }Copy the code

5. To summarize

The Android Gradle plugin creates a binding class for each XML layout file. In Fragment#onDestroyView(), set the null bound class object to avoid memory leaks. But this can lead to a lot of repetition of writing boilerplate code, and using property delegates can converge the template code and keep the caller code clean.

The Angle findViewById ButterKnife Kotlin Synthetics DataBinding ViewBinding ViewBindingProperty
simplicity
Compile-time check
Compilation speed
Support for Kotlin & Java
Convergence template code

The resources

  • View Binding — official document
  • View Binding with Kotlin delegate property — Kirill Rozov, translated by Fantersie
  • Who is ButterKnife’s terminator? – the fundroid

Creation is not easy, your “three company” is the biggest power of Ugly, we see next time!