Practical practice of combining viewBinding and reflection

First, the official tutorial points the way: view binding

It will take about 5-20 minutes to read this article carefully.

You can also jump directly to 3.0 at the end of the article to see the final plan.

Directory

    • 1 About ViewBinding
      • The first point is that naming conforms to certain rules.
      • Second point, inherited from ViewBinding
    • 2 Inconveniences in normal use
    • 3 Without further ado, let’s just look at the code 1.0
    • 4 Without further ado, let’s just look at the code 2.0
    • 5 Without further ado, let’s just look at the code 3.0

1 About ViewBinding

XXXBinding is automatically generated:
1. Naming conforms to certain rules
2. All XXXBindings inherit from the ViewBinding base class

First point, naming follows certain rules

This can be seen in the official documentation:

When view binding is enabled for a module, a binding class is generated for each XML layout file contained in the module. Each binding class contains a reference to the root view and all views with IDs. The name of the binding class is generated by converting the name of the XML file to camelCase and adding the word “Binding” to the end.

For example, if the Activity is named FirstTestActivity, then the corresponding generated binding class is named ActivityTestFirstBinding

Second point, inherited from ViewBinding

This can be seen in the code:

2 Inconveniences in normal use

According to the official documentation, use it in Activity as follows:

 private lateinit var binding: ResultProfileBinding

    override fun onCreate(savedInstanceState: Bundle) {<!-- -->
        super.onCreate(savedInstanceState)
        binding = ResultProfileBinding.inflate(layoutInflater)
        val view = binding.root
        setContentView(view)
    }
    

Note that a lateinit var variable is defined here
This means we have to manually initialize before using
As in the official documentation, in the Activity#onCreate() method, we need to manually call the inflate method for initialization
Apparently some of this code is fixed template:

binding = ResultProfileBinding.inflate(layoutInflater) // Only the Binding class name of ResultProfileBinding will change
val view = binding.root
setContentView(view)

Change the corresponding template of YyyActivity only to be different from YyyBinding, but the others remain unchanged.

Similarly, let’s see what it looks like in Fragment:

 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() {<!-- -->
        super.onDestroyView()
        _binding = null
    }

It can be seen that the same is true for using viewBinding in Fragment, and there is template code

By now you should know what I want to do: can I get rid of the template code?

The answer is yes, the practical plan I gave is: Reflection + Generics

3 Without further ado, let’s just look at the code 1.0

We can directly use the above Binding naming rules to do it

/**
 * Simplify the initialization of ViewBinding through generics + reflection, simplifying the complex and simplifying it. You don’t need to initialize it manually every time. You only need to pass in the type.
 * Inheritance rules: subclass suffix must end with Activity
 */
open class BaseActivity<BindingClass> : AppCompatActivity() {<!-- -->
    //Private Binding class, the type is the actual Binding type used
    private var innerBinding: BindingClass? = null
    //Binding class exposed to subclasses, returning the non-empty type of innerBinding is mainly to avoid use by subclasses
    //It needs to be added frequently!! (If you have a better solution, please tell me)
    val binding: BindingClass by lazy {<!-- --> innerBinding!! }
    override fun onCreate(savedInstanceState: Bundle?) {<!-- -->
        super.onCreate(savedInstanceState)
        //The following operations are basically the same
        val acName = javaClass.simpleName
        val name = acName.substring(0, acName.indexOf("Activity"))
        val bindingClass =
            classLoader.loadClass("com.example.test.databinding.Activity${<!-- -->name}Binding")
        //Finally forced conversion to the actual Binding type passed in as a generic
        innerBinding = bindingClass.getMethod("inflate", LayoutInflater::class.java)
            .invoke(null, layoutInflater) as BindingClass
    }
}

// Subclass
class MainActivity : BaseActivity<ActivityMainBinding>() {<!-- -->
    override fun onCreate(savedInstanceState: Bundle?) {<!-- -->
        super.onCreate(savedInstanceState)
        //Associated layout (just use the binding of BaseActivity directly)
        setContentView(binding.root)
    }
}

At this point, the subclass only needs to associate the layout setContentView(binding.root)

4 Without further ado, let’s just look at the code 2.0

In fact, careful friends will find that version 1.0 has limitations:
The defined XxxActivity must end with Activity, and the package it is in must be com.example.test.databinding

This is equivalent to dead code, which is not recommended and is not good.
So let’s continue to think of ways to solve this problem

We actually need to solve this problem now loadClass("com.example.test.databinding.Activity${name}Binding")

Note again that in the above 1.0 we used generics, and in the subclass we actually gave the Binding class name

class MainActivity : BaseActivity<ActivityMainBinding>()

Isn’t this the actual parameter name of the generic parameter?

Therefore, friends who are familiar with reflection should be able to imagine that our next step is to use reflection clazz.getGenericSuperclass().getActualTypeArguments() to get the class name of the subclass’s generic actual parameter.

Without further ado, let’s look directly at the code:

//BaseActivity.kt
open class BaseActivity<BindingClass> : AppCompatActivity() {<!-- -->
    //Private Binding class, the type is the actual Binding type used
    private var innerViewBinding: BindingClass? = null
    //Use lazy method to avoid initialization errors during creation. Because of the actual usage of the official template, binding needs to initialize LayoutInflater after onCreate.
    // Then here, through lazy method, subsequent subclasses no longer need to be initialized manually.
    protected val mViewBinding: BindingClass by lazy {<!-- --> innerViewBinding!! }

    override fun onCreate(savedInstanceState: Bundle?) {<!-- -->
        super.onCreate(savedInstanceState)
        // Get the binding class name and class of the corresponding view through reflection
        val actualGenericTypeName = GenericUtil.getActualGenericTypeName(this.javaClass)
        MyLogUtil.d("binding", "this.javaClass...: $actualGenericTypeName")
        //Class loader loads the class
        val bindingClass = classLoader.loadClass(actualGenericTypeName)
        //Normal initialization of viewBinding in an Activity:
        // binding = ResultProfileBinding.inflate(layoutInflater)
        //In Fragment:
        // _binding = ResultProfileBinding.inflate(inflater, container, false)
        innerViewBinding = bindingClass
            .getMethod("inflate", LayoutInflater::class.java)
            .invoke(null, layoutInflater) as BindingClass
        //The inflate method is a class method of ViewBinding, not an object method, so the obj parameter is null

    }
}
// GenericUtil.java
public class GenericUtil {<!-- -->
    public static String getActualGenericTypeName(@NonNull Class clazz) {<!-- -->
        String className;
        Type genericSuperclass = clazz.getGenericSuperclass();
        ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
        try {<!-- -->
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            Class tClass = (Class) actualTypeArguments[0];
            className = tClass.getName();
        } catch (Exception e) {<!-- -->
            throw new IllegalArgumentException("Wrong ViewBinding Type");
        }
        return className;
    }
}

use:

//Subclass
class MainActivity : BaseActivity<ActivityMainBinding>() {<!-- -->
    override fun onCreate(savedInstanceState: Bundle?) {<!-- -->
        super.onCreate(savedInstanceState)
        //Associated layout (just use the binding of BaseActivity directly)
        setContentView(binding.root)
    }
}

5 Without further ado, let’s just look at the code 3.0

At this point, it’s basically solved

But some friends will also find that the line of code setContentView(binding.root) in the subclass is also a fixed template code. Can this be simplified to the extreme?

The answer is of course yes

Direct operation:

The binding has been loaded in BaseActivity. Can I just write setContentView(binding.root) directly into BaseActivity?
NO, in actual operation, I found that there is no need to compile, the compiler will prompt an error:

Why? Ask your friends to think about the dynamic loading process for themselves

Then you will find that this problem can continue to be completed through dynamic loading:

// BaseActivity.kt
val invoke = bindingClass.getMethod("getRoot").invoke(mViewBinding) as View
setContentView(invoke)

Complete code:

// BaseActivity.kt
open class BaseActivity<BindingClass> : AppCompatActivity() {<!-- -->
    //Private Binding class, the type is the actual Binding type used
    private var innerViewBinding: BindingClass? = null
    protected val mViewBinding: BindingClass by lazy {<!-- --> innerViewBinding!! }

    override fun onCreate(savedInstanceState: Bundle?) {<!-- -->
        super.onCreate(savedInstanceState)
        // Get the binding class name and class of the corresponding view through reflection
        val actualGenericTypeName = GenericUtil.getActualGenericTypeName(this.javaClass)
        val bindingClass = classLoader.loadClass(actualGenericTypeName)
        innerViewBinding = bindingClass
            .getMethod("inflate", LayoutInflater::class.java)
            .invoke(null, layoutInflater) as BindingClass
        val invoke = bindingClass.getMethod("getRoot").invoke(mViewBinding) as View
        setContentView(invoke)
    }
}

Use in subclasses:

class MainActivity : BaseActivity<ActivityMainBinding>() {<!-- -->
    override fun onCreate(savedInstanceState: Bundle?) {<!-- -->
        super.onCreate(savedInstanceState)
        // ...Complete other logic of the subclass...
    }
}

The subclass does not need to do anything at all, it only needs to bind the generic parameters!

You’re done, it’s much easier to use in practice

The above is this practice. If it is helpful to you, please use your little hands to make a fortune, like and follow~

If you have other opinions, please feel free to communicate via private message or in the comment area~