引子
Android 中的MVVM模式的實現其很大一部分依託於Android Architecture Component 中的Databinding的實現,DataBinding讓我們的資料和介面產生了連線,而不需要我們手動的操作著令人煩悶的控制元件賦值操作。MVVM也正是藉助於DataBinding實現了資料與介面的解耦。其類似與一種輕量級的標記語言,通過ide的支援,實現介面支援基礎的標記語法操作,並通過編譯佈局檔案將對應的語法轉換為多個與之繫結的實現類來負責Model與資料之間的繫結,由此可見其發展方向類似與前端,知道前端的同學應該會發現這很類似與java中的jsp、python的模板語言,但就目前而言,其功能相對較弱,並不能如前面兩種標記語言那般強大到可以直接實現佈局與程式碼的混合開發,但未來可期。
- MVVM的組成結構
- MVVM之DataBinding的使用
- MVVM之LiveData的使用
- MVVM之Room的使用
- MVVM之Retrofit與LiveData的整合
- MVVM之ViewModel的簡單封裝
DataBinding
DataBinding的使用Google給了不少的示例,寫的也挺全面,除了都是英文外沒什麼缺點,基本上DataBinding的各種使用方式都有對應的操作,相較於網上充斥的介紹文件,個人比較推薦看google提供的demo案例,看的仔細了你會有一種 麻雀雖小、五臟俱全 的感覺,讓你感覺Google大爺還是你家大爺。這裡給出了Goolge提供相關的DataBinding操作的示例導航以及使用的簡要介紹.
DataBindng 案例
這裡給出了Google的有關DataBiding的示例最後一個是我用的MVVM模式的案例(MVVM集數成了DataBinding),DataBidng單獨使用的話其優勢並不是很大,一般而言DataBinding都是配合著觀察者使用的,如下的案例大多都是使用Observer資料或者LiveData資料配合使用。
- DataBindingBasicSample
- DataBindingTwoWaySample
- Android Architecture Blueprints (todo-mvvm-live-kotlin branch)
- GithubBrowserSample
- Android Sunflower
- MVVM案例
DataBinding的繫結
DataBidng 的繫結主要分為:變數繫結、事件繫結和介面卡繫結,其繫結方式有分為單向繫結和雙向繫結,其實現也都稍有不同。
變數繫結
變數的繫結是通過實現傳入的繫結物件,通過繫結的物件的引數進行繫結,同時也支援表示式、方法輸出等,如下示例:
<lanyout 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>
<import type="com.example.android.databinding.basicsample.R"/>
<import type="com.example.android.databinding.basicsample.util.ConverterUtil"/>
<variable
name="user"
type="com.example.android.databinding.basicsample.data.ObservableFieldProfile" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--user物件的變數實現 -->
<TextView
android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="128dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:text="@{user.name}"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/name_label"/>
<!--表示式實現 -->
<ImageView android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:layout_marginTop="24dp"
android:contentDescription="@string/profile_avatar_cd"
android:minHeight="48dp"
android:minWidth="48dp"
app:layout_constraintTop_toBottomOf="@+id/name"
app:layout_constraintStart_toStartOf="parent"
android:tint="@{user.likes > 9 ? @color/star : @android:color/black}"
app:srcCompat="@{user.likes < 4 ? R.drawable.ic_person_black_96dp : R.drawable.ic_whatshot_black_96dp }"/>
<!--靜態方法實現 -->
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:max="@{100}"
android:visibility="@{ConverterUtil.isZero(user.likes)}"
app:progressScaled="@{user.likes}"
app:layout_constraintTop_toBottomOf="@+id/imageView"
app:layout_constraintStart_toStartOf="parent"
tools:progressBackgroundTint="@android:color/darker_gray"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
複製程式碼
事件繫結
實現的繫結主要指的是layout中提供的方法傳入實現(基本就是代指onClick方法,當然也可自行進行定製),事件的繫結方式有兩種:一種是lambda方式、另一種是保證和andorid實現方法引數相同的只需要傳入方法名,如下:
/**
*帶繫結viewmodle
*/
class ProfileLiveDataViewModel : ViewModel() {
fun onLike() {
_likes.value = (_likes.value ?: 0) + 1
}
fun disLike(view:View) {
_unlikes.value = (_unlikes.value ?: 0) + 1
}
}
複製程式碼
<lanyout 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>
<import type="com.example.android.databinding.basicsample.R"/>
<import type="com.example.android.databinding.basicsample.util.ConverterUtil"/>
<variable
name="viewmodel"
type="com.example.android.databinding.basicsample.data.ProfileLiveDataViewModel"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<!--lambda方式實現 -->
<Button
android:id="@+id/like_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginTop="16dp"
android:onClick="@{() -> viewmodel.onLike()}"
android:text="@string/like"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toStartOf="@+id/imageView"
app:layout_constraintTop_toBottomOf="@+id/likes"/>
<!--原生樣式實現 -->
<Button
android:id="@+id/unlike_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginTop="16dp"
android:onClick="@{viewmodel.disLike}"
android:text="@string/like"
app:layout_constraintTop_toBottomOf="@id/like_button"
app:layout_constraintStart_toStartOf="@+id/imageView"
app:layout_constraintTop_toBottomOf="@+id/likes"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
複製程式碼
介面卡繫結
說起DataBinding的介面卡繫結就有點高階了,它是一個類似kotlin的擴充套件一樣的東西,只不過kotlin的擴充套件作用的是實現程式碼的具體類中,而DataBinding的介面卡作用到的是佈局檔案中的view的屬性中,它可以幫我們減少很多麻煩的操作,讓我們的程式碼看起來更具有可讀性、美觀性,如下:
/**
* Databinding介面卡的使用
* 需要注意的是由於介面卡使用的是java的static方法,為了適配kotlin這裡每個介面卡方法均需要新增註解@JvmStatic使得其可以與java適配
* 詳細請參考kotlin官網靜態的使用
*
*
*/
object BindingAdapters {
/**
*
* 一個繫結的介面卡可以在任何地方陪使用用於設定ImageView的Popularity,接受值為popularity
*
*/
@BindingAdapter("app:popularityIcon")
@JvmStatic fun popularityIcon(view: ImageView, popularity: Popularity) {
val color = getAssociatedColor(popularity, view.context)
ImageViewCompat.setImageTintList(view, ColorStateList.valueOf(color))
view.setImageDrawable(getDrawablePopularity(popularity, view.context))
}
}
複製程式碼
<lanyout 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>
<import type="com.example.android.databinding.basicsample.R"/>
<import type="com.example.android.databinding.basicsample.util.ConverterUtil"/>
<variable
name="viewmodel"
type="com.example.android.databinding.basicsample.data.ProfileLiveDataViewModel"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:layout_marginTop="24dp"
android:contentDescription="@string/profile_avatar_cd"
android:minHeight="48dp"
android:minWidth="48dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:popularityIcon="@{viewmodel.popularity}"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
複製程式碼
雙向繫結
DataBinding其本身並不支援雙向繫結,一般都是通過一個Observer型別資料進行觀察實現的雙向繫結的過程,一般實現的方式分為兩種:一種是Observabler介面的實現,另一種則是LiveData的繫結實現.這裡有幾個小的地方需要注意點:xml中一般使用 @{} 用於給控制元件賦值,xml中一般使用 **=@{}**實現xml控制元件的賦值和控制元件值變化修改對應變數的值。
- Observer介面的實現
Observer介面實現原理說來也簡單,但操作並不簡單,它是DataBinding內部封裝的一個介面,代理實現屬性的變化自動更新ui介面,但程式碼中變數的變化需要我們主動觸發通知UI介面進行更新,其實現如下:
/**
*作為實現Observer功能的一個viewmodle基類
*/
open class ObservableViewModel : ViewModel(), Observable {
private val callbacks: PropertyChangeRegistry = PropertyChangeRegistry()
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
callbacks.add(callback)
}
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
callbacks.remove(callback)
}
/**
* 這個方法呼叫會對model下的所有屬性進行檢查並更新ui
*/
fun notifyChange() {
callbacks.notifyCallbacks(this, 0, null)
}
/**
*
*
* 更新modle下制定id的控制元件值,其中id是databinding對應生成的一個變數id,不同於控制元件的屬性id,databinding其本質是控制元件與屬性的一一繫結.
*
* @param fieldId The generated BR id for the Bindable field.
*/
fun notifyPropertyChanged(fieldId: Int) {
callbacks.notifyCallbacks(this, fieldId, null)
}
}
/**
* viewmodle的實現類,具體需要繫結的viewmodle,主要對於需要雙向繫結的需要手動提醒更新
*/
class ProfileObservableViewModel : ObservableViewModel() {
val name = ObservableField("Ada")
val lastName = ObservableField("Lovelace")
val likes = ObservableInt(0)
fun onLike() {
likes.increment()
//需要手動提醒更新
notifyPropertyChanged(BR.popularity)
}
@Bindable
fun getPopularity(): Popularity {
return likes.get().let {
when {
it > 9 -> Popularity.STAR
it > 4 -> Popularity.POPULAR
else -> Popularity.NORMAL
}
}
}
}
enum class Popularity {
NORMAL,
POPULAR,
STAR
}
private fun ObservableInt.increment() {
set(get() + 1)
}
複製程式碼
- LiveData繫結實現
LiveData的實現相對就比較容易寫,LiveData其本省就是被作為一個Observer監聽模式的一個存在,如下:
class ProfileLiveDataViewModel : ViewModel() {
private val _name = MutableLiveData("Ada")
private val _lastName = MutableLiveData("Lovelace")
private val _likes = MutableLiveData(0)
val name: LiveData<String> = _name
val lastName: LiveData<String> = _lastName
val likes: LiveData<Int> = _likes
// popularity is exposed as LiveData using a Transformation instead of a @Bindable property.
val popularity: LiveData<Popularity> = Transformations.map(_likes) {
when {
it > 9 -> Popularity.STAR
it > 4 -> Popularity.POPULAR
else -> Popularity.NORMAL
}
}
fun onLike() {
_likes.value = (_likes.value ?: 0) + 1
}
}
複製程式碼
其上所實現的功能相同,就開發的便捷性而言個人比較推薦LiveData實現雙向繫結,而且LiveData本身還有其他好玩的方式等待我們的探索。本篇介紹的程式碼類似與虛擬碼性質,不具有貫通性,具體實現的使用個人還是比較推薦大家可以去看看我上面推薦的DataBinding的demo示例程式碼,那裡有你想要的一切,該有的它都有!另外,需要注意的是,在我們需要的model裡新增設定程式碼:
android {
.....
dataBinding {
enabled true
}
}
複製程式碼
歡迎關注我的個人部落格Enjoytoday,有更新更全的python、Kotlin、Java、Gradle開發相關部落格更新!