Android—Jetpack教程(三)

这是我参与11月更文挑战的第24天,活动详情查看:2021最后一次更文挑战

前言

在上一篇中,对Jetpack里面的ViewModel以及LiveData进行了详解。在本篇中,将会对DataBinding进行详解!

那么DataBinding有什么作用呢?

让布局文件承担了部分原本属于页面的工作,使页面与布局耦合度进一步降低!

不过要想是用DataBinding功能,需要在对应AppModule的build.gradle开启dataBinding功能:

android {
    //...略
    defaultConfig {
	//...略
	dataBinding {
	//需要开启dataBinding功能
        enabled = true
    }
  }
}
复制代码

知道了对应的作用,以及开启了对应的功能,接下来开始实战部分!(将会循序渐进,由浅入深,依次讲解

1、示例一

第一个讲细一点,后面的可以稍微快点!

1.1 先看布局文件

1.png

如图所示

就现在这个布局,我们再熟悉不过了!不过现在需要将鼠标光标移动至<?xml前面,随后按Alt+enter,就会出现该弹窗,选择第一个按回车,布局就变成了:

<?xml version="1.0" encoding="utf-8"?>
<layout 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">
    <data>
    
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

...略
复制代码

这时我们看到:

  • 布局里面多了一对<data>...</data>标签。
  • 在这队标签里面将会放与布局相关的实体类以及辅助显示类。

我们先来看看对应的实体类长啥样:

1.2 实体类UserInfo.kt

class UserInfo {
    constructor(name: String?, image: Int, star: Int) {
        this.name = name
        this.star = star
        this.image = image
    }
    var name: String? = null
    var star = 0
    var image = 0
}
复制代码

我们看到就是一个很标准的实体类,刚刚提到<data>...</data>标签里就是放实体类以及辅助类,那该怎样在布局里面使用呢?

1.3 回到布局,添加实体类

<layout 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">
    <data>
        <variable
            name="userInfo"  
            type="com.hqk.databinding.UserInfo" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
 ...略 略略
            <TextView
	            android:id="@+id/textView"
	            android:layout_width="wrap_content"
	            android:layout_height="wrap_content"
	            android:layout_marginTop="44dp"
	            android:text="@{userInfo.name}"
	            android:textSize="24sp"
	            app:layout_constraintEnd_toEndOf="parent"
	            app:layout_constraintHorizontal_bias="0.498"
	            app:layout_constraintStart_toStartOf="parent"
	            app:layout_constraintTop_toTopOf="@+id/guideline2"
	            tools:text="姓名" />
 ...略 略略
复制代码

这时我们看到:

  • 使用了<variable 标签将对应的实体类与布局相互绑定;
  • name 这个顾名思义,就是定义对应的别名,在布局里以及在逻辑代码里,将通过这个别名相互绑定数据
  • type 这个表示,对应实体类的具体位置

现在我们看到已经有效的将对应的String类型和布局绑定了,现在实体类里面还差image star ,它们都是int类型,那该怎么和布局绑定呢?

1.4 辅助类登场

StarUtils.kt

class StarUtils {
	
    companion object {
        @JvmStatic
        fun getStar(star: Int): String? {
            when (star) {
                1 -> return "一星"
                2 -> return "二星"
                3 -> return "三星"
                4 -> return "四星"
                5 -> return "五星"
            }
            return ""
        }
    }
}
复制代码

ImageUtils.kt

class ImageUtils {

    companion object {
        @JvmStatic
        fun getDrawable(context: Context, resourceId: Int): Drawable? {
            return ContextCompat.getDrawable(context, resourceId)
        }
    }
}
复制代码

注意:当布局调用对应方法时,它是通过Java的方式,并非Kotlin方式,所以想要达到直接静态点出来的效果(不想出现companion关键字),需要在对应方法上加@JvmStatic注解。

现在辅助类准备好了,继续回到布局进行改造!

1.5 再次改造布局

<?xml version="1.0" encoding="utf-8"?>
<layout 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">
    <data>
        <variable
            name="userInfo"
            type="com.hqk.databinding.UserInfo" />
        <import type="com.hqk.databinding.StarUtils" />
        <import type="com.hqk.databinding.ImageUtils" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
...略 略 略

       <ImageView
            android:id="@+id/imageView"
            android:layout_width="300dp"
            android:layout_height="300dp"
            android:src="@{ImageUtils.getDrawable(context,userInfo.image)}"
            app:layout_constraintBottom_toTopOf="@+id/guideline2"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:srcCompat="@tools:sample/avatars" />

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="44dp"
            android:text="@{userInfo.name}"
            android:textSize="24sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.498"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="@+id/guideline2"
            tools:text="姓名" />

        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="40dp"
            android:text="@{StarUtils.getStar(userInfo.star)}"
            android:textSize="18sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.498"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView"
            tools:text="五星" />
   ...略 略 略
复制代码

这时我们通过<import将对应的辅助类也添加至布局中,并且通过@{xxx}实现了单向绑定数据。

那这样就好了么?数据就能自动在布局上赋值?

当然不行!仔细想下到目前为止是不是忽略了对应Activity的逻辑代码?不可能什么都不做就实现了数据的绑定!那么!!

1.6 进入对应Activity逻辑代码

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
//        setContentView(R.layout.activity_main)
		
        val activityMainBinding: ActivityMainBinding =
            DataBindingUtil.setContentView(this, R.layout.activity_main)
        
        val userInfo =
            UserInfo("斯嘉丽.约翰逊", R.drawable.scarlettjohansson, 4)
        
        activityMainBinding.userInfo = userInfo

    }
}
复制代码

这里我们看到:

  • 原有的setContentView(xxx)已成历史,

  • 而是使用了DataBindingUtil.setContentView(xx)

  • 这里的activityMainBinding.userInfo对应的userInfo与布局里面的name一致

            <variable
                name="userInfo"
                type="com.hqk.databinding.UserInfo" />
    复制代码

到现在为止,所有准备工作已经做好了,运行看下效果:

2.png

如图所示

我们发现,对应的数据已经成功的显示在布局上。

现在问题来了,那按钮呢?能用DataBinding么?? 当然可以!

1.7 按钮事件注入

class ClickHandle {
    fun buttonOnClick(view: View) {
        Toast.makeText(view.context, "喜欢", Toast.LENGTH_SHORT).show()
    }
}
复制代码

对应布局改造:

<?xml version="1.0" encoding="utf-8"?>
<layout 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">
    <data>
        <variable
            name="userInfo"
            type="com.hqk.databinding.UserInfo" />
            
        <import type="com.hqk.databinding.StarUtils" />

        <variable
            name="clickHandle"
            type="com.hqk.databinding.ClickHandle" />

        <import type="com.hqk.databinding.ImageUtils" />

    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
      ...略  略  略
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="56dp"
            android:onClick="@{clickHandle.buttonOnClick}"
            android:text="点我"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.498"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView2" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>     
复制代码

这时又通过<variable 将对应的事件类绑定与布局里,和userInfo一样。

既然这里和userInfo一样,那么对应的activity应该也要加对应逻辑吧?

1.8 再次进入Activity逻辑代码

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
//        setContentView(R.layout.activity_main)

        val activityMainBinding: ActivityMainBinding =
            DataBindingUtil.setContentView(this, R.layout.activity_main)

        val userInfo =
            UserInfo("斯嘉丽.约翰逊", R.drawable.scarlettjohansson, 4)

        activityMainBinding.userInfo = userInfo

        activityMainBinding.clickHandle = ClickHandle()
    }
}
复制代码

这次就不用多说了吧。相信通过上面的详解,看到这的你应该能看懂这里的逻辑!

OK,到这里,第一个示例就结束了!当然本篇远不止这些内容!像这样的例子我还有六个!

2、示例二

刚刚我们实现了无需findViewById方法,将数据动态赋值给对应的控件。但对应的布局是一层布局,那如果说对应布局使用了 <include这种标签,导入主布局的呢?要怎么将数据绑定在布局上呢?

既然提出了问题,那就实践一番!(在示例一的基础上)

2.1 先看对应主布局:

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>
        <variable
            name="userInfo"
            type="com.hqk.databinding2.UserInfo" />

        <variable
            name="clickHandle"
            type="com.hqk.databinding2.ClickHandle" />

        <import type="com.hqk.databinding2.ImageUtils" />

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <androidx.constraintlayout.widget.Guideline
            android:id="@+id/guideline2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            app:layout_constraintGuide_percent="0.5" />

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="300dp"
            android:layout_height="300dp"
            android:src="@{ImageUtils.getDrawable(context,userInfo.image)}"
            app:layout_constraintBottom_toTopOf="@+id/guideline2"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:srcCompat="@tools:sample/avatars" />

        <include
            layout="@layout/sub"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="48dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="@+id/guideline2"
            app:userInfo="@{userInfo}"
            app:clickHandle="@{clickHandle}" />


    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
复制代码

这里我们看到:

  • 我这将对应的TextViewButton,放入了次布局,然后通过<include标签导入进来。
  • 随后使用了app:userInfoapp:clickHandle将当前userInfoclickHandle导入了@layout/sub布局里

2.2 再来看次布局

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>
        <variable
            name="userInfo"
            type="com.hqk.databinding2.UserInfo" />

        <import type="com.hqk.databinding2.StarUtils" />

        <variable
            name="clickHandle"
            type="com.hqk.databinding2.ClickHandle" />

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="44dp"
            android:textSize="24sp"
            android:text="@{userInfo.name}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.498"
            app:layout_constraintStart_toStartOf="parent"
            tools:text="姓名"
            tools:ignore="MissingConstraints" />

        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{StarUtils.getStar(userInfo.star)}"
            android:layout_marginTop="40dp"
            android:textSize="18sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.498"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView"
            tools:text="五星" />

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="56dp"
            android:text="点我"
            android:onClick="@{clickHandle.buttonOnClick}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.498"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView2" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
复制代码

这里我们看到:

  • 依然通过<variable 接收了对应的实体类以及事件注入类,
  • 而辅助类还是通过 <import导入进布局里
  • 其他的依旧都没变

不过注意的是:

  • 次布局的<variable name=“userInfo” 与 主布局赋值次布局时app:userInfo相同,
  • 因此同一个实体类或事件注入类,与次布局,主布局对应的的别名最好一致
  • 对应的 <import 可以根据不同的功能分别导入(不用全部注入在主布局里)

好了示例二到这也结束了,业务逻辑和示例一的一致。

现在示例一和二,玩的都是静态数据,要不整个网络数据玩玩?好嘞!!

3、示例三

3.1 先来看看布局

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>
        <variable
            name="networkImage"
            type="String" />

        <variable
            name="localImage"
            type="int" />

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="300dp"
            android:layout_height="300dp"
            app:defaultImage="@{localImage}"
            app:image="@{networkImage}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:srcCompat="@tools:sample/avatars" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
复制代码

从这个布局里面可以看出:

  • 导入的实体类分别为:Stringint
  • ImageView控件里,分别表示加载网络图片和本地图片的功能。
  • 这里使用的是app:image加载网络图片
  • 使用app:defaultImage加载本地默认图片

既然如此,那看看业务逻辑怎么实现呢?

3.2 实现加载图片

class ImageViewBindingAdapter {

    companion object {
    
        // app:image="@{networkImage}" 和对应的app:image的image相互对应
        @BindingAdapter("image")
        @JvmStatic
        fun setImage(imageView: ImageView, url: String?) {
            if (!TextUtils.isEmpty(url)) {
                Glide.with(imageView.context).load(url).into(imageView)
                return
            }
            imageView.setBackgroundColor(Color.GRAY)
        }

        // app:defaultImage="@{localImage}" 和对应的localImage的image相互对应
        @BindingAdapter("defaultImage")
        @JvmStatic
        fun setImage(imageView: ImageView, id: Int) {
            imageView.setImageResource(id)
        }
    }
}
复制代码

这时我们看到:

  • 通过@BindingAdapter("xxx")这个注解将布局app:xxx加载图片的方式与业务逻辑单向绑定了

  • 因为这里用到了注解@BindingAdapter,所以在App的Module下,需要引入对应的插件

    plugins {
        id 'com.android.application'
        id 'kotlin-android'
        id 'kotlin-kapt' //需要引入该插件
    }
    
    android {
    ...略
    }
    复制代码

现在准备工作都做好了,就差Activity了

3.3 对应Activity业务逻辑

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
//        setContentView(R.layout.activity_main)

        val activityMainBinding: ActivityMainBinding =
            DataBindingUtil.setContentView(this, R.layout.activity_main)
            
//        activityMainBinding.networkImage=null
        activityMainBinding.networkImage =            "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F1114%2F060421091316%2F210604091316-6-1200.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1640276707&t=16e9f137e14ca63ce8ed7c3fd461f12e"
        
//        activityMainBinding.localImage=R.drawable.angelinajolie


    }
}
复制代码

别忘了网络权限

<uses-permission android:name="android.permission.INTERNET" />
复制代码

这时,我们就可以通过activityMainBinding.networkImageactivityMainBinding.localImage,动态加载对应的网络图片和本地图片。(运行效果就不贴了,就两张不一样的图)

不过现在发现没,加载图片的方式,分别定义了两个方法。

一个ImageView这么傲娇么?万一网络图片为空想要加载本地图片岂不是还要调两个方法?

不能忍!必须要改!

3.4 重构加载图片的方式

class ImageViewBindingAdapter {


    companion object {

//        // app:image="@{networkImage}" 和对应的app:image的image相互对应
//        @BindingAdapter("image")
//        @JvmStatic
//        fun setImage(imageView: ImageView, url: String?) {
//            if (!TextUtils.isEmpty(url)) {
//                Glide.with(imageView.context).load(url).into(imageView)
//                return
//            }
//            imageView.setBackgroundColor(Color.GRAY)
//        }
//
//        // app:defaultImage="@{localImage}"
//        @BindingAdapter("defaultImage")
//        @JvmStatic
//        fun setImage(imageView: ImageView, id: Int) {
//            imageView.setImageResource(id)
//        }


        /**
         * 加载网络图片,如果图片资源为空,则加载默认图片
         *
         * requireAll 这个意思是 方法setImage 对应的形参为可传参数(三个形参可以不用全部传入)
         */
        @BindingAdapter(value = ["image", "defaultImage"], requireAll = false)
        @JvmStatic
        fun setImage(imageView: ImageView, url: String?, id: Int) {
            if (!TextUtils.isEmpty(url)) {
                Glide.with(imageView.context).load(url).into(imageView)
                return
            }
            imageView.setImageResource(id)

        }

    }

}
复制代码

这时,我们看到:

  • 通过@BindingAdapter(value = ["image", "defaultImage"], requireAll = false),将两个方法变成了一个方法
  • value = ["image", "defaultImage"] 表示可以接受对应的加载方式(对应布局)
  • requireAll = false 这个表示,对应注解的方法里面的形参可以不用全部传入

这时我们通过动态修改activityMainBinding.networkImage activityMainBinding.localImage 就能加载不同的图片了。(运行效果,读者可亲自运行)

好了,示例三也完了!

在前三个例子里,我时不时提到过单向绑定。单向绑定,顾名思义就是对应的业务逻辑内容单方面与布局内容绑定、赋值。

既然有单向绑定,那么肯定有双向绑定!不然我们EditText输入的内容怎么获取?

4、示例四

4.1 还是老规矩,先看布局

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>
        <variable
            name="userViewModel"
            type="com.hqk.databinding4.UserViewModel" />

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <EditText
            android:id="@+id/editText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ems="10"
            android:inputType="textPersonName"
            android:text="@={userViewModel.userName}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
复制代码

注意布局里面:

  • 赋值的时候使用的是: android:text="@={userViewModel.userName}"
  • 也就是说:单向绑定使用的是@{xxx}
  • 双向绑定使用的是@={xxx}
  • 访问的内容为:userViewModel.userName里面的userName属性或者getUserName方法

这里面使用的是userViewModel,因此

4.2 userViewModel.kt

class UserViewModel : BaseObservable {

    constructor() {
        user = User("Hqk")
    }

    var user: User? = null


    // id 'kotlin-kapt'
    @Bindable
    fun getUserName(): String? {
        return user!!.userName
    }
    
    fun setUserName(userName: String?) {
        if (userName != null && userName != user!!.userName) {
            user!!.userName = userName
            Log.d("hqk", "set userName: $userName")
            notifyPropertyChanged(BR.userName)
        }
    }
}
复制代码

这里我们看到:

  • 对应的Model继承了BaseObservable
  • 对应的getUserName方法与布局 android:text="@={userViewModel.userName}" 一致
  • 使用了@Bindable注解,因此需要在对应的build.gradle里面加入'kotlin-kapt'插件
  • 通过setUserName方法监听EditText输入的内容,
  • 最后通过notifyPropertyChanged(BR.userName)通知刷新!

这里用到了User

4.3 User.kt

class User {
    constructor(userName: String?) {
        this.userName = userName
    }

    var userName: String? = null

}
复制代码

这个狠简单,直接过掉。来看看Activity对应的业务逻辑

4.4 对应Activity

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
//        setContentView(R.layout.activity_main)

        var activityMainBinding =
            DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
            
        activityMainBinding.userViewModel = UserViewModel()
    }
}
复制代码

这个不用说了吧,这几个例子下来,想必读者应该很熟悉这段代码了!

来看看运行效果

3.png

如图所示

当我输入对应的内容时,监听了对应EditText输入的内容。

这里我们看到,示例四的双向绑定用到了注解,那能否不使用注解呢?当然可以哇!

5、示例五

本示例基于示例四!

5.1 UserViewModel.kt

class UserViewModel {

    private var userObservableField: ObservableField<User>? = null

    constructor() {
        user = User("Hqk")
        userObservableField = ObservableField<User>()
        userObservableField!!.set(user)
    }

    var user: User? = null

    fun getUserName(): String? {
        return userObservableField!!.get()!!.userName
    }

    fun setUserName(userName: String) {
        Log.d("hqk", "userObservableField: $userName")
        userObservableField!!.get()!!.userName = userName
    }
}
复制代码

这里可以看出:

  • 并没有继承BaseObservable
  • 而对应的User也改成了ObservableField<User>
  • 对应的getUserName也没有用注解了

其他代码不动,本示例结束,来看看运行效果:

4.png

特意换了个TAG打印,运行效果一致,下一个!

前几个示例都是比较简单的页面,那如果说换至RecycleView呢?将会产生怎样的故事?

6、示例六

老样子,看布局!

6.1 先看对应布局

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
复制代码

这里就一个RecyclerView,没有直接的数据交互,因此data标签内为空

来看看item布局

太长了,所以甩个截图上来

5.png

如图所示

从这个布局可以看出:

  • 通过这个@{xx},可以看出,这里面数据都是单向绑定
  • 实体类为com.hqk.databinding6.UserInfo,别名为userInfo

因此

6.2 UserInfo.kt

class UserInfo {

    var chName:String?=null
    var enName:String?=null

    constructor(chName: String?, enName: String?, image: String?) {
        this.chName = chName
        this.enName = enName
        this.image = image
    }

    var image:String?=null


}
复制代码

没啥可说的,想要加载内容,肯定要有对应的数据。我这直接写了个本地数据,没有从网络获取

6.3 UserInfoUtils.kt

class UserInfoUtils {

    companion object {
    
        fun get(): List<UserInfo> {
            val list: MutableList<UserInfo> = ArrayList<UserInfo>()
            val i1 = UserInfo(
                "斯嘉丽.约翰逊",
                "Scarlett Johansson",
                "https://5b0988e595225.cdn.sohucs.com/images/20190624/d93dbf866aa2405f8b9b1d660c15db9d.jpeg"
            )
            list.add(i1)
			//....略
			//....略
			//....略
            val i10 = UserInfo(
                "詹妮弗·洛芙·休伊特",
                "Jennifer Love Hewitt",
                "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=791729948,2390587761&fm=26&gp=0.jpg"
            )
            list.add(i10)
            return list
        }
    }
}
复制代码

这个就是模拟从网络获取的数据。没啥可说的。

这里看到有网络图片的,因此查看网络图片加载

6.4 网络图片加载

class ImageViewBindingAdapter {


    companion object{
        @BindingAdapter("itemImage")
        @JvmStatic
        fun setImage(imageView: ImageView, url: String?) {
            if (!TextUtils.isEmpty(url)) {
                Glide.with(imageView.context).load(url).into(imageView)
                return
            }
            imageView.setBackgroundColor(Color.GRAY)
        }
    }

}
复制代码

这个讲过,直接过!重点不在这!

6.5 重点,RecycleView绑定的Adapter

class RecyclerViewAdapter() : RecyclerView.Adapter<RecyclerViewAdapter.MyViewHolder>() {

    var listUserInfo: List<UserInfo> = ArrayList<UserInfo>()

    constructor(listUserInfo: List<UserInfo>) : this() {
        this.listUserInfo = listUserInfo
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        var itemBinding: ItemBinding = DataBindingUtil.inflate(
            LayoutInflater.from(parent.context),
            R.layout.item, parent, false
        )
        return MyViewHolder(itemBinding)
    }
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        var userInfo = listUserInfo[position]
        holder.itemBinding!!.userInfo = userInfo
    }

    override fun getItemCount(): Int {
        return listUserInfo.size
    }

    class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

        var itemBinding: ItemBinding? = null

        constructor(itemBinding: ItemBinding) : this(itemBinding.root) {
            this.itemBinding = itemBinding
        }

    }
}
复制代码

代码解析

  • 前面的直接过掉,直接看onCreateViewHolder
  • 在这里使用的是 DataBindingUtil.inflate(xxx),返回是ItemBinding ,并非ActivityMainBinding
  • 在方法onBindViewHolder里,使用了 holder.itemBinding!!.userInfo,而这里的.userInfo
  • 与item布局绑定的别名name="userInfo"一致

OK,这里讲解完了,

6.6 来看看对应的Activity

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
//        setContentView(R.layout.activity_main)
        
        var activityMainBinding: ActivityMainBinding =
            DataBindingUtil.setContentView(this, R.layout.activity_main)
        
        activityMainBinding.recyclerView.layoutManager = LinearLayoutManager(this)
        var adapter = RecyclerViewAdapter(UserInfoUtils.get())
        activityMainBinding.recyclerView.adapter = adapter
    }
}
复制代码

这里不用多说了,直接看运行效果:

6.png

OK,DataBinding内容已经讲解完毕!现在开始最后一个实战,将前几篇所讲的知识点和本篇结合起来!

来做一个裁判记分牌!

7、示例七(裁判记分牌)

7.1 一如既往,先看布局

7.png

如图所示

  • 这里分为两个队伍,分别为:Team ATeam B
  • 加分,分别为 1分、2分、3分
  • 拥有撤销单次操作,以及清空积分的功能
  • 这里绑定的Model为MyViewModel
  • 对应按钮点击事件使用的是android:onClick="@{()->viewModel.aTeamAdd(1)}"

7.2 来看MyViewModel

class MyViewModel : ViewModel() {

    //使用LiveData定义A队积分
    private var aTeamScore: MutableLiveData<Int?>? = null
    //使用LiveData定义B队积分
    private var bTeamScore: MutableLiveData<Int?>? = null
    
    private var aLast: Int? = null//A队上一次分数
    private var bLast: Int? = null//B对上一次分数

    //A队积分获取
    fun getaTeamScore(): MutableLiveData<Int?>? {
        if (aTeamScore == null) {
            aTeamScore = MutableLiveData()
            aTeamScore!!.value = 0
        }
        return aTeamScore
    }

    //B队积分获取
    fun getbTeamScore(): MutableLiveData<Int?>? {
        if (bTeamScore == null) {
            bTeamScore = MutableLiveData()
            bTeamScore!!.value = 0
        }
        return bTeamScore
    }

    //A队单次添加的积分
    fun aTeamAdd(i: Int) {
        saveLastScore()
        aTeamScore!!.value = aTeamScore!!.value!! + i
    }
	
    //B队单次添加的积分
    fun bTeamAdd(i: Int) {
        saveLastScore()
        bTeamScore!!.value = bTeamScore!!.value!! + i
    }

    //回退分数
    fun undo() {
        aTeamScore!!.value = aLast
        bTeamScore!!.value = bLast
    }

    //重置分数
    fun reset() {
        aTeamScore!!.value = 0
        bTeamScore!!.value = 0
    }

    //记录上一次的分数
    private fun saveLastScore() {
        aLast = aTeamScore!!.value
        bLast = bTeamScore!!.value
    }

}
复制代码

代码解析

  • 这次的MyViewModel 才是真正的ViewModel,因为继承了ViewModel()
  • 前面几个示例都是单个DataBinding使用
  • 对应的按钮交互变成了@{()->viewModel.按钮点击方法名(方法参数)}

7.3 来看看Activity使用:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
//        setContentView(R.layout.activity_main)

        var activityMainBinding: ActivityMainBinding =
            DataBindingUtil.setContentView(this, R.layout.activity_main)

        var viewModel: MyViewModel = ViewModelProvider(this)[MyViewModel::class.java]
        
        // appcompat:1.3.0 及以后的版本,可以不用下面那种方式,用上面更简便
//      var  viewModel = 
//            ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory(application))[MyViewModel::class.java]

        activityMainBinding.viewModel = viewModel
        activityMainBinding.lifecycleOwner = this //使对应的dataBinding拥有Lifecycle功能
    }
}
复制代码

这里不用多说了吧,不过注意看注释,一切都在注释中。

来看看运行效果

在这里插入图片描述 OK!完美运行!

总结

从上面的示例中,可以看出DataBinding的优势:

  • 不再需要findViewById,项目更简洁,可读性更高
  • 布局文件可以包含简单的业务逻辑

结束语

好了,本篇DataBinding所有讲解,到这里就结束了!下一篇讲解另一个组件Room,敬请期待吧!

Demo下载: 点我下载

猜你喜欢

转载自juejin.im/post/7034462483930693668