ViewBinding使用入门

ViewBinding

参考资料:
新技术 ViewBinding 最佳实践 & 原理击穿
更多 ViewBinding 的封装思路

1. kotlin-android-extensions(KAE) 的问题

根据Google官方的说法, KAE存在以下问题:

  • 污染全局命名空间
  • 不能暴露可空性信息
  • 仅支持Kotlin代码

kotlin在1.4.20中 开始废弃这个库了(https://blog.jetbrains.com/kotlin/2020/11/kotlin-1-4-20-released/#Deprecation_of_Kotlin_Android_Extensions)
ReleaseNote中的原因是 KAT所填补的空白已经被ViewBinding所代替了, 所以他们不再支持了…

2. ViewBinding的使用

1. 开启ViewBinding

在model的build.gradle 中开启

android {
    viewBinding {
        enabled = true
    }
}

之后每个xml布局都会生成对应ViewBinding类

如果想要忽略摸个布局文件, 可以添加tools:viewBindingIgnore="true"属性

<LinearLayout
            ...
            tools:viewBindingIgnore="true" >
        ...
    LinearLayout>

2. 基本用法

生成的ViewBinding类使用以下方式的命名规则:将 XML 文件的名称转换为驼峰式大小写,并在末尾添加“Binding”一词

例如, 有如下布局 名称为result_profile.xml:

<LinearLayout ... >
        <TextView android:id="@+id/name" />
        <ImageView android:cropToPadding="true" />
        <Button android:id="@+id/button"
            android:background="@drawable/rounded_button" />
LinearLayout>

所生成的绑定类的名称就为 ResultProfileBinding。此类具有两个字段:一个是名为 name 的 TextView,另一个是名为 button 的 Button。该布局中的 ImageView 没有 ID,因此绑定类中不存在对它的引用。

每个绑定类还包含一个 getRoot() 方法,用于为相应布局文件的根视图提供直接引用。在此示例中,ResultProfileBinding 类中的 getRoot() 方法会返回 LinearLayout 根视图。

每一个Binding类都提供了3中创建ViewBinding对象的方式, 可以根据情况选择:

ViewBinding使用入门_第1张图片

位置:
ViewBinding使用入门_第2张图片

ViewBinding的用法基本上都是在围绕着上面的3个方法

2.1 在Activity中只用

  1. 调用生成的绑定类中包含的静态 inflate() 方法。此操作会创建该绑定类的实例以供 Activity 使用。
  2. 通过调用 getRoot() 方法或使用 Kotlin 属性语法获取对根视图的引用。
  3. 将根视图传递到 setContentView(),使其成为屏幕上的活动视图。
    private lateinit var binding: ResultProfileBinding

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

然后就可以使用对应的View了

    binding.name.text = viewModel.name
    binding.button.setOnClickListener { viewModel.userClicked() }

2.2 在Fragment中使用

  1. 调用生成的绑定类中包含的静态 inflate() 方法。此操作会创建该绑定类的实例以供 Fragment 使用。
  2. 通过调用 getRoot() 方法或使用 Kotlin 属性语法获取对根视图的引用。
  3. 从 onCreateView() 方法返回根视图,使其成为屏幕上的活动视图。
    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
    }
    

2.3 在Adapter中使用ViewBinding

在Adapter中使用ViewBinding与之前没有什么不同

class SimpleDataAdapter : RecyclerView.Adapter<SimpleDataAdapter.ViewHolder>() {
    var dataList: List<Data> = emptyList()
        set(value) {
            field = value
            notifyDataSetChanged()
        }

    inner class ViewHolder(binding: ItemDataBinding) : RecyclerView.ViewHolder(binding.root) {
        val dataIv = binding.dataIv
        val dataTv = binding.dataTv
    }

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

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        dataList[position].let {
            holder.dataIv.setImageResource(it.img)
            holder.dataTv.text = it.txt
        }
    }

    override fun getItemCount(): Int = dataList.size
}

2.4 对引入布局使用ViewBinding

2.4.1 include

比如有如下布局: titlebar.xml 希望作为一个通用布局引入到其他布局中


<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <Button
        android:id="@+id/back"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:text="Back" />

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Title"
        android:textSize="20sp" />

    <Button
        android:id="@+id/done"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:layout_centerVertical="true"
        android:text="Done" />

RelativeLayout>

activity的布局:


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <include
        android:id="@+id/titleBar"
        layout="@layout/titlebar" />
LinearLayout>

Activity中使用

class IncludeActivity : AppCompatActivity() {
    private lateinit var binding: ActivityIncludeBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityIncludeBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.titleBar.title.text = "Title"
        binding.titleBar.back.setOnClickListener {
        }
        binding.titleBar.done.setOnClickListener {
        }
    }

}

这里可以看到 binding中根据id生成了一个titleBar的对象, 查看ViewBinding生成的代码可知, 这个titleBar还是一个ViewBinding的类:

ViewBinding使用入门_第3张图片

2.4.2 merge

如果被引入的布局根标签是merge, 则不能使用上述方式了, 疑问merge会将被引入的布局直接合并到对应的位置, 所以在ViewBinding中, 通过id找对应的ViewBinding的过程中会失败
ViewBinding使用入门_第4张图片

相较于2.4.1的代码:

  1. 删除include标签上的id属性
  2. 在kotlin代码中, 自己赋值ViewBinding对象:
class MergeActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMergeBinding
    private lateinit var titlebarBinding: TitlebarMergeBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMergeBinding.inflate(layoutInflater)
        setContentView(binding.root)
        titlebarBinding = TitlebarMergeBinding.bind(binding.root)
        titlebarBinding.title.text = "Title"
        titlebarBinding.back.setOnClickListener {
        }
        titlebarBinding.done.setOnClickListener {
        }
    }

}

3. 封装

封装的主要思路是尽量不写,或少写一些模板代码, 主要的思路就是在创建ViewBinding对象上实现自动创建

3.1 Activity

open class BaseReflectionAty<VB : ViewBinding> : AppCompatActivity() {
    protected lateinit var binding: VB
        private set

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        generateViewBinding()
        if (this::binding.isInitialized) {
            setContentView(binding.root)
            binding.initViews()
        }
    }

    open fun VB.initViews() {}

    /**
     * 生成ViewBinding
     */
    private fun generateViewBinding() {
        val type = this::class.java.genericSuperclass as? ParameterizedType ?: return
        val vbClazz = type.actualTypeArguments.find {
            // 找到泛型声明为 实现了 ViewBinding接口的类型
            (it as Class<*>).genericInterfaces[0] == ViewBinding::class.java
        } as? Class<*> ?: return

        val method = vbClazz.getMethod("inflate", LayoutInflater::class.java)
        binding = method.invoke(null, layoutInflater) as VB
    }
}

在onCreate时, 通过generateViewBinding方法来生成对应的ViewBinding对象,使用时:

class MainActivity : BaseReflectionAty<ActivityMainBinding>() {
    override fun ActivityMainBinding.initViews() {
        mainTextView.setOnClickListener {
            Toast.makeText(this@MainActivity, "lalala", Toast.LENGTH_SHORT).show()
        }
    }
}

直接在initViews中使用布局文件中东的View即可

3.2 Fragment

Fragment的思路与Activity相同

class BaseReflectionFragment<VB : ViewBinding> : Fragment() {
    private var _binding: VB? = null
    protected val binding: VB
        get() = _binding!!


    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        generateViewBinding(container)

        if (_binding != null) {
            return binding.root
        }
        return super.onCreateView(inflater, container, savedInstanceState)
    }

    open fun VB.initViews() {}

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        _binding?.initViews()
    }

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

    /**
     * 生成ViewBinding
     */
    private fun generateViewBinding(rootView: ViewGroup?) {
        val type = this::class.java.genericSuperclass as? ParameterizedType ?: return
        val vbClazz = type.actualTypeArguments.find {
            // 找到泛型声明为 实现了 ViewBinding接口的类型
            (it as Class<*>).genericInterfaces[0] == ViewBinding::class.java
        } as? Class<*> ?: return

        val method = vbClazz.getMethod(
            "inflate",
            LayoutInflater::class.java,
            ViewGroup::class.java,
            Boolean::class.java
        )
        _binding = method.invoke(null, layoutInflater, rootView, false) as VB
    }
}

3.3 Adapter

ViewHolder:

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

Adapter:

abstract class ViewBindingAdapter<VB : ViewBinding> : RecyclerView.Adapter<BaseViewHolder<VB>>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder<VB> {
        generateViewBinding(parent)?.let {
            return BaseViewHolder(it)
        } ?: TODO("Not yet implemented")
    }

    private fun generateViewBinding(parent: ViewGroup): VB? {
        val type = this::class.java.genericSuperclass as? ParameterizedType ?: return null
        val vbClazz = type.actualTypeArguments.find {
            // 找到泛型声明为 实现了 ViewBinding接口的类型
            (it as Class<*>).genericInterfaces[0] == ViewBinding::class.java
        } as? Class<*> ?: return null
        val layoutInflater = LayoutInflater.from(parent.context)

        val method = vbClazz.getMethod(
            "inflate",
            LayoutInflater::class.java,
            ViewGroup::class.java,
            Boolean::class.java
        )
        return method.invoke(null, layoutInflater, parent, false) as VB
    }
}

你可能感兴趣的:(Android人生,android,kotlin)