视图绑定(ViewBinding )与数据绑定(Databinding)

3,317 阅读8分钟

视图绑定(ViewBinding )与数据绑定(Databinding)

什么是ViewBinding

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

用法

如何配置Viewbinding

在每一个模块中的build.gradle中进行如下配置

 android {
         ...
         viewBinding {
             enabled = true
         }
     }
     

为某个模块启用视图绑定功能后,系统会为该模块中包含的每个 XML 布局文件生成一个绑定类。每个绑定类均包含对根视图以及具有 ID 的所有视图的引用。系统会通过以下方式生成绑定类的名称:将 XML 文件的名称转换为驼峰式大小写,并在末尾添加“Binding”一词。

viewbinding在Activity中使用

例子:假设有一个布局是 activity_main.xml

 <LinearLayout 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"
     android:background="@color/white"
     tools:context=".MainActivity">
 ​
     <Button
         android:text="这是按钮"
         android:id="@+id/test_view_binding"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"/>
 ​
 </LinearLayout>

在Activity中,viewBinding的用法如下:

 class MainActivity : Activity() {
 ​
     //首先声明变量
     private lateinit var binding: ActivityMainBinding
     
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         
         //再通过生成的binding去加载该布局
         binding = ActivityMainBinding.inflate(layoutInflater)
         
         //调用Binding类的getRoot()函数可以得到activity_main.xml中根元素的实例
         val view = binding.root
         
         //将根视图传递到 setContentView(),使其成为屏幕上的活动视图
         setContentView(view)
         
         //使用binding实例获取到控件
         binding.testViewBinding.text = "button"
 ​
     }
 ​
 }

viewbinding在Fragment中使用

在fragment使用viewBinding,以下是官网的一个例子

     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
     }
     

这里做一下解释为什么这里实现起来稍微有点奇怪。首先kotlin是有空安全的属性的,所以直接使用_binding这个属性是不可以的。那为什么不使用和activity中的那种通过声明lateinit变量的方式来实现呢?来看看上述官网的例子里面的一个注释 ,这句话翻译过来就是这个binding变量只有在onCreateView与onDestroyView才是可用的。因为我们fragment的生命周期和activity的不同,fragment 可以超出其视图的生命周期,如果不将这里置为空,有可能引起内存泄漏。

viewbinding含有include标签中布局的使用

title_bar.xml

 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:orientation="vertical" android:layout_width="match_parent"
     android:layout_height="match_parent">
     <Button
         android:text="include"
         android:id="@+id/test_include"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"/>
 ​
 </LinearLayout>

activity_main.xml

 <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout 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"
     android:background="@color/white"
     tools:context=".MainActivity">
     
     <!--在include标签中添加id属性-->
     <include
         android:id="@+id/title_bar"
         layout="@layout/title_bar"/>
     ...
 </LinearLayout>

在activity中使用包含include标签的布局

 class MainActivity : Activity() {
     //首先声明变量
     private lateinit var binding: ActivityMainBinding
 ​
     override fun onCreate(savedInstanceState: Bundle?) {
        ...
         //直接使用include标签的id,然后再根据include的id引用include布局里面的id
         binding.titleBar.testInclude.text = "hello"
     }
 }

viewbinding含有merge的标签的布局中使用

首先是要吧include标签里面的id去掉,不然会直接报错。

 <merge xmlns:android="http://schemas.android.com/apk/res/android">
 ​
     <Button
         android:text="include"
         android:id="@+id/test_include"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"/>
 ​
 </merge>

接着是在activity中的使用

 private lateinit var binding: ActivityMainBinding
 //声明变量
 private lateinit var titleBarBinding: TitleBarBinding
 override fun onCreate(savedInstanceState: Bundle?) {
     super.onCreate(savedInstanceState)
     binding = ActivityMainBinding.inflate(layoutInflater)
 ​
     //调用TitleBarBind的bind函数让title_bar.xml和我们的activity_main.xml关联起来
     titleBarBinding = TitleBarBinding.bind(binding.root)
     val view = binding.root
     setContentView(view)
     //直接使用titlrBarBinding变量引用控件
     titleBarBinding.testInclude.text = "button"
 ​
 }

viewbinding含有Adapter中的使用

 class BindingAdapter(val mData:List<String>): RecyclerView.Adapter<BindingAdapter.MyHolder>() {
     //Myholder接受RvItemBinding参数,RecyclerView.ViewHolder接受的是一个View,通过这个binding.root返回一个root
     inner class MyHolder(binding: RvItemBinding):RecyclerView.ViewHolder(binding.root){
         val textView = binding.textView
     }
 ​
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyHolder {
         //首先调用RvItemBinding的inflate函数去加载rv_item.xml的布局
         val binding = RvItemBinding.inflate(LayoutInflater.from(parent.context),parent,false)
         return MyHolder(binding)
     }
 ​
     override fun getItemCount() = mData.size
     
     override fun onBindViewHolder(holder: MyHolder, position: Int) {
         //通过holder直接使用它自己的成员变量
         holder.textView.text = mData[position]
     }
 }

使用viewBinding能为我们带来什么好处

使用viewBinding比传统findViewById的好处

1.类型安全:不用担心出现类型转换的错误

2.写法方便,不用写很多声明的代码,使得Activity里面的代码更加整洁

使用viewBinding比synthetic的好处

synthetic算法是android提供的一个插件实现的。通过在build.gradle添加 apply plugin: 'kotlin-android-extensions'就可以引用。

 import androidx.appcompat.app.AppCompatActivity
 import android.os.Bundle
 import kotlinx.android.synthetic.main.activity_synthetic.*
 ​
 class SyntheticActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_synthetic)
         //直接使用synthetic_button就可以使用控件
         synthetic_button.text = "hello"
     }
 }

但是这个插件已经不被推荐使用了。

既然这个插件这么好用,为什么会被谷歌不推荐使用呢?谷歌也没有写明。我们直接将上面的代码反编译成java代码看看。得到如下代码

 package com.eebbk.mvvmlearn;
 ​
 import android.os.Bundle;
 import android.view.View;
 import android.widget.Button;
 import androidx.appcompat.app.AppCompatActivity;
 import com.eebbk.mvvmlearn.R.id;
 import java.util.HashMap;
 import kotlin.Metadata;
 import kotlin.jvm.internal.Intrinsics;
 import org.jetbrains.annotations.Nullable;
 ​
 public final class SyntheticActivity extends AppCompatActivity {
     //新增一个成员变量
    private HashMap _$_findViewCache;
 ​
    protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       this.setContentView(1300109);
       Button var10000 = (Button)this._$_findCachedViewById(id.synthetic_button);
       Intrinsics.checkExpressionValueIsNotNull(var10000, "synthetic_button");
       var10000.setText((CharSequence)"hello");
    }
 ​
     //通过命名一个奇怪的函数名来避免与用户声明的函数重复,并通过这个函数找到我们的view
    public View _$_findCachedViewById(int var1) {
       if (this._$_findViewCache == null) {
          this._$_findViewCache = new HashMap();
       }
 ​
       View var2 = (View)this._$_findViewCache.get(var1);
       if (var2 == null) {
          var2 = this.findViewById(var1);
          this._$_findViewCache.put(var1, var2);
       }
 ​
       return var2;
    }
 ​
    public void _$_clearFindViewByIdCache() {
       if (this._$_findViewCache != null) {
          this._$_findViewCache.clear();
       }
 ​
    }
 }

可以看到这里是新增一个成员变量来帮助我们实现findViewById的功能。这无形中增加了我们的内存开销。这是其中一点,还有一点就是它提高了我们程序的不稳定性。使用过这个控件的同事都知道。

他通过引入import kotlinx.android.synthetic.main.activity_synthetic.* 来直接使用控件id使用控件。那么就会存在一个问题,如果不小心引入其他布局,使用了其他布局的控件,那么这个错误不会在编译时期被发现。是一个运行时的错误。这种运行时的错误就使得我们的程序变得不稳定。特别是一旦项目复杂起来,存在很多命名一样的控件,这更加会增大我们程序的不稳定性。

数据绑定(dataBinding)

什么是databinding

数据绑定:数据绑定库是一种支持库,借助该库,您可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。

这句话的意思就是我们可以在布局文件中给某个控件的某一个属性的值,与我们程序中的某一个值绑定。

用法

如何配置databinding

要想再项目中使用databinding,还是需要在模块的build.gradle进行如下配置

 android {
         ...
         dataBinding {
             enabled = true
         }
     }

基础入门

首先布局文件修改做一定的更改。布局文件以layout为根标签。其中data标签就是我们的数据元素,接着就是我们的视图元素。

 <?xml version="1.0" encoding="utf-8"?>
 <layout xmlns:android="http://schemas.android.com/apk/res/android">
     <!--数据元素-->
     <data>
         <variable
             name="user"
             type="com.eebbk.mvvmlearn.bean.User" />
     </data>
 ​
     <!--视图元素-->
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent">
         <TextView
             android:id="@+id/user_name"
             android:text="@{user.userName}"
             android:layout_width="200px"
             android:layout_height="70px"/>
 ​
     </LinearLayout>
 </layout>

可以使用快捷键的方式快速创建databinding布局。在布局文件中使用Alt+Enter快捷键,然后弹出下面弹窗,点击Convert to data binding layout就可以快速实现转换。

image.png

 data class User(var userName:String = "") {
 }
 class DataActivity : AppCompatActivity() {
 ​
     lateinit var dataBindingActivity:ActivityDataBinding
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         //使用DataBindingUtil将布局与activity进行绑定
         dataBindingActivity = DataBindingUtil.setContentView(this,R.layout.activity_data)
         val user = User("hello")
         //给布局文件中的数据元素赋值
         dataBindingActivity.user = user
         //user.userName = "hello world"
     }
 ​
 }

以上就是databinding的最基本的使用。看到这里有人就会有疑问了,如果我改变了user变量的userName属性,那么控件的属性值会不会变呢?答案是:不会变。

那如果要控件的属性值发生变化,我们应该怎么做呢?下面我们就来讲讲单向数据绑定

单向数据绑定

BaseObservable

我们希望数据变更之后,UI会即时刷新,这时候就需要借助Observable来实现这个功能了。我们直接通过一个例子一步步讲解这个单向数据绑定。

 //我们的实体类继承了BaseObservable
 class User():BaseObservable() {
     @get:Bindable
     var userName:String = ""
         set(value)  {
             field = value
             //BR 是编译阶段生成的一个类,功能与 R.java 类似,我们可以通过notifyPropertyChanged来更新这个属性关联的视图。这一步一定要执行。
             notifyPropertyChanged(BR.userName)
         }
 }

BaseObservable为我们提供了两个方法,一个是notifyPropertyChanged(int fieldId) 另一个是notifyChange()

notifyPropertyChanged(int fieldId)这个值是刷新我们这个类实体类的某一个属性,notifyChange()会刷新我们这个实体类的所有属性。

 <?xml version="1.0" encoding="utf-8"?>
 <layout xmlns:android="http://schemas.android.com/apk/res/android">
     <data>
         <variable
             name="user"
             type="com.eebbk.mvvmlearn.bean.User" />
     </data>
 ​
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="match_parent">
         <TextView
             android:id="@+id/user_name"
             android:text="@{user.userName}"
             android:layout_width="200px"
             android:layout_height="70px"/>
         <Button
             android:text="换名字"
             android:id="@+id/change_name"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"/>
 ​
     </LinearLayout>
 </layout>
 class DataActivity : AppCompatActivity() {
 ​
     lateinit var dataBindingActivity:ActivityDataBinding
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         dataBindingActivity = DataBindingUtil.setContentView(this,R.layout.activity_data)
         val user = User()
         user.userName = "hello"
         dataBindingActivity.user = user
         dataBindingActivity.changeName.setOnClickListener{
             //通过重新设置user的userName,这时候UI也会及时刷新。
             user.userName = "hello world"
         }
     }
 ​
 }
ObservableField

以上就是BaseObservable的用法,这种用法步骤确实比较繁琐。所以我们还有一个ObservableField类。这个类的也是基于BaseObservable封装的一个类,只不过他的用法比较简单。

 class User2 {
     var userName:ObservableField<String> = ObservableField("")
 }
 ...
 dataBindingActivity.changeName.setOnClickListener{
     //注意,这里需用通过set函数来更改userName的值。通过user2.userName = ObservableField("")这种方式是不能更新UI的,具体原因就是ObservableField只有执行了set函数,才会去执行notifyChange()函数。
     user2.userName.set("你好世界")
 }

双向绑定

单向绑定就是数据改变了,所绑定的ui视图也会及时刷新。那么有没有UI里面的数据变化了,我们的数据也会随即发生变化呢?答案肯定是有的。那怎么实现这种视图里面的数据变化,我们的程序的数据也会发生变化呢?这时候就需要使用双向绑定了。我们知道单向绑定的xml文件中是通过android:text="@{user.userName}"来进行赋值的。那么双向绑定需要改变一下赋值方式,变为android:text="@={user.userName}"就可以实现双向绑定。当然在JetPack中会有LiveData这种数据结构帮助我们更加实现简单实现Databinding。

事件绑定

事件绑定就是设置变量绑定的是回调接口。一般用于事件绑定的有

  • onClick,
  • onLongClick,
  • afterTextChanged,
  • onTextChanged等。

一下是一个登陆的例子

 <?xml version="1.0" encoding="utf-8"?>
 <layout  xmlns:android="http://schemas.android.com/apk/res/android">
     <data>
         <variable
             name="userInfo"
             type="com.eebbk.mvvmlearn.bean.UserInfo" />
 ​
         <variable
             name="loginPresenter"
             type="com.eebbk.mvvmlearn.LoginPresenter" />
     </data>
     <LinearLayout
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">
         <EditText
             android:afterTextChanged = "@{loginPresenter.setLoginName}"
             android:id="@+id/login_name"
             android:layout_width="match_parent"
             android:layout_height="100px"/>
         <EditText
             android:afterTextChanged = "@{loginPresenter.setLoginPassword}"
             android:id="@+id/login_password"
             android:layout_width="match_parent"
             android:layout_height="100px"/>
         <Button
             android:onClick="@{()->loginPresenter.login(userInfo)}"
             android:text="登录"
             android:id="@+id/login_button"
             android:layout_width="match_parent"
             android:layout_height="100px"/>
 ​
     </LinearLayout>
 </layout>
 class LoginPresenter(private val userInfo: UserInfo,
                      private val binding: ActivityLoginBinding) {
     //处理登录的回调,点击登录会回调到这里
     fun login(userInfo: UserInfo){
         Log.d("hch",userInfo.toString())
     }
 ​
     //处理回调,用户名发生变化时会回调到这里
     fun setLoginName(loginName:Editable){
         userInfo.loginName = loginName.toString()
         binding.userInfo = userInfo
     }
 ​
     //处理回调,密码发生变化时候会回调到这里
     fun setLoginPassword(loginPassword:Editable){
         userInfo.loginPassword = loginPassword.toString()
         binding.userInfo = userInfo
     }
 }

以上就是事件绑定了,当然这个登录的例子也可以通过双向绑定的方式实现,而且实现起来更加的简单。

参考文献:guolin.blog.csdn.net/article/det…