ViewBinding encapsulation

ViewBinding is complicated to use, and there is a lot of template code, so some encapsulation is necessary.

First try

First, we try to extract a layer of Base for encapsulation.

abstract class BaseBindingActivity<T : ViewBinding> : BaseActivity() {

    val binding by lazy { getViewBinding() }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        contentView = getViewBinding().root
    }

    protected abstract fun getViewBinding(): T
}
Copy the code

When used:

class XXXActivity : BaseBindingActivity<XXXLayoutBinding>() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        contentView = binding.root

        binding.title.text = "XXX"
    }

    override fun getViewBinding(): XXXLayoutBinding = XXXLayoutBinding.inflate(layoutInflater)
}
Copy the code

Doesn’t seem to have any use for it?

The reason why it is difficult to encapsulate is that you operate the inflate according to the Binding file of the layout file for different activities (or other fragments or dialogs that you want to use). This is the core of the problem and seems to be ungeneric. As a result of the previous encapsulation, the ViewBinding implementation must be passed in.

reflection

In this case, there is only one solution, which is to instantiate by reflection, as shown below.

inline fun <reified T : ViewBinding> inflateViewBinding(layoutInflater: LayoutInflater) =
    T::class.java.getMethod("inflate", LayoutInflater::class.java).invoke(null, layoutInflater) as T
Copy the code

With this reflection, instead of using the base class, use:

class PocketSquareActivity : BaseActivity() {

    val binding by lazy { inflateViewBinding<PocketSquareLayoutBinding>(layoutInflater) }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        contentView = binding.root

        binding.title.text = "XXX"
    }
}
Copy the code

This is… What’s the difference between TMD and me??

So let’s extract the lazy part as well. Create an extension function.

inline fun <reified T : ViewBinding> Activity.inflate() = lazy {
    inflateViewBinding<T>(layoutInflater).apply { setContentView(root) }
}
Copy the code

And then when you use it again, you just pass T, and you get rid of the setContentView call.

class PocketSquareActivity : BaseActivity() {

    val binding by inflate<PocketSquareLayoutBinding>()

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

        binding.title.text = "XXX"
    }
}
Copy the code

It seems that this is ok, can’t save any more, after all, T is dead, must be set separately.

If it is a Dialog, we can also create a similar extension function:

inline fun <reified T : ViewBinding> Dialog.inflate() = lazy {
    inflateViewBinding<T>(layoutInflater).apply { setContentView(root) }
}
Copy the code

The base class

However, it is still necessary to create a variable binding. Going back to the original base class design, since reflection is used, let’s put this logic in the base class.

abstract class BaseBindingActivity<T : ViewBinding> : BaseActivity() { protected lateinit var binding: T @Suppress("UNCHECKED_CAST") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val type = javaClass.genericSuperclass if (type is ParameterizedType) { val clazz =  type.actualTypeArguments[0] as Class<T> val method = clazz.getMethod("inflate", LayoutInflater::class.java) binding = method.invoke(null, layoutInflater) as T contentView = binding.root } } }Copy the code

So our call is finally clean.

class PocketSquareActivity : BaseBindingActivity<PocketSquareLayoutBinding>() {

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

        binding.title.text = "XXX"
    }
}
Copy the code

If an Activity works that way, so does a Fragment.

abstract class BaseBindingFragment<T : ViewBinding> : Fragment() { private var _binding: T? = null protected val binding get() = _binding!! @Suppress("UNCHECKED_CAST") override fun onCreateView(inflater: LayoutInflater, container: ViewGroup? , savedInstanceState: Bundle?) : View { val type = javaClass.genericSuperclass val clazz = (type as ParameterizedType).actualTypeArguments[0] as Class<T>  val method = clazz.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java) _binding = method.invoke(null, layoutInflater, container, false) as T return binding.root } override fun onDestroyView() { super.onDestroyView() _binding = null } }Copy the code

There is a problem with this, however, and that is the release in onDestroyView. Let’s continue simplifying.

abstract class BaseBindingFragment<T : ViewBinding> : Fragment() { private var _binding: T? = null protected val binding get() = _binding!! @Suppress("UNCHECKED_CAST") override fun onCreateView(inflater: LayoutInflater, container: ViewGroup? , savedInstanceState: Bundle?) : View { val type = javaClass.genericSuperclass val clazz = (type as ParameterizedType).actualTypeArguments[0] as Class<T>  val method = clazz.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java) _binding = method.invoke(null, layoutInflater, container, false) as T this.viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onDestroy(owner: LifecycleOwner) { _binding = null } }) return binding.root } }Copy the code

With Lifecycle, the released code can be wrapped in a base class, so that fragments and activities can be used similarly.

For RecyclerView, I think there is no need to packaging, specific use can see the previous article.

Base classes are not used

All of the above are based on inheritance to achieve encapsulation, which has the advantage of simplifying the code call of the subclass as much as possible, but the disadvantage is that it causes some code intrusion, so if you do not use the way of inheriting the base class, you can use Kotlin’s delegate to simplify the call.

For an Activity, this is already done.

inline fun <reified T : ViewBinding> Activity.inflate() = lazy {
    inflateViewBinding<T>(layoutInflater).apply { setContentView(root) }
}

val binding by inflate<PocketSquareLayoutBinding>()
Copy the code

In the case of fragments, it is a little more complicated because the inflate parameter in the Fragment needs three parameters, which are different from the Activity parameter, especially the parent parameter, which is not easy to obtain. Therefore, you can create a Binding using bind, which is also recommended in the official website.

Note: The inflate() method requires you to pass in a layout inflater. If the layout has already been inflated, you can instead call the binding class’s static bind() method. To learn more, see an example from the view binding GitHub sample.

So, we can create the delegate like this.

inline fun <reified T : ViewBinding> Fragment.inflate() = FragmentViewBindingDelegate(T::class.java) class FragmentViewBindingDelegate<T : ViewBinding>(private val clazz: Class<T>) : ReadOnlyProperty<Fragment, T> { private var binding: T? = null @Suppress("UNCHECKED_CAST") override fun getValue(thisRef: Fragment, property: KProperty<*>): T { if (binding == null) { binding = clazz.getMethod("bind", View::class.java).invoke(null, thisRef.requireView()) as T thisRef.viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onDestroy(owner: LifecycleOwner) { binding = null } }) } return binding!! }}Copy the code

Use the same method as Activity.

There are actually two ways to initialize a Binding for a Fragment, one in onCreateView and one in onViewCreated.

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup? ,savedInstanceState: Bundle?) : View? { val binding = XXXXXBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val binding = XXXXBinding.bind(view) }Copy the code

Exotic curiosity-a solution looking

A more interesting encapsulation approach was recently seen on Medium, and if you can read it, you know Kotlin better.

First, let’s look at Kotlin’s “function reference.”

In Kotlin, ::method is used to convert a function into an object, which can then be passed as a parameter. This is an important feature of higher-order functions. We can pass one function as an argument to another function, so that within this function, we can call the function passed in, thus avoiding reflection.

Let’s go back to the extension function. In the end, what we’re really looking for is this:

XXXLayoutBinding.inflate(layoutInflater)
Copy the code

Isn’t that a function of XXXLayoutBinding? So we can use function references to do the following:

fun <T : ViewBinding> Activity.inflate(inflater: (LayoutInflater) -> T) = lazy {
    inflater(layoutInflater).apply { setContentView(root) }
}
Copy the code

Function references do not support inline operations

Object functions are referred to by — object :: function name

Static functions are referred to as — class name :: function name

You add a higher-order function parameter to the inflate extension, the xxxLayoutbinding.inflate method.

It’s also easy to use:

val binding by inflate(PocketSquareLayoutBinding::inflate)
Copy the code

It takes one more parameter than the previous way of using reflection, but has the advantage of avoiding reflection.

Article citations zhuinden.medium.com/simple-one- here…

A little bit simpler?

This might seem like a bit of a hassle, but thankfully Android Studio has code templates, so we can make it as simple as possible.

Open Live Templates for AS, add a template under Kotlin, and set the trigger code to “byVB” (you can customize it, of course) :

private val binding by inflate($CLASS$::inflate)
Copy the code

So in the Kotlin code, just type byVB and the code will complete for us automatically. Just type the Binding class name.

The ViewBinding package is not as easy to use as the Kotlin-Android-Extensions extensions, but there is no way around it. As Kotlin updates, it will be necessary to switch to ViewBinding.

I would like to recommend my website xuyisheng. Top/focusing on Android-Kotlin-flutter welcome you to visit