A ViewBinding profile

Kotlin’s 1.4.20 update deprecates the kotlin-android-extensions extension and moves its Parcelable functionality to the new kotlin-parcelize extension. **ViewBinding** is recommended for ViewBinding.

JakeWharton’s ButterKnife has long since been taken out of maintenance and is recommended to migrate to ViewBinding

The Kotlin-Android-Extensions plug-in provides two handy features:

  1. You can get the control object directly from the layout id.
  2. The @parcelize annotation is provided to help developers quickly implement Parcelize

The advantage of this plug-in is that it is particularly comfortable to use, directly with the control ID can be done. The main disadvantages are also obvious, otherwise they would not have been abandoned:

  1. Can not provide null information, different layout files, id is inconsistent, can not be found during compilation (but will provide a warning)
  2. Only supports kotlin
  3. Contaminate the global namespace

In order to solve these problems, Android has introduced ViewBinding solution. When the id of different layout files is inconsistent, the property is declared as @nullable, thus providing Nullable information

While the official viewBinding solves these problems, it also has an obvious drawback: it’s too cumbersome to use!! In addition, when using the Fragment, you must also release binding in the onDestroyView to prevent memory leaks and meaningless code duplication.

ViewBinding is cumbersome

This section only shows the simple fragment usage to introduce the problem. Check out the official Exploring Android View Binding in Depth guide for more details.

ViewBinding is built into The Gradle plugin since Android Studio 3.6. Developers do not need to add additional libraries to enable the build. Gradle file at the module level

/ / Android Studio 3.6
android {
    viewBinding {
        enabled = true}}/ / Android Studio 4.0
android {
    buildFeatures {
        viewBinding = true}}Copy the code

When view binding is enabled, a binding class is generated for each XML layout file in the module. Each binding class contains references to the root view as well as all views with ids. The name of the Binding class is generated by converting the name of the XML file to camel case and adding the word “Binding” to the end. If fragment_binding is used, it is a FragmentBindingBinding

After make project, the binding class file will be generated, which can be found in the build/generated/data_binding_base_class_source_out/ directory at the module level

A common problem with using ViewBinding in a fragment is memory leaks, because the fragment’s lifetime is longer than the lifetime of its view. So release the view in onDestroyView, as shown in the code in the user guide:

.private var _binding: ResultProfileBinding? = null
    // This property is only valid between onCreateView and
    // onDestroyView.
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup? , savedInstanceState:Bundle?).: View? {
        _binding = ResultProfileBinding.inflate(inflater, container, false)
        val view = binding.root
        return view
    }

    override fun onDestroyView(a) {
        super.onDestroyView()
        _binding = null}...Copy the code

You have to release it manually every time. This isn’t a problem if you have a few simple pages, but as your project gets bigger, you have to repeat a lot of code over and over again, and the way your activity is used is different, which can result in a lot of duplicate code in your project

DRY — Don’t repeat yourself

There are two feasible ideas here. One is to change the base class to encapsulate, and the other is to use Kotlin’s delegate attribute function and reflection to encapsulate (but only for Kotlin, easy to use).

3.1 Encapsulation based on base classes

Only one fragment idea is provided here. Other fragments can be implemented similarly and are relatively simple.

abstract class BaseFragment<T : ViewBinding>(layoutId: Int) : Fragment(layoutId) {
    private var _binding: T? = null

    val binding
        get() = _binding!!

    override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
        super.onViewCreated(view, savedInstanceState)
        _binding = initBinding(view)
    }

    abstract fun initBinding(view: View): T

    override fun onDestroyView(a) {
        _binding = null
        super.onDestroyView()
    }
}

class MainFragment :BaseFragment<FragmentMainBinding>(R.layout.fragment_main) {

    override fun initBinding(view: View): FragmentMainBinding = FragmentMainBinding.bind(view)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
        super.onViewCreated(view, savedInstanceState)
        binding.tvContent = "Get high"}}Copy the code

3.2 Delegate attribute + reflection

For those of you who are not familiar with kotlin’s delegate properties, check out this article to thoroughly understand kotlin’s delegates

Only one fragment idea is provided here, the others can be implemented similarly.

To do this, use a direct by bind() :

class BindingFragment:Fragment(R.layout.fragment_binding) {

    private val binding:FragmentBindingBinding by bind()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?). {
        super.onViewCreated(view, savedInstanceState)
        Log.e("binding"."${binding.tvContent.text}")
        binding.tvContent.text = "new value"}}Copy the code

First implement the extension method, returning the direct delegate object FragmentBindingDelegate

inline fun <reified T : ViewBinding> Fragment.bind(a): FragmentBindingDelegate<T> {
    return FragmentBindingDelegate(T::class.java, this)}Copy the code

The delegate object code is as follows:

class FragmentBindingDelegate<T : ViewBinding>(classes: Class<T>, fragment: Fragment) :
    ReadOnlyProperty<Fragment, T> {

    private var binding: T? = null

    private val bindViewMethod by lazy { classes.getMethod("bind", View::class.java) }

    init {
        fragment.lifecycle.donOnDestroy { release() }
    }

    @Suppress("UNCHECKED_CAST")
    override fun getValue(thisRef: Fragment, property: KProperty< * >): T {
        returnbinding ? : let { Log.e("binding"."generate value")
            (bindViewMethod.invoke(null, thisRef.view) as T).also { binding = it }
        }
    }

    private fun release(a) {
        Log.e("binding"."release binding")

        binding = null}}Copy the code
  1. bindingProperty, if not null, returns the object directly:return binding
  2. whenbindingIs reflected and called when the property has not yet been assigned a valueViewBindingthebind(view)Methods:classes.getMethod("bind", View::class.java)And assign a value tobindingattribute
  3. Register life cycle listening inON_DESTROYThe time to release holdbindingObject:fragment.lifecycle.donOnDestroy { release() }

Lifecycle’s extension functions are as follows, freeing resources and removing their own listeners on the ON_DESTROY event

inline fun Lifecycle.donOnDestroy(crossinline destroyed: () -> Unit) {
    addObserver(object : LifecycleEventObserver {
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
            if (event == Lifecycle.Event.ON_DESTROY) {
                destroyed()
                source.lifecycle.removeObserver(this)}}})}Copy the code

A binding value must be used in at least one place to trigger a series of assignment operations. So if you use this scheme to implement the activity and wrap setContentView() in the delegate method, but don’t trigger a binding anywhere, you won’t set the view in it

If there are other better solutions, welcome to point out!!

Iv Reference Links

  1. View Binding Guide
  2. Exploring Android View Binding in Depth
  3. ViewBinding Delegate — one line

Thank you five ❤ ️

  1. If you find this post helpful, give it a thumbs up (👍).

  2. About error correction and suggestions: Welcome to share the record directly in the message (🌹)