Android JetPack系列之——DataBinding

在傳統(tǒng)的Android應(yīng)用開發(fā)中,布局文件通常只負(fù)責(zé)應(yīng)用界面的布局工作,如果需要實(shí)現(xiàn)頁面交互就需要調(diào)用setContentView()將Activity、fragment和XML布局文件關(guān)聯(lián)起來。然后通過控件的id找到控件,接著在頁面中通過代碼對控件進(jìn)行邏輯處理。在這種傳統(tǒng)的開發(fā)方式中,頁面承擔(dān)了大部分的工作量,大量的邏輯處理需要在Activity、Fragment中進(jìn)行處理,因此頁面顯得臃腫不堪,維護(hù)起來也很困難,為了減輕頁面的工作量,Google提出了DataBinding(視圖綁定)。

一、DataBinding的優(yōu)點(diǎn)?

(1)解耦。將部分原屬于Activity/Fragment去實(shí)現(xiàn)的功能交由Model實(shí)體類去實(shí)現(xiàn),從而實(shí)現(xiàn)了部分功能代碼的分離,既保證了Activity/Fragment過于臃腫,也便于后期代碼的維護(hù)和擴(kuò)展。
(2)避免重復(fù)無效代碼。不再需要findViewById操作(其實(shí)這條不是那么重要,因?yàn)椴捎胟otlin編碼的話本身就沒有findViewById)。

總結(jié):Databinding的作用就是在XML文件里面實(shí)現(xiàn)數(shù)據(jù)的部分綁定從而避免將數(shù)據(jù)的全部展示交由Controller層去實(shí)現(xiàn)從而達(dá)到代碼的易于擴(kuò)展和后期的維護(hù)目的。

二、DataBinding的基本使用

先來了解一下DataBinding常用的幾個類:

  • DataBindingUtil:在Activity/Fragment中獲取相關(guān)的Binding對象。
  • BaseObservable:Bean可以繼承該抽象類,實(shí)現(xiàn)可觀察的模式,在set屬性的時候調(diào)用notifyPropertyChanged方法,喚起刷新操作,也可以調(diào)用notifyChange方法全部刷新。
  • Observable:Bean可以實(shí)現(xiàn)該接口,實(shí)現(xiàn)可觀察的模式,在set屬性的時候調(diào)用notifyPropertyChanged方法,喚起刷新操作,也可以調(diào)用notifyChange方法全部刷新。
  • ObservableFloat:這不是一個類,而是一類類的代表,如ObservableShort、ObservableParcelable等等,可觀察的屬性,通過get和set方法操作相關(guān)的值。
  • BaseObservableField<>:和上述類似,泛型可以傳入String等類型,比上述定義的基類型更加自由。

1.添加依賴

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

2.修改布局文件
<layout>為頭,以</layout>為尾。

<layout>
    <data>
        <variable
            name="login"
            type="com.jack.androidjetpack.login.Login" />
    </data>

    <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:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingBottom="@dimen/activity_vertical_margin">
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

3.點(diǎn)擊Build->Rebuild Project生成對應(yīng)的Binding

4.使用DataBindingUtil類來進(jìn)行視圖的綁定
Activity的處理方式

class LoginActivity : AppCompatActivity() {

    var binding: ActivityLoginBinding? = null
    var login: Login? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_login)
        login = Login("jack", "123456")
        binding?.setLogin(login)
    }
}

Fragment的處理方式

class PhoneCodeFragment : Fragment() {

    private var param1: String? = null
    private var param2: String? = null
    private var binding: FragmentPhoneCodeBinding? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        arguments?.let {
            param1 = it.getString(ARG_PARAM1)
            param2 = it.getString(ARG_PARAM2)
        }
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentPhoneCodeBinding.inflate(inflater, container, false)
        return binding?.root
    }

    companion object {
        @JvmStatic
        fun newInstance(param1: String, param2: String) =
            PhoneCodeFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_PARAM1, param1)
                    putString(ARG_PARAM2, param2)
                }
            }
    }
}

5.在layout布局中添加data標(biāo)簽
經(jīng)過前面的幾個步驟,我們已經(jīng)將Databinding和我們的XML文件綁定起來了,現(xiàn)在你點(diǎn)擊Databinding會發(fā)現(xiàn)直接可以跳轉(zhuǎn)到對應(yīng)的XML文件里面去了,現(xiàn)在我們就來看看如何給我們的XML文件里面的View設(shè)置值。

在XML文件的layout標(biāo)簽下,創(chuàng)建data標(biāo)簽,在data標(biāo)簽中再創(chuàng)建variable標(biāo)簽,variable標(biāo)簽主要用到的就是name屬性和type屬性,類似于Java語言聲明變量時,需要為該變量指定類型和名稱。新建一個名為Login的數(shù)據(jù)類。

data class Login(var userName: String, var password: String):BaseObservable()

然后在布局的 data 標(biāo)簽里聲明要使用到的變量名、類的全路徑等信息,如下所示:

<layout>

    <data>

        <variable
            name="login"
            type="com.jack.androidjetpack.login.Login" />

    </data>
    ...
</layout>

如果 Login有多處用到,也可以直接將之 import 進(jìn)來,這樣就不用每次都指明整個包名路徑了,而 java.lang.* 包中的類會被自動導(dǎo)入,所以可以直接使用。

<layout>

    <data>

        <import type="com.jack.androidjetpack.login.Login" />

        <variable
            name="login"
            type="Login" />

    </data>
    ...
</layout>

在XML文件中聲明好variable屬性后,接下來就可以在XML使用它了。使用variable屬性時需要使用到布局表達(dá)式: @{ }??梢栽诓季直磉_(dá)式@{ }中獲取傳入variable對象的值,如下所示:

 <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:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingBottom="@dimen/activity_vertical_margin">

        <EditText
            android:id="@+id/username"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="24dp"
            android:layout_marginTop="96dp"
            android:layout_marginEnd="24dp"
            android:hint="@string/prompt_name"
            android:inputType="textEmailAddress"
            android:selectAllOnFocus="true"
            android:text="@{login.userName}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <EditText
            android:id="@+id/password"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="24dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="24dp"
            android:hint="@string/prompt_password"
            android:imeActionLabel="@string/action_sign_in_short"
            android:imeOptions="actionDone"
            android:inputType="textPassword"
            android:selectAllOnFocus="true"
            android:text="@{login.password}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/username" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>

最后在我們的Controller層將我們的datamodel相關(guān)聯(lián)。

    var binding: ActivityLoginBinding? = null
    var login: Login? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_login)
        login = Login("jack", "123456")
        binding?.setLogin(login)
    }

到這里,一個最基礎(chǔ)的DataBinding的例子就結(jié)束了,接下來我們繼續(xù)往下看。

三、DataBinding的進(jìn)階

進(jìn)階1:給控件添加響應(yīng)事件
方式一:直接在Controller層通過原來的方式添加

binding?.login?.setOnClickListener {
          
}

方式二:創(chuàng)建一個工具類,在類中定義響應(yīng)的點(diǎn)擊事件
第一步:創(chuàng)建點(diǎn)擊的工具類

/**
 * @author: zhoufan
 * @date:   2021/8/30 11:14
 */
class ButtonClickListener {

    fun click(view: View) {
        Log.e("click","響應(yīng)登錄的點(diǎn)擊事件")
    }
}

第二步:在XML文件中添加工具類

<variable
      name="btnHandler"
      type="com.jack.androidjetpack.login.ButtonClickListener" />

第三步:在XML文件中添加響應(yīng)事件

<Button
     android:id="@+id/login"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:onClick="@{btnHandler::click}"
     android:text="@string/action_sign_in"
/>

第四步:在Controller里面進(jìn)行關(guān)聯(lián)

class LoginActivity : AppCompatActivity() {

    var binding: ActivityLoginBinding? = null
    var login: Login? = null
    var clickListener:ButtonClickListener?=null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_login)
        login = Login("jack", "123456")
        clickListener= ButtonClickListener()
        binding?.setLogin(login)
        // 這一步必須要,否則點(diǎn)擊沒反應(yīng)
        binding?.btnHandler = clickListener
    }
}

進(jìn)階2:BindingAdapter
使用DataBinding庫時,DataBinding會針對控件屬性生成對應(yīng)的XXXBindingAdapter類,如TextViewBindingAdapter類,其對TextView的每個可以使用DataBinding的屬性都生成了對應(yīng)的方法,而且每個方法都使用了@BindingAdapter注解,注解中的參數(shù)就是對應(yīng)View的屬性。

自定義BindingAdapter
編寫一個處理圖片的自定義BindingAdapter類。然后定義一個靜態(tài)方法,主要用于添加 BindingAdapter 注解,注解值是 ImageView 控件自定義的屬性名,如下所示。

class ImageBindingAdapter {

    companion object {
        @BindingAdapter("url")
        @JvmStatic
        fun loadImage(view: ImageView?, url: String?) {
            var realValue: String? = null
            if (url.equals("null")) {
                realValue = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fattach.bbs.miui.com%2Fforum%2F201412%2F27%2F111335whdlgodddosl6swq.jpg&refer=http%3A%2F%2Fattach.bbs.miui.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1632887161&t=f6a13d2953ea7be364a111344b72a654"
            }
            if (!TextUtils.isEmpty(realValue)) {
                Glide.with(view!!)
                    .load(realValue)
                    .into(view)
            }
        }
    }
}

在XML文件里面直接引用

<ImageView
      android:layout_width="300dp"
      android:layout_height="200dp"
      android:layout_below="@+id/login"
      android:layout_marginTop="16dp"
      app:url="@{`null`}" />

有時候,我們需要自定義多個屬性,那如何處理呢?和一個參數(shù)一樣,我們只需要使用BindingAdapter添加參數(shù)即可,如下所示:

class ImageBindingAdapter {

    companion object {
        @BindingAdapter(value = ["url", "placeholder", "error"])
        @JvmStatic
        fun loadImage(view: ImageView?, url: String?, placeholder: Drawable?, error: Drawable?) {
            var realValue: String? = null
            if (url.equals("null")) {
                realValue =
                    "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fattach.bbs.miui.com%2Fforum%2F201412%2F27%2F111335whdlgodddosl6swq.jpg&refer=http%3A%2F%2Fattach.bbs.miui.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1632887161&t=f6a13d2953ea7be364a111344b72a654"
            }
            if (!TextUtils.isEmpty(realValue)) {
                val options = RequestOptions()
                options.placeholder(placeholder)
                options.error(error)
                Glide.with(view!!)
                    .load(realValue)
                    .apply(options)
                    .into(view)
            }
        }
    }
}

對應(yīng)的XML文件為:

<ImageView
       android:layout_width="300dp"
       android:layout_height="200dp"
       android:layout_below="@+id/login"
       android:layout_marginTop="16dp"
       app:placeholder="@{ContextCompat.getDrawable(context, R.mipmap.ic_launcher)}"
       app:error="@{ContextCompat.getDrawable(context, R.mipmap.ic_launcher)}"
       app:url="@{`null`}" />

別忘記導(dǎo)入對應(yīng)的依賴

<import type="androidx.core.content.ContextCompat" />
<import type="com.jack.androidjetpack.R"/>

進(jìn)階3:雙向綁定
DataBinding的本身是對View層狀態(tài)的一種觀察者模式的實(shí)現(xiàn),通過讓View與ViewModel層可觀察的對象進(jìn)行綁定,當(dāng)ViewModel層數(shù)據(jù)發(fā)生改變時,View層也會自動進(jìn)行UI的更新,這種場景稱之為單向綁定。

但是在實(shí)際的開發(fā)過程中,單向綁定并不能滿足所有的需求。例如有下面的場景:如果布局中有一個EditText,當(dāng)用戶在輸入框中輸入內(nèi)容時,我們希望對應(yīng)的Model類能夠?qū)崟r更新,這就需要雙向綁定,DataBinding同樣支持這樣的能力。

實(shí)現(xiàn)雙向綁定需要用到ObservableField類,它能夠?qū)⑵胀ǖ臄?shù)據(jù)對象包裝成一個可觀察的數(shù)據(jù)對象,數(shù)據(jù)類型可以是基本數(shù)據(jù)類型、變量、集合,也可以是自定義類型。

第一步:修改我們的實(shí)體類

class Login : BaseObservable() {

    @get:Bindable
    var userName: String? = null
        set(userName) {
            field = userName
            notifyPropertyChanged(BR.userName)
        }

    @get:Bindable
    var password: String? = null
        set(password) {
            field = password
             notifyPropertyChanged(BR.userName)
        }
}

第二步:修改我們的實(shí)體類

  <EditText
            android:id="@+id/username"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:layout_marginStart="24dp"
            android:layout_marginTop="96dp"
            android:layout_marginEnd="24dp"
            android:hint="@string/prompt_name"
            android:inputType="textEmailAddress"
            android:selectAllOnFocus="true"
            android:text="@={login.userName}" />

        <EditText
            android:id="@+id/password"
            android:layout_width="match_parent"
            android:layout_height="40dp"
            android:layout_below="@+id/username"
            android:layout_marginStart="24dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="24dp"
            android:hint="@string/prompt_password"
            android:imeActionLabel="@string/action_sign_in_short"
            android:imeOptions="actionDone"
            android:inputType="textPassword"
            android:selectAllOnFocus="true"
            android:text="@={login.password}" />

將我們原來的@{login.userName}換成@={login.userName}就可以了。

四、DataBinding的實(shí)戰(zhàn)

在我們的實(shí)際開發(fā)過程中,RecyclerView算是使用非常頻繁的,接下來我們就看看如何使用DataBinding對RecyclerView進(jìn)行處理。
第一步:定義我們的實(shí)體類

class UserModel: BaseObservable() {

    @get:Bindable
    var name: String? = null
        set(name) {
            field = name
            notifyPropertyChanged(BR.name)
        }

    @get:Bindable
    var address: String? = null
        set(address) {
            field = address
            notifyPropertyChanged(BR.address)
        }

    @get:Bindable
    var age: String? = null
        set(age) {
            field = age
            notifyPropertyChanged(BR.age)
        }
}

第二步:創(chuàng)建我們的適配器的布局

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="user"
            type="com.jack.androidjetpack.list.UserModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.name}" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.address}" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.age}" />
    </LinearLayout>
</layout>?

第三步:創(chuàng)建我們的適配器

class UserAdapter : RecyclerView.Adapter<UserAdapter.ViewHolder>() {

    var dataList: MutableList<UserModel> = mutableListOf()
        set(value) {
            field = value
            notifyDataSetChanged()
        }


    inner class ViewHolder(binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root) {

        var adapterBinding: AdapterListBinding? = null

        init {
            adapterBinding = binding as AdapterListBinding
        }
    }

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

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val userModel = dataList[position]
        holder.adapterBinding?.user = userModel
    }

    override fun getItemCount() = dataList.size

}

第四步:在Activity里面完成功能

class ListActivity : AppCompatActivity() {

    private var userModelList: MutableList<UserModel>? = mutableListOf()
    private var activityListBinding: ActivityListBinding? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        activityListBinding = DataBindingUtil.setContentView(this, R.layout.activity_list)
        initData()
        initRecyclerView()
    }

    private fun initData() {
        for (i in 0..9) {
            val userModel = UserModel()
            userModel.name = "jack" + 1
            userModel.address = "beijing$i"
            userModel.age = "age$i"
            userModelList?.add(userModel)
        }
    }

    private fun initRecyclerView() {
        val layoutManager = LinearLayoutManager(this)
        activityListBinding?.recyclerView?.layoutManager = layoutManager
        val adapter = UserAdapter()
        activityListBinding?.recyclerView?.adapter = adapter
        adapter.dataList = userModelList!!
    }
}

好了,關(guān)于DataBinding的內(nèi)容就介紹到這里了,當(dāng)然,實(shí)際開發(fā)過程中DataBinding使用的遠(yuǎn)遠(yuǎn)不止這些,需要我們平時多總結(jié),多思考才能有更多的收獲。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容