封装和使用ViewBinding

封装和使用 ViewBinding,该替代 Kotlin synthetic 和 ButterKnife

ViewBinding 的基础用法

首先要在 module 的 build.gradle 文件配置开启 ViewBinding:

1
2
3
4
5
6
7
8
9
10
android {
...
viewBinding {
enabled = true
}

buildFeatures {
viewBinding true
}
}

这样该模块下每个 XML 文件都生成一个对应的绑定类,每个绑定类会包含根视图以及具有 ID 的所有视图的引用。绑定类的命名是:将 XML 文件的名称转换为驼峰命名,并在末尾添加 “Binding” 。

比如现在有 activity_app.xml 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".AppActivity">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Heihei"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

如果不想生成某个布局的绑定类,可以在根视图添加 tools:viewBindingIgnore=”true” 属性。

那这个绑定类的对象怎么实例化呢?该类会生成相关的 inflate 静态方法,调用该方法即可获得绑定对象。

1
2
3
4
5
6
7
8
9
10
class AppActivity : AppCompatActivity() {

private lateinit var binding: ActivityAppBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAppBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}

在 Fragment 使用有点不同,由于 Fragment 的存在时间比其视图长,需要在 onDestroyView() 方法中清除对绑定类实例的所有引用,所以写起来会有点麻烦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class HomeFragment : Fragment() {
private var _binding: HomeFragmentBinding? = null
private val binding get() = _binding!!

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

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.tvHelloWorld.text = "Hello Android!"
}

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

还有在 Adapter 的使用,因为布局不是只创建一次,而是每有一项数据就会创建,不能像上面那样在 Adapter 里写一个 binding 全局变量,这样 binding 只会得到最后一次创建的视图。所以 binding 对象应该是给 ViewHolder 持有。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TextAdapter(
private val list: List<String>
) : RecyclerView.Adapter<TextAdapter.TextViewHolder>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TextViewHolder {
val binding = ItemTextBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return TextViewHolder(binding)
}

override fun onBindViewHolder(holder: TextViewHolder, position: Int) {
val content = list[position]
holder.binding.tvContent.text = content
}

override fun getItemCount() = list.size

class TextViewHolder(val binding : ItemTextBinding) : RecyclerView.ViewHolder(binding.root)
}

封装方法

1
2
3
4
5
6
7
8
9
10
11
inline fun <reified VB : ViewBinding> Activity.inflate() = lazy {
inflateBinding<VB>(layoutInflater).apply { setContentView(root) }
}

inline fun <reified VB : ViewBinding> Dialog.inflate() = lazy {
inflateBinding<VB>(layoutInflater).apply { setContentView(root) }
}

@Suppress("UNCHECKED_CAST")
inline fun <reified VB : ViewBinding> inflateBinding(layoutInflater: LayoutInflater) =
VB::class.java.getMethod("inflate", LayoutInflater::class.java).invoke(null, layoutInflater) as VB

Activity 的用法,在 Dialog 使用是类似的。

Fragment 的封装不一样,首先生成方法 bind(),只需传个 View。另外还需要释放 binding 对象,不能用延时委托改用属性委托

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
inline fun <reified VB : ViewBinding> Fragment.bindView() =
FragmentBindingDelegate(VB::class.java)

inline fun Fragment.doOnDestroyView(crossinline block: () -> Unit) =
viewLifecycleOwner.lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroyView() {
block.invoke()
}
})

class FragmentBindingDelegate<VB : ViewBinding>(
private val clazz: Class<VB>
) : ReadOnlyProperty<Fragment, VB> {

private var binding: VB? = null

@Suppress("UNCHECKED_CAST")
override fun getValue(thisRef: Fragment, property: KProperty<*>): VB {
if (binding == null) {
binding = clazz.getMethod("bind", View::class.java)
.invoke(null, thisRef.requireView()) as VB
thisRef.doOnDestroyView { binding = null }
}
return binding!!
}
}

如果还有其它释放操作要在 binding 销毁前执行,需要写在 doOnDestroyView() 方法里

1
2
3
4
5
6
7
8
9
10
11
12
class HomeFragment : Fragment(R.layout.fragment_home) {

private val binding: FragmentHomeBinding by bindView()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.tvHelloWorld.text = "Hello Android!"
doOnDestroyView {
// 在 binding 对象销毁前进行释放操作
}
}
}

列表的封装,binding 对象是给 ViewHolder 持有,所以 BindingViewHolder 来接收 binding

1
class BindingViewHolder<VB : ViewBinding>(val binding: VB) : RecyclerView.ViewHolder(binding.root)

反射进行实例化

1
2
3
4
5
inline fun <reified T : ViewBinding> newBindingViewHolder(parent: ViewGroup): BindingViewHolder<T> {
val method = T::class.java.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)
val binding = method.invoke(null, LayoutInflater.from(parent.context), parent, false) as T
return BindingViewHolder(binding)
}

onCreateViewHolder 调用封装的方法创建 BindingViewHolder 对象,然后在 onBindViewHolder 方法通过 holder 持有的 binding 拿到控件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TextAdapter(
private val list: List<String>
) : RecyclerView.Adapter<BindingViewHolder<ItemTextBinding>>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
newBindingViewHolder<ItemTextBinding>(parent)

override fun onBindViewHolder(holder: BindingViewHolder<ItemTextBinding>, position: Int) {
val content = list[position]
holder.binding.tvContent.text = content
}

override fun getItemCount() = list.size
}

基类封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class BaseBindingActivity<VB extends ViewBinding> extends AppCompatActivity {

private VB binding;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ViewBindingUtil.inflateWithGeneric(this, getLayoutInflater());
setContentView(binding.getRoot());
}

public VB getBinding() {
return binding;
}
}

class MainActivity extends BaseBindingActivity<ActivityMainBinding> {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getBinding().tvHelloWorld.setText("Hello Android!");
}
}

混淆配置

1
2
3
4
5
-keepclassmembers class * implements androidx.viewbinding.ViewBinding {
public static * inflate(android.view.LayoutInflater);
public static * inflate(android.view.LayoutInflater, android.view.ViewGroup, boolean);
public static * bind(android.view.View);
}