封装和使用 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 对象是给 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); }
|