Make writing a habit together! This is my first day to participate in the “Gold Digging Day New Plan · April More text challenge”, click to see the details of the activity.

preface

While optimizing the ViewBinding utility class some time ago, I suddenly came up with a new encapsulation idea that can further simplify the use of ViewBinding. Personally, I haven’t seen anyone wrap ViewBinding in this way on the web, so I think it’s worth sharing.

But one might ask, in 2022 still learning ViewBinding? While Jetpack Compose is officially in the works, it’s not only Kotlin that Compose has to learn, but also a new way to write UI, which is very expensive to learn. For many people, Java is not unusable and XML layout is not unusable, so they are not willing or have enough time to learn Compose. Copmpose still has a long way to go. ViewBinding is the best option until Compose becomes popular, and I think it’s worth learning about ViewBinding. Even though Compose is currently used, not all pages will be composed, and some will write XML layouts.

First, it is not to demolish the existing package scheme, but to complement and improve the existing scheme, can expand more use scenarios. Also, this is not a particularly hard idea to think about. You may have seen this idea in other scenarios, but you haven’t seen anyone using ViewBinding this way.

This article will focus on encapsulation. If you are not familiar with ViewBinding, you can read the previous article on ViewBinding.

  1. Elegantly encapsulating and using ViewBinding, time to replace Kotlin Synthetic and ButterKnife
  2. ViewBinding cleverly encapsulates ideas and ADAPTS BRVAH in this way

The essence of ViewBinding

Let’s take a quick look at the ViewBinding usage. Use a ViewBinding in build.gradle configuration.

android {
    buildFeatures {
        viewBinding true
    }
}
Copy the code

After configuration, each layout will generate the corresponding ViewBinding class. For example, we create a layout_test.xml:

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <TextView
    android:id="@+id/tv_hello_world"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hello world!"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code

This generates a LayoutTestBinding class, and there are three ways to create a Binding object, depending on the situation. The ViewBinding object is used to obtain the control whose ID is declared on the layout.

val binding = LayoutTestBinding.inflate(layoutInflater)
// val binding = LayoutTestBinding.inflate(layoutInflater, parent, false)
// val binding = LayoutTestBinding.bind(view)
binding.tvHelloWorld.text = "Hello Android!"
Copy the code

For those of you wondering what this Binding class is, let’s take a look at the source code:

public final class LayoutTestBinding implements ViewBinding {
  @NonNull
  private final ConstraintLayout rootView;

  @NonNull
  public final TextView tvHelloWorld;

  private LayoutTestBinding(@NonNull ConstraintLayout rootView, @NonNull TextView tvHelloWorld) {
    this.rootView = rootView;
    this.tvHelloWorld = tvHelloWorld;
  }

  @Override
  @NonNull
  public ConstraintLayout getRoot(a) {
    return rootView;
  }
  
  // ...
}
Copy the code

As you can see from the above section of the code, the Binding class generates all the idled controls and root view controls on the layout, which are then passed in private constructors.

Looking at the rest of the code, there are three static functions that create a Binding object.

public final class LayoutTestBinding implements ViewBinding {
  
  // ...

  @NonNull
  public static LayoutTestBinding inflate(@NonNull LayoutInflater inflater) {
    return inflate(inflater, null.false);
  }

  @NonNull
  public static LayoutTestBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup parent, boolean attachToParent) {
    View root = inflater.inflate(R.layout.layout_test, parent, false);
    if (attachToParent) {
      parent.addView(root);
    }
    return bind(root);
  }

  @NonNull
  public static LayoutTestBinding bind(@NonNull View rootView) {
    // The body of this method is generated in a way you would not otherwise write.
    // This is done to optimize the compiled bytecode for size and performance.
    int id;
    missingId: {
      id = R.id.tv_hello_world;
      TextView tvHelloWorld = rootView.findViewById(id);
      if (tvHelloWorld == null) {
        break missingId;
      }

      return new LayoutTestBinding((ConstraintLayout) rootView, tvHelloWorld);
    }
    String missingId = rootView.getResources().getResourceName(id);
    throw new NullPointerException("Missing required view with ID: ".concat(missingId)); }}Copy the code

Although there are three static functions that create a Binding object, only the bind() function does so, and the other two inflate() functions end up calling the bind() function. Whereas bind() does the simplest findViewById() operation, all three static functions end up going through the findViewById() logic, finding all the controls on the layout with their IDS declared and creating Binding objects.

The nature of the generated Binding class is simply a tool that the compiler automatically generates for us, not something fancy.

Another point that many people miss is that all three static functions that create the Binding class call findViewById() to find all the controls. If you don’t know, you might write the following code:

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
  val binding = ItemTextBinding.bind(holder.itemView)
}
Copy the code

The above usage is not recommended, as onBindViewHolder() calls back frequently, executing a bunch of findViewById() each time. The ViewBinding object is cached in the ViewHolder.

Packaging ideas

Why encapsulate? We use ViewBinding to have a lot of template code, such as:

private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?). {
  super.onCreate(savedInstanceState)
  binding = ActivityMainBinding.inflate(layoutInflater)
  setContentView(binding.root)
}
Copy the code

It is still a bit cumbersome to write every class, so it is necessary to encapsulate the simplified code.

Existing scheme

So how do you encapsulate it? Wrapping a ViewBinding does two things:

  1. Create a ViewBinding object;
  2. Caching ViewBinding objects;

There are only two ways to create a ViewBinding object. The first is to reflect static methods to create a ViewBinding object. The second option is to pass the static function as an argument without reflection, and then execute the higher-order function and call the static function to create a ViewBinding object.

And then there’s the caching scheme, which I mentioned before, the usual scheme is to use the Kotlin attribute delegate, and declare a variable in the attribute delegate class for caching. If you are only doing lazy initialization, you can use lazy {… }, some people will create a custom delegate class, in fact, the function of the official delay delegate is the same, there is no need to write another class. But using fragments not only delays initialization, but also destroys the object in onDestroyView(), allowing you to customize only one property of the delegate class.

All of the above are existing encapsulation schemes, which can be used in common scenarios. However, there are limitations to the caching scheme, namely the use of attribute delegate, which requires the declaration of an attribute. You can declare a binding for activities, Fragments, dialogs, and other inherited classes, but you can’t declare a binding for an existing control unless you write a control’s inherited class, which feels silly.

So in scenarios where attribute delegates are not available, a new way of caching is required.

New train of thought

Let’s start with a hole a man dug for himself. We’ve wrapped an extension to TabLayout to quickly implement a custom tag layout as follows:

TabLayoutMediator(tabLayout, viewPager2) { tab, position ->
  tab.setCustomView<LayoutBottomTabBinding> {
    tvTitle.setText(titleList[position])
    ivIcon.setImageResource(iconList[position])
    ivIcon.contentDescription = titleList[position]
  }
}.attach()
Copy the code

Here the custom layout is only set once at initialization, there is no need to cache the ViewBinding object, and it is not easy to cache. But then someone asked me how to get the ViewBinding object that I set earlier in OnTabSelectedListener, and change the font and icon size.

This is a little awkward, because we can’t change the official code, we can’t use property delegate. When I didn’t think of a good way to save the ViewBinding object, I let it call bind() to get it.

tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
  override fun onTabSelected(tab: TabLayout.Tab) {
    valbinding = tab.customView? .let { LayoutBottomTabBinding.bind(it) }// ...
  }

  override fun onTabUnselected(tab: TabLayout.Tab){}override fun onTabReselected(tab: TabLayout.Tab){}})Copy the code

This listener event is not as frequent as the onBindViewHolder() callback, and the layout is usually just two or three controls. Even if findViewById() were to happen every time you toggle the bottom TAB, you’d be fine.

But the individual has some perfectionism, this problem becomes a knot. Some time ago, I was free to come back and think about what a good way to solve it. After some analysis, we found that only Tab objects can be saved, so let’s check the source code to see if there is any official reserve for us to save things, if not, it will be more difficult to handle. I didn’t expect to find a tag object to save, and no place to call. Wait, doesn’t a View have a tag?

Yes, that’s the new encapsulation idea. Bind () only requires a View to get a ViewBinding. Now that we have a View, we can set the ViewBinding to the View’s tag.

In short, once you have a View, you can get a ViewBinding object and put it together and actually bind it.

Application scenarios

Any ViewHolder gets a Binding object

BaseRecyclerViewAdapterHelper, for example, is due to BaseViewHolder library provides, if you want to increase the binding properties, must write a class inherits BaseViewHolder. However, I previously shared a decorator + extension function encapsulation idea, which can hide the inherited class, looks like a direct “add” attribute.

class FooAdapter : BaseQuickAdapter<Foo, BaseViewHolder>(R.layout.item_foo) {

  override fun onCreateDefViewHolder(parent: ViewGroup, viewType: Int) : BaseViewHolder { 
    return super.onCreateDefViewHolder(parent, viewType).withBinding { ItemFooBinding.bind(it) }
  }
    
  override fun convert(holder: BaseViewHolder, item: Foo) {
    holder.getViewBinding<ItemFooBinding>().tvFoo.text = item.value
  }
}
Copy the code

This is compatible with ViewBinding without changing the original code, but requires rewriting a created function, which is not perfect.

Instead of declaring a Binding object, we get the binding object from the itemView tag. We do not need to inherit the class.

@Suppress("UNCHECKED_CAST")
fun <VB : ViewBinding> BaseViewHolder.getBinding(bind: (View) - >VB): VB =
  itemView.getTag(Int.MIN_VALUE) as? VB ? : bind(itemView).also { itemView.setTag(Int.MIN_VALUE, it) }
Copy the code

Instead of overwriting the created method, you can get the binding directly from the ViewHolder, making it easier to use.

class FooAdapter : BaseQuickAdapter<Foo, BaseViewHolder>(R.layout.item_foo) {
    
  override fun convert(holder: BaseViewHolder, item: Foo) {
    holder.getBinding(ItemFooBinding::bind).tvFoo.text = item.value
  }
}
Copy the code

Ps: as the BaseRecyclerViewAdapterHelper create ViewHolder will reflect once, then create ViewBinding don’t suggest to use reflection.

Optimize the timing of Fragment destruction bindings

The Lifecycle listener is used to release the Binding object in the Fragment property delegate wrapper. Lifecycle and Fragment callbacks are executed in the following order:

Fragment onCreateView
Lifecycle onCreateView
Lifecycle onDestroyView
Fragment onDestroyView
Copy the code

We would normally do this in the Fragment’s onDestroyView().

class SomeFragment: Fragment(R.layout.fragment_some) {
  private val binding by binding(SomeFragment::bind)
  // ...

  override fun onDestroyView(a) {
    super.onDestroyView()
    binding.someView.release()
  }
}
Copy the code

This code will fail because Lifecycle will destroy the Binding object according to the order of execution, and a null pointer will be returned when a binding is obtained on the Fragment’s onDestroyView(). So the Fragment usage I designed earlier requires the release operation to be performed in another interface method.

class SomeFragment: Fragment(R.layout.fragment_some), BindingLifecycleOwner {
  private val binding by binding(SomeFragment::bind)
  // ...
  
  override fun onDestroyViewBinding(a) {
    binding.someView.release()
  }
}
Copy the code

The functionality is fine, but the cost is a little bit higher. Lifecycle can be released without the need to cache variables in the attribute delegate declaration. Lifecycle can be released without using Lifecycle.

fun <VB : ViewBinding> Fragment.binding(bind: (View) - >VB) = FragmentBindingDelegate(bind)

class FragmentBindingDelegate<VB : ViewBinding>(private val bind: (View) -> VB) : ReadOnlyProperty<Fragment, VB> {
  @Suppress("UNCHECKED_CAST")
  override fun getValue(thisRef: Fragment, property: KProperty< * >): VB =
    requireNotNull(thisRef.view) { "The property of ${property.name} has been destroyed." }
      .let { getTag(Int.MIN_VALUE) as? VB ? : bind(this).also { setTag(Int.MIN_VALUE, it) } }
}
Copy the code

In this case, as long as the View exists, the ViewBinding object will exist, and the onDestroyView() Fragment will have no problem releasing it. When the View is destroyed, the ViewBinding is destroyed as well.

Update dynamically added layouts

Suitable for TabLayout update custom tag layout, NavigationView update header layout and other dynamic add layout scenarios. For example, wrap an extension function that updates the header layout of NavigationView:

fun <VB : ViewBinding> NavigationView.updateHeaderView(bind: (View) - >VB, index: Int = 0, block: VB. () - >Unit)= getHeaderView(index)? .let { getTag(Int.MIN_VALUE) as? VB ? : bind(this).also { setTag(Int.MIN_VALUE, it) } }? .run(block)Copy the code
navigationView.updateHeaderView(LayoutNavHeaderBinding::bind) {
  tvNickname.text = nickname
}
Copy the code

Final plan

Finally, a personal wrapped ViewBinding library, ViewBindingKTX. Some of you may already be using it, and it has been upgraded to version 2.0.

The new version has optimized the source code, in order to facilitate the use of some people copy the source code, put a lot of methods in a KT file. With the adaptation of the scene changes, the code will look a bit messy, so upgrade version 2.0 after the code split, convenient for some interested partners to read and learn the source code.

Feature

  • Support for Kotlin and Java usage
  • Supports a variety of uses with and without reflection
  • Support encapsulation to modify its base class to use ViewBinding
  • Support BaseRecyclerViewAdapterHelper
  • Supports activities, fragments, Dialog, and Adapter
  • Supports automatic release of bound class instance objects in Fragment
  • Supports the implementation of custom composite controls
  • PopupWindow creation is supported
  • Support TabLayout to achieve custom label layout
  • NavigationView set header controls are supported
  • Seamless switching of DataBinding is supported

Gradle

To build. Gradle in the root directory add:

allprojects {
    repositories {
        // ...
        maven { url 'https://www.jitpack.io' }
    }
}
Copy the code

Add configurations and dependencies:

Android {buildFeatures {viewBinding true}} dependencies {// Please according to the need to add implementation 'com. Making. DylanCaiCoding. ViewBindingKTX: viewbinding - KTX: 2.0.3' implementation 'com. Making. DylanCaiCoding. ViewBindingKTX: viewbinding nonreflection - KTX: 2.0.3' implementation 'com. Making. DylanCaiCoding. ViewBindingKTX: viewbinding - base: the 2.0.3' implementation 'com. Making. DylanCaiCoding. ViewBindingKTX: viewbinding - brvah: 2.0.3'}Copy the code

New features in version 2.0

The following describes the new functions of version 2.0. You can refer to the usage documents for other scenarios.

Easier adaptation for BRVAH

There is no need to override the method of creating a BaseViewHolder, which is simpler to use than the older version.

Kotlin usage:

class FooAdapter : BaseQuickAdapter<Foo, BaseViewHolder>(R.layout.item_foo) {

  override fun convert(holder: BaseViewHolder, item: Foo) {
    holder.getBinding(ItemFooBinding::bind).tvFoo.text = item.value
  }
}
Copy the code

Java usage:

public class FooAdapter extends BaseQuickAdapter<Foo, BaseViewHolder> {

  public FooAdapter() {
    super(R.layout.item_foo);
  }

  @Override
  protected void convert(@NotNullBaseViewHolder holder, Foo foo) { ItemFooBinding binding = BaseViewHolderUtil.getBinding(holder, ItemFooBinding::bind); binding.tvFoo.setText(foo.getValue()); }}Copy the code

Customize the TabLayout as you like

TabLayout + ViewPager2 + TabLayout + ViewPager2

TabLayoutMediator(tabLayout, viewPager2, false) { tab, position ->
  tab.setCustomView<LayoutBottomTabBinding> {
    tvTitle.text = getString(tabs[position].title)
    ivIcon.setImageResource(tabs[position].icon)
    tvTitle.contentDescription = getString(tabs[position].title)
  }
}.attach()
Copy the code

Now, increased TabLayout updateCustomTab < VB > {… } methods can update custom layouts, such as displaying a small red dot on the second TAB after receiving a message:

viewModel.unreadCount.observe(this) { count ->
  tabLayout.updateCustomTab<LayoutBottomTabBinding>(1) {
    ivUnreadState.isVisible = count > 0}}Copy the code

Can also use TabLayout. DoOnCustomTabSelected < VB > (…). Listen for click events, such as clicking on the second TAB and updating the number of unread hidden red dots:

tabLayout.doOnCustomTabSelected<LayoutBottomTabBinding>(
  onTabSelected = { tab ->
    if (tab.position == 1) {
      viewModel.unreadCount.value = 0}})Copy the code

It’s easy to add unread numbers or switch animations, and you can basically implement the bottom navigation bar of any layout.

Quick implementation of simple lists

The ViewHolder was wrapped and the base class of the adapter was not provided, considering that the list adapters used were different. But not having an adapter base class isn’t very convenient, so the viewbinding-base dependency adds the ability to quickly implement simple lists.

This is a ListAdapter implementation based on RecyclerView (note not the ListAdapter of ListView). For those of you who haven’t used it before, ListAdapter is an official DiffUtil encapsulated adapter. It is much easier to set up the latest data to automatically execute the changed animation.

Before using ListAdapter, we need to write a class that inherits DiffUtil.ItemCallback

and implements two methods, one comparing whether the items are the same and the other comparing whether the contents are the same. Personally, I prefer to write this class in the corresponding entity class, which is easy to reuse in ListAdapter.

data class Message(
  val id: String,
  val content: String
) {
  class DiffCallback : DiffUtil.ItemCallback<Message>() {
    override fun areItemsTheSame(oldItem: Message, newItem: Message) = oldItem.id == newItem.id
    override fun areContentsTheSame(oldItem: Message, newItem: Message) = oldItem == newItem
  }
}
Copy the code

You can then write a class that inherits the ListAdapter, passing the diffUtil.itemCallback

in the constructor, and otherwise writing an Adapter.

class MessageAdapter : ListAdapter<Message, MessageAdapter.ViewHolder>(Message.DiffCallback()) {

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
    ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_message, parent, false))

  override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    holder.tvContent.text = getItem(position).content
  }

  class ViewHolder(root: View) : RecyclerView.ViewHolder(root) {
    val tvContent: TextView = root.findViewById(R.id.tv_content)
  }
}
Copy the code

After sending the list data, the update animation is automatically performed, without calling insert, remove, etc.

adapter.submitList(newList)
Copy the code

This library encapsulates a SimpleListAdapter

class with ListAdapter and ViewBinding to simplify usage.
,>

class MessageAdapter : SimpleListAdapter<Message, ItemMessageBinding>(Message.DiffCallback()) {

  override fun onBindViewHolder(binding: ItemMessageBinding, item: Message, position: Int) {
    binding.tvContent.text = item.content
  }
}
Copy the code

If the adapter does not need to be reused, simpleListAdapter

(callback) {… } delegate methods to create adapters without having to specifically declare a class.
,>

private val adapter by simpleListAdapter<Message, ItemMessageBinding>(Message.DiffCallback()) { item ->
  tvContent.text = item.content
}
Copy the code

SimpleXXXListAdapter

{simpleXXXListAdapter

{… }, which also provides the corresponding SimpleXXXListAdapter

base class.


private val adapter by simpleStringListAdapter<ItemFooBinding> {
  textView.text = it
}
Copy the code

Supports setting click events and long press events:

adapter.doOnItemClick { item, position ->
  // Click the event
}
adapter.doOnItemLongClick { item, position ->
  // Long press event
}
Copy the code

The viewholder.getBinding

() extension can be used for other adapters.

Support PopupWindow

private val popupWindow by popupWindow<LayoutPopupBinding> {
// private val popupWindow by popupWindow(LayoutPopupBinding::inflate) {btnLike.setOnClickListener { ... }}Copy the code

If you need any ViewBinding usage scenarios, please mention Issues. I will try my best to meet them.

conclusion

This article explains the nature of ViewBinding, which is essentially a tool for findViewById(). A new wrapper idea is shared below, which adds some scenarios for getting binding objects on controls. It is easy to use ViewBinding in BRVAH. Finally, ViewBindingKTX library is shared. TabLayout and list functions added in version 2.0 are introduced. It is more convenient to use. If you feel helpful, I hope you can click a star to support yo ~ I will share more encapsulation related articles to you later.