当前位置: 首页 > 工具软件 > Rounded View > 使用案例 >

ViewBinding数据绑定这一篇就够了

皇甫乐
2023-12-01
引言: 接触Android时,finViewById的写法估计是最让人头疼的了,一个类上来bulabula一堆重复代码,所幸后来JakeWharton开源ButterKnife优化了写法,但是还是要写上一大堆重复代码,看着都令人心塞。解铃还须系铃人,google推出的viewbinding以及大力支持的kotlin中的kitlin android extions包都对这个做出了优化。
与findViewById的区别
  • Null 安全:由于视图绑定会创建对视图的直接引用,因此不存在因视图 ID 无效而引发 Null 指针异常的风险。
    此外,如果视图仅出现在布局的某些配置中,则绑定类中包含其引用的字段会使用 @Nullable 标记。
  • 类型安全:每个绑定类中的字段均具有与它们在 XML 文件中引用的视图相匹配的类型。这意味着不存在发生类转换异常的风险。
  • 这些差异意味着布局和代码之间的不兼容将会导致构建在编译时(而非运行时)失败。更方便写出健壮的代码。
与数据绑定的对比(databinding)

视图绑定和数据绑定均会生成可用于直接引用视图的绑定类。但是,视图绑定旨在处理更简单的用例,与数据绑定相比,具有以下优势:

  • 更快的编译速度:视图绑定不需要处理注释,因此编译时间更短。
  • 易于使用:视图绑定不需要特别标记的 XML 布局文件,因此在应用中采用速度更快。在模块中启用视图绑定后,它会自动应用于该模块的所有布局。
    反过来,与数据绑定相比,视图绑定也具有以下限制:
  • 视图绑定不支持布局变量或布局表达式,因此不能用于直接在 XML 布局文件中声明动态界面内容。
  • 视图绑定不支持双向数据绑定。

提起databinding估计大家第一印象就是复杂麻烦难调试,如果不是为了更好的双向绑定一般都不会考虑。

Kotlin Android Extensions 和 ViewBinding 用哪个?
使用条件
  • 第一个肯定需要使用kotlin进行开发,有个小问题需要注意一下,UI的更新必须在onCreateView方法之后,当然也可以在此方法,不过调用稍微复杂一点,一般都是在onViewCreate方法里进行刷新UI。
  • ViewBinding支持Java和Kotlin。
简单程度
  • 如果基于Kotlin语言开发,那android extension不需要任何声明,拿来即用,很是方便。
  • 如果基于Java语言开发,那么ViewBinding是你的不二之选了。
个人感觉ViewBinding就是为了Java而生的,一旦用了Kotlin,Ktx感觉更方便实用。
ButterKnife已经逐步淘汰了,JK大佬的仓库已经说明,再次印证了Java开发的朋友需要拥抱一下ViewBinding了。

Attention:
This tool is now deprecated. Please switch to view binding. Existing versions will continue to work,
obviously, but only critical bug fixes for integration with AGP will be considered.
Feature development and general bug fixes have stopped.
// 该工具已经废弃,请切换至ViewBinding。现有版本继续工作,只会修复严重bug,其他随意了。

接下来将从Java和Kotlin的写法来展示一下ViewBinding的用法,最后讲一下如何封装BaseActivity,BaseFragment及BaseAdapter等。
使用

通过视图绑定功能,您可以更轻松地编写可与视图交互的代码。在模块中启用视图绑定之后,系统会为该模块中的每个 XML 布局文件生成一个绑定类。
绑定类的实例包含对在相应布局中具有 ID 的所有视图的直接引用。

设置说明
- 注意:视图绑定在 Android Studio 3.6 Canary 11 及更高版本中可用。记得升级垃圾软件哦。
- 目前我们的项目一般都是分模块的,视图绑定功能也支持按模块启用,想在哪个模块启用就直接在该模块的build.gradle
文件中添加以下代码。

android {
        // android块下添加
        viewBinding {
            enabled = true
        }
    }
    
  • 如果您希望在生成绑定类时忽略某个布局文件,请将 tools:viewBindingIgnore=“true” 属性添加到相应布局文件的根视图中:
<LinearLayout
            
            tools:viewBindingIgnore="true" >
        ...
    </LinearLayout>
    

用法
为某个模块启用视图绑定功能后,系统会为该模块中包含的每个 XML 布局文件生成一个绑定类。每个绑定类均包含对根视图以及具有 ID 的所有视图的引用。
系统会通过以下方式生成绑定类的名称:将 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 根视图。

以下几个部分介绍了生成的绑定类在 Activity 和 Fragment 中的使用。

在 Activity 中使用视图绑定
如需设置绑定类的实例以供 Activity 使用,请在 Activity 的 onCreate() 方法中执行以下步骤:
- 调用生成的绑定类中包含的静态 inflate() 方法。此操作会创建该绑定类的实例以供 Activity 使用。
- 通过调用 getRoot() 方法或使用 Kotlin 属性语法获取对根视图的引用。
- 将根视图传递到 setContentView(),使其成为屏幕上的活动视图。
Java

class ResultProfileActivity extends AppCompatActivity {
    private ResultProfileBinding binding;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       binding = ResultProfileBinding.inflate(getLayoutInflater());
       View view = binding.getRoot();
       setContentView(view);
   }     
}
    

Kotlin

class ResultProfileActivity : AppCompatActivity() {
    private lateinit var binding: ResultProfileBinding

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

您现在即可使用该绑定类的实例来引用任何视图:

    binding.getName().setText(viewModel.getName());
    binding.button.setOnClickListener(new View.OnClickListener() {
        viewModel.userClicked()
    });
    binding.name.text = viewModel.name
    binding.button.setOnClickListener { viewModel.userClicked() }

在 Fragment 中使用视图绑定
如需设置绑定类的实例以供 Fragment 使用,请在 Fragment 的 onCreateView() 方法中执行以下步骤:
- 调用生成的绑定类中包含的静态 inflate() 方法。此操作会创建该绑定类的实例以供 Fragment 使用。
- 通过调用 getRoot() 方法或使用 Kotlin 属性语法获取对根视图的引用。
- 从 onCreateView() 方法返回根视图,使其成为屏幕上的活动视图。
Java

class ResultProfileFragment extends Fragment {
    private ResultProfileBinding binding;

    @Override
    public View onCreateView (LayoutInflater inflater,
                              ViewGroup container,
                              Bundle savedInstanceState) {
        binding = ResultProfileBinding.inflate(inflater, container, false);
        View view = binding.getRoot();
        return view;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }
}
    

Kotlin

class ResultProfileFragment : Fragment() {
    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
    }
} 

您现在即可使用该绑定类的实例来引用任何视图:

    binding.getName().setText(viewModel.getName());
    binding.button.setOnClickListener(new View.OnClickListener() {
        viewModel.userClicked()
    });
    binding.name.text = viewModel.name
    binding.button.setOnClickListener { viewModel.userClicked() }

以上简单介绍了应用,细心的小伙伴说了,我应用中多个Activity和Fragment都需要写一遍这个吗,那不是炸了吗。

不要着急,你要的封装在下面。

思考一下我们能不能直接在BaseXXX中将binding给初始化出来,然后在对应的子界面可以直接使用,安排。
因为项目中使用的是kotlin,懒得转换成java了,参考kotlin的实现,java手到擒来,如果没调通可以留言,后续更新。
针对BaseActivity

abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {

    protected lateinit var binding: VB

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val type = javaClass.genericSuperclass
        if (type is ParameterizedType) {
            val clazz = type.actualTypeArguments[0] as Class<VB>
            val method = clazz.getMethod("inflate", LayoutInflater::class.java)
            binding = method.invoke(null, layoutInflater) as VB
            setContentView(binding.root)
        }
        onCreated(savedInstanceState)
    }

    abstract fun onCreated(savedInstanceState: Bundle?)
}

那么在具体的Activity中可以直接使用:

class MainActivity : FullActivity<ActivityMainBinding>() {

    override fun onCreated(savedInstanceState: Bundle?) {
        // xxx就是你在activity_main布局中定义的属性名
        binding.xxx.text = "123"        
    }
}

针对BaseFragment

abstract class BaseFragment<out VB : ViewBinding> : Fragment {
    private var _binding: VB? = null
    val binding: VB get() = _binding!!
    
constructor() : super()

    @ContentView
    constructor(@LayoutRes contentLayoutId: Int) : super(contentLayoutId)

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        //利用反射,调用指定ViewBinding中的inflate方法填充视图
        val type = javaClass.genericSuperclass
        val clazz = (type as ParameterizedType).actualTypeArguments[0] as Class<VB>
        val method = clazz.getMethod(
            "inflate",
            LayoutInflater::class.java,
            ViewGroup::class.java,
            Boolean::class.java
        )
        _binding = method.invoke(null, layoutInflater, container, false) as VB

        return _binding!!.root
    }

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

针对BaseAdapter

abstract class BaseRecyclerviewAdapter<VB : ViewBinding, M> :
    RecyclerView.Adapter<BaseRecyclerviewAdapter.ViewHolder>() {

    protected var data: List<M>? = null
    protected lateinit var binding: VB

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val type = javaClass.genericSuperclass
        val clazz = (type as ParameterizedType).actualTypeArguments[0] as Class<VB>
        val method = clazz.getMethod(
            "inflate",
            LayoutInflater::class.java,
            ViewGroup::class.java,
            Boolean::class.java
        )
        binding = method.invoke(null, LayoutInflater.from(parent.context), parent, false) as VB
        return ViewHolder(binding.root)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.setIsRecyclable(false)
        bindData(binding, data!![position],position)
    }

    override fun getItemCount(): Int {
        return data?.size ?: 0
    }

    fun setMoreData(newData: List<M>) {
        data = newData
        notifyDataSetChanged()
    }

    abstract fun bindData(binding: VB, item: M,position: Int)


    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)    
}

针对BaseViewGroup

abstract class BaseViewGroup<out VB : ViewBinding>(
    context: Context,
    attr: AttributeSet? = null,
    def: Int = 0
) : FrameLayout(context, attr, def) {

    protected val binding: VB by lazy {
        val type = javaClass.genericSuperclass
        val clazz = (type as ParameterizedType).actualTypeArguments[0] as Class<VB>
        val method = clazz.getMethod(
            "inflate",
            LayoutInflater::class.java,
            ViewGroup::class.java,
            Boolean::class.java
        )
        method.invoke(null, LayoutInflater.from(context), this, true) as VB
    }

    init {
        binding.root
    }

}

愉快的时光总是这么短暂,拜拜了各位。有需要请移步公号"程序员指北",虽然我也不经常写,但不影响你关注哈哈。

 类似资料: