Android MVVM探索(一) - DataBiding初解

cy不想說話發表於2018-10-22
發現我好久沒寫部落格,其實最近一直都很想寫部落格的,但是不知道寫點什麼好。剛好碰上最近在學習Android的MVVM設計模式以及官方提供給我們的控制元件,所以才有了這篇文章。(其實還是因為我懶,我懶!)

今天我想給大家講講DataBinding,為了保證我寫的不會出錯,我也借鑑參考了不少文章和視訊。給大家看看一篇我個人覺得還不錯的。DataBinding最全使用說明(掘金部落格)還有某課網的視訊,Android Data Binding實戰-入門篇Android Data Binding實戰-高階篇

本篇文章程式碼地址

Android MVVM探索系列

Android MVVM探索(一) - DataBiding初解

Android MVVM探索(二) - DataBiding常用註解

Android MVVM探索(三) - ViewModel,DataBinding,LiveData混合三打

1, 什麼是DataBinding?

DataBinding,2015年IO大會介紹的一個框架,字面理解即為資料繫結,是Google對MVVM在Android上的一種實現,可以直接繫結資料到xml中,並實現自動重新整理(即,資料變化UI進行相應的變化)。而且還支援一些表示式。比如常見的三元運算子:

1+x == 3 ? "true" : "false"

它還可以支援lambda表示式:

(v,fcs) -> presenter.onFocusChange(user)}

使用了DataBinding,可以省去一些控制元件繫結程式碼,例如:findviewById等。

2, 開始使用DataBinding

要想使用DataBinding的話,首先要在你安卓工程中,安卓Application的module(一般為app這個module)的android配置中加上如下程式碼:

android{
    // 這裡省去一些常有的配置程式碼
    dataBinding {
        enabled = true;
    }
}
複製程式碼

另外,如果你是使用Kotlin進行程式設計的話,你還要在加入了上面程式碼的Gradle檔案中頂部加上以下程式碼,否則Kotlin將無法識別DataBinding資源,至於什麼是DataBinding資源,我們後面會提到:

apply plugin: 'kotlin-kapt'
複製程式碼

是的,你沒有看錯,就這麼簡單我們就加上了DataBiding,不需要引入任何依賴。 首先我們先建立一個普通類作為ViewModel:

package top.cyixlq.test

class MainViewModel {
    var name = "張三"
    var age = 15
    var isMan = true
    fun log() {
        Log.d("MyTAG", "按鈕被點選了一下")
    }
}
複製程式碼

其次,我們要將我們佈局檔案程式碼進行一些改動。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="viewModel"
            type="top.cyixlq.test.MainViewModel"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

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

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf(viewModel.age)}"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.isMan ? @string/man : @string/woman}"/>

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{v -> viewModel.log()}"
            android:text="點我"/>

    </LinearLayout>
</layout>
複製程式碼

最後,改造我們的Activity程式碼:

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        // 第一種將資料填充到xml檔案中的方法(程式碼在下面這行),我們直接例項化了一個MainViewModel賦值給BR資源中一個叫viewModel的變數
        // binding.setVariable(BR.viewModel, MainViewModel())
        // 以下是一些說明:
        // BR就是前文提到的DataBinding資源,像R檔案一樣自動生成,記錄所有xml中data標籤內的變數名稱,有點像控制元件id的感覺
        // viewModel來自佈局檔案中data標籤內的variable標籤中的name

        // 第二種將資料填充到xml檔案中的方法(程式碼在下面這行),viewModel這個變數名視你在xml中variable標籤中的name而定
        binding.viewModel = MainViewModel()
        // 假如你的name為user,並且class名稱也為User的話(name和class的名稱不一定要相同)
        // 那麼程式碼就是binding.user = User()

        // java 程式碼如下
        // binding.setViewModel(new MainViewModel())
        // binding.setUser(new User())
    }

    override fun onDestroy() {
        super.onDestroy()
        // 在Activity銷燬時記得解綁,以免記憶體洩漏
        binding.unbind()
    }
}
複製程式碼

3, 加入DataBinding後,xml檔案的一些新的用法

  1. 資料的填充

    可以很明顯的看到,我們在佈局檔案的最外層不是任何佈局標籤,而是layout標籤。之後再引入data標籤,data裡面是變數集合,整個xml檔案中只允許有一個data標籤。data標籤中可以包含多個variable。name代表變數名稱,type是變數型別。在activity中,我們新建了一個binding變數,並且通過binding變數把MainViewModel例項化的物件賦值到xml檔案中,這樣我們在xml中就可以直接填充到對應控制元件中。通過@{},我們的控制元件就可以直接引用到viewModel中的對應的值。就像:

    android:text="@{viewModel.name}"
    複製程式碼
  2. import標籤

    就像java中的import關鍵字一樣,可以匯入型別,所以我們上面的xml檔案中data部份還可以這樣寫:

    <import type="top.cyixlq.test.MainViewModel"/>
    <variable
        name="viewModel"
        type="MainViewModel"/>
    複製程式碼

    我們還注意到,填充age屬性的時候,我們是@{String.valueOf(viewModel.age)}。因為age是整數型,我們知道TextView的Text是不可以為整數型的,所以我們使用了String這個類中的方法進行了轉換。按理說,String理應也需要使用import標籤進行引入,然而我們並沒有這麼做。是的,和Java一樣,java.lang包下的東西是自動引入的。

  3. 三元運算子和lambda表示式以及簡單運算

    我們可以看到,我們填充isMan這個屬性的時候使用了三元運算子,並且使用@string/man和@string/woman作為兩個可選值。看起來是不是很神奇?其實我們也可以直接這樣寫:

    android:text='@{viewModel.isMan ? "男" : "女"}'
    複製程式碼

    但是值得注意的是,在Windows下,我們這樣寫可能會報錯。是關於utf-8的一個錯誤,具體不太清楚。如果你是java程式碼,在編譯的時候會告訴你這個錯誤。如果是kotlin下,就會顯示無法列印這個錯誤log。所以我還是推薦引用string資源。

    就像我們在xml檔案中設定按鈕的點選事件一樣,我們可以直接引入lambda表示式,從而直接呼叫viewModel中的公開方法,是不是覺得簡單多了?

    雖然可以直接在xml檔案中進行運算了,例如字串的拼接,數字的加減,如下所示:

    android:text="@{String.valueOf(viewModel.age + 1)}"
    android:text='"性別:" + viewModel.isMan ? "男" : "女"'
    複製程式碼

    但是,我不推薦在xml檔案中進行過於複雜的運算,可以在ViewModel類中處理好之後利用函式返回。如下所示:

    <!-- xml檔案中 -->
    android:text="@{viewModel.convertSex()}"
    
    // MainViewModel檔案中
    fun convertSex(): String {
        var result = "性別:"
        val sex = if (isMan) "男" else "女"
        return result + sex
    }
    複製程式碼
  4. include標籤的一些變化

    我們在開發中難免要進行佈局的複用,這就會用到include標籤了,但是如果我們引入的佈局檔案中也有variable怎麼辦,怎麼才能從當前佈局檔案中傳入到include匯入的佈局中呢?請直接看程式碼說明!我們以自己做一個標題欄為例。

    1. 首先隱藏我們原有的標題欄,在Activity的onCreate中加入下面的程式碼:
      supportActionBar?.hide()
      // java程式碼
      // ActionBar actionBar = getSupportActionBar();
      // if (actionBar != null) actionBar.hide()
      複製程式碼
    2. 新建一個layout_title_bar.xml,內容如下:
      <layout xmlns:android="http://schemas.android.com/apk/res/android">
          <data>
              <variable
                  name="title"
                  type="String"/>
          </data>
          <RelativeLayout
              android:layout_width="match_parent"
              android:layout_height="?attr/actionBarSize"
              android:background="@color/colorPrimary">
      
              <TextView
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:layout_centerInParent="true"
                  android:textColor="@android:color/white"
                  android:text="@{title}"/>
      
          </RelativeLayout>
      </layout>
      複製程式碼
    3. 在activity_main.xml中加入以下程式碼:
      <variable
          name="text"
          type="String" />
      
      <!-- 這裡傳過去的屬性名稱要與include引入的佈局檔案中variable的name一樣 -->
      <!-- 同樣,這裡可以使用MainViewModel中的某個字串屬性作為值傳過去,不宣告一個新的variable -->
      <include layout="@layout/layout_titlt_bar"
          title="@{text}"/>
      複製程式碼
    4. 別忘了在Activity中給text賦值:
      binding.text = "測試"
      複製程式碼

4, 關於Activity檔案中binding變數的一些說明

  1. binding變數的型別是ActivityMainBinding,這個是專案build後自動生成的,根據佈局檔名:activity_main.xml 來命名的類名稱。也許你也發現了,它就是佈局檔案每個單詞首字母大寫,然後拼接上Binding。
  2. 我們前面說過,加入DataBinding後我們可以省去一些UI相關程式碼,比如findviewById。那麼具體是怎麼操作呢。很簡單,在binding變數賦值後,我們直接通過binding.控制元件ID就可以直接獲取該控制元件例項。例如:binding.button.setOnClickListener(*)
  3. 我們還可以將xml檔案中的variable進行賦值。具體見上面Activity程式碼及相關注釋!

5,資料的實時更新,雙向繫結

在MainViewModel中和xml佈局檔案中新新增如下程式碼:

// MainViewModel中
fun oneYearLater() {
    age++
    Log.d("MyTAG", "年齡:$age")
}

<!-- 在xml佈局檔案中 -->
<Button
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:onClick="@{v -> viewModel.oneYearLater()}"
    android:text="一年後"/>
複製程式碼

當我們點選這個按鈕一年後之後會執行oneYearLater方法,裡面的age屬性會自增。但是,這樣寫好之後,我們發現age變化了,但是檢視上的年齡的文字並沒有重新整理。我們不是說加入了DataBinding之後會自動實時重新整理嗎?別急,如果我們要實現實時重新整理的話,我們要對MainViewModel進行小小的改造,其中有三種方法:

  1. 就像下面那樣,將對應屬性改成這樣:

    // var age = 15
    var age = ObservableInt(15)
    
    fun oneYearLater() {
        // age++
        val lastAge = age.get()
        age.set(lastAge + 1)
        Log.d("MyTAG", "年齡:$age")
    }
    複製程式碼

    將變數age宣告為可觀察的Int物件。其中,類似ObservableInt的變數型別還有:

    • ObservableBoolean
    • ObservableByte
    • ObservableChar
    • ObservableDouble
    • ObservableLong
      ...此處省略了一些基本資料型別

    對於列表和Map,還有下面這些型別:

    • ObservableList< T >
    • ObservableArrayList< T >
    • ObservableArrayMap<K,V>
    • ObservableMap<K,V>

    那麼對於String或者自定義的類這種非基本資料型別,那麼怎麼辦?DataBinding給我們提供了:ObservableField,我們就可以這樣用:

    val name = ObservableField<String>("張三")
    複製程式碼

    對於序列化,還有這個資料型別:

    ObservableParcelable< T >

  2. 讓類繼承BaseObservable:

    我們先新建一個ObserveViewModel的類,讓它繼承BaseObservable:

    class ObserveViewModel : BaseObservable() {
    
        private var firstName = "y"
        private var lastName = "c"
    
        // 這裡要加上這個標籤,在set方法中BR才能找到對應屬性
        @Bindable
        fun getFirstName(): String {
            return firstName
        }
    
        @Bindable
        fun getLastName():String {
            return lastName
        }
    
        fun setFirstName(name:String) {
            this.firstName = name
            notifyPropertyChanged(BR.firstName)
        }
    
        fun setLastName(name:String) {
            this.lastName = name
            notifyPropertyChanged(BR.lastName)
        }
    
        // 改姓的方法
        fun changeLastName() {
            setLastName("薛")
        }
    
    }
    複製程式碼

    在xml佈局中進行引入,並且將對應屬性值進行展示以及設定按鈕點選事件:

    <!-- 這段請放在data標籤內 -->
    <variable
        name="observeViewModel"
        type="top.cyixlq.test.ObserveViewModel"/>
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{observeViewModel.firstName}"/>
    
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{observeViewModel.lastName}"/>
    
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="改姓"
        android:onClick="@{v -> observeViewModel.changeLastName()}"/>
    複製程式碼

    別忘了還要在Activity中進行賦值:

    val observeViewModel = ObserveViewModel()
    binding.observeViewModel = observeViewModel
    複製程式碼
  3. 在Activity中進行監聽。

    在ObserveViewModel類中新新增一個屬性:

    val age = ObservableInt(17)
    複製程式碼

    在Activity中新加入如下程式碼:

    // 給按鈕設定點選監聽事件
    binding.btnAddAge.setOnClickListener {
        val lastAge = observeViewModel.age.get()
        observeViewModel.age.set(lastAge + 1)
    }
    // 監聽ObserveViewModel中值的變化並進行回撥處理
    observeViewModel.age.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
        override fun onPropertyChanged(observable: Observable, i: Int) {
            binding.age.text = observeViewModel.age.get().toString()
        }
    })
    複製程式碼

    在佈局檔案中新增一個TextView展示新的屬性,並新增一個按鈕改變新的屬性值:

    <TextView
        android:id="@+id/age"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="年齡"/>
    
    <Button
        android:id="@+id/btn_add_age"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="加一歲"/>
    複製程式碼

看過上面三種方法後,是不是覺得第一種方法最簡單?是的,我個人也比較推崇第一種方法,簡單粗暴,但是並不意味著其他方法就用不到了,我們還是應該根據業務需求使用不同的方法靈活變通!

瞭解Vue的同學知道,當我使用:value={{text}}的時候,就可以實現資料檢視雙向繫結。即輸入框中的內容是什麼,對應的屬性值就是輸入框中的內容。那麼,DataBding也可以做到嗎?答案是當然可以的。首先我們在MainViewModel中新新增一個text屬性:

val text = ObservableField<String>("")
複製程式碼

然後在activity_main.xml佈局檔案中多加一個EditText和一個TextView:

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

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={viewModel.text}"/>
複製程式碼

這樣做完之後,我們在輸入框中輸入什麼,我們在TextView上面看到的就是什麼。這樣就實現了雙向繫結。我們不然發現,我們實現雙向繫結其實就是多加了一個“ = ”!

OK,這一小章節我們就先到這裡了,下一章節我們就介紹一下DataBinding的一些常用註解!另外本章節中出現的問題還望大家留言指正,畢竟還是在MVVM探索道路中!

相關文章