上一篇介紹了運用 Kotlin DSL 構建佈局的方法,相較於 XML,可讀性和性能都有顯著提高。若是這套 DSL 還能數據綁定就更好了,這樣就能去掉 findViewById 和 Activity 中的業務邏輯代碼(findViewById 須要遍歷 View 樹,這是耗時的,而 Activity 中的業務邏輯讓其變得愈加臃腫)。這一篇就介紹一種實現思路。android
這是該系列的第十篇,系列文章目錄以下:git
在沒有 Data Binding 以前,咱們是這樣爲控件綁定數據的:
class MainActivity : AppCompatActivity() {
private var tvName: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//'獲取控件引用'
tvName = findViewById<TextView>(R.id.tvName)
}
override fun onUserReturn(user: User){
//'在數據返回時設置控件'
tvName?.text = user.name
}
}
複製代碼
tvName
被靜態地聲明在 XML 中,程序動態地經過findViewById()
獲取引用,在數據返回的地方調用設值 API。
靜態的意味着能夠預先定義,且保持不變。而動態的偏偏相反。
對於某個特定的業務場景,除了界面佈局是靜態的以外,佈局中某個控件和哪一個數據綁定也是靜態的。這種綁定關係最初是經過「動態」代碼實現的,直到出現了Data Binding
。
它是 Google 推出的一種將數據和控件相關聯的方法。
若是把 XML 稱爲聲明型的
,那 Kotlin 代碼就是程序型的
,前者是靜態的,後者是動態的。爲了讓它倆關聯,Data Binding 的思路是把程序型的
變量引入到聲明型的
佈局中,好比下面把 User.name 綁定到 TextView 上( data_binding_activity.xml ):
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable name="user" type="com.test.User"/>
</data>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@{user.name}"/>
</layout>
複製代碼
其中User
是程序型的實體類:
package com.test
data class User(var name: ObservableField<String>, var age: ObservableField<Int>)
複製代碼
ObservableField
用於將任何類型包裝成可被觀察的對象,當對象值發生變化時,觀察者就會被通知。在 Data Binding 中,控件是觀察者。
在 Activity 中,這樣寫代碼就完成了數據綁定:
class MainActivity: AppCompatActivity() {
//'聲明在 Activity 中的數據源'
private var user:User? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//'爲 Activity 設置佈局並綁定控件'
val binding = DataBindingUtil.setContentView<DataBindingActivityBinding>(this, R.layout.data_binding_activity);
//'綁定數據源'
binding.user = this.user
}
override fun onUserReturn(user: User){
//'修改數據源'
this.user.name.set( user.name )
}
}
複製代碼
這樣寫的好處是,Activity 中不會再出現findViewById()
和各類爲控件設置屬性的方法,而只須要觀察數據源的變更。
回顧下上一篇構建佈局的 DSL :
class MainActivity : AppCompatActivity() {
//'構建佈局'
private val rootView by lazy {
ConstraintLayout {
layout_width = match_parent
layout_height = match_parent
TextView {
layout_id = "tvName"
layout_width = wrap_content
layout_height = wrap_content
textSize = 30f
textStyle = bold
align_vertical_to = parent_id
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//'將佈局設置爲 Activity 的 content view'
setContentView(rootView)
}
}
複製代碼
Activity content view 的根佈局是 ConstraintLayout,其中包含一個 TextView。爲了讓它的值和User.name
聯動,新增擴展屬性以下:
inline var TextView.bindText: LiveData<CharSequence>?
get() {
return null
}
set(value) {
//'爲 TextView 的 text 屬性綁定數據源'
observe(value) { text = it }
}
//'爲控件綁定 LiveData 類型的數據源'
fun <T> View.observe(liveData: LiveData<T>?, action: (T) -> Unit) {
(context as? LifecycleOwner)?.let { owner ->
liveData?.observe(owner, Observer { action(it) })
}
}
複製代碼
爲 View 新增一個擴展方法,用於在 View 生命週期內觀察數據源 LiveData 的變化。當數據源發生變化時執行action
。
而後就能夠像這樣爲 TextView 綁定數據源了:
class MainActivity : AppCompatActivity() {
private val nameLiveData = MutableLiveData<CharSequence>()
private val rootView by lazy {
ConstraintLayout {
layout_width = match_parent
layout_height = match_parent
TextView {
layout_id = "tvName"
layout_width = wrap_content
layout_height = wrap_content
textSize = 30f
textStyle = bold
//'綁定數據源'
bindText = nameLiveData
align_vertical_to = parent_id
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(rootView)
}
override fun onUserReturn(user: User){
//'數據源變動'
nameLiveData.value = user.name
}
}
複製代碼
佈局 DSL 和界面類定義在同一個kt
文件中,因此它能方便地訪問到各類數據源。
把數據源都抽象爲LiveData<T>
, 控件中每個須要綁定數據的屬性,均可覺得其擴展一個bindXXX
屬性,它的值是LiveData<T>
。
上面的例子雖然運用了數據綁定
和LiveData
,但仍是沿用了MVP
的架構。在業務邏輯更復雜的場景,MVP
架構下的 Activity 類就會變得愈來愈臃腫。
再來看一個業務邏輯更復雜的例子。不一樣性別的用戶名稱有不一樣顏色:
class MainActivity : AppCompatActivity() {
//'應該讓 ViewModel 持有 LiveData'
private val nameLiveData = MutableLiveData<CharSequence>()
//'構建佈局應該在 Activity 層完成'
private val rootView by lazy {
ConstraintLayout {
layout_width = match_parent
layout_height = match_parent
TextView {
layout_id = "tvName"
layout_width = wrap_content
layout_height = wrap_content
textSize = 30f
textStyle = bold
//'數據源和控件的綁定應該在Activity層完成'
bindText = nameLiveData
align_vertical_to = parent_id
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(rootView)
}
override fun showUserName(user: User){
//'數據源變動(這段業務邏輯應該寫在 ViewModel 裏面)'
nameLiveData.value = SpannableStringBuilder(user.name).apply {
setSpan(ForegroundColorSpan(Color.RED), 0, it.name.indexOf(" "), Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
val color = if (user.gender == 1) "#b300ff00" else "#b3ff00ff"
setSpan(ForegroundColorSpan(Color.parseColor(color)), user.name.indexOf(" "), user.name.lastIndex + 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE)
}
}
}
複製代碼
這裏的業務邏輯是:用戶的姓和名之間會以空格分隔,將用戶的姓展現爲紅色,而名隨性別的變化而變色。
demo 是按照 MVP 來寫的,但沒有展現全部架構的細節,用文字補充以下:
showUserName()
通知界面刷新(由 Activity 實現)。如有了數據綁定,則能夠把 Presenter 和 Activity 間通訊都去掉,這也正是 MVP 模式被詬病的地方,即 View 層接口會隨着業務邏輯而膨脹。
若改用 MVVM 架構從新實現一邊 demo,應該是這樣的:
這樣 Activity 裏面就再也不有 findViewById 和 業務邏輯代碼。
想要擴展這套 「DSL + 數據綁定」 方案也極爲方便,好比爲 ImageView 添加一個綁定 url 的屬性:
inline var ImageView.bindSrc: LiveData<Bitmap>?
get() {
return null
}
set(value) {
observe(value) { setImageBitmap(it) }
}
複製代碼
先爲 ImageView 控件擴展一個名爲bindSrc
的屬性,它是LiveData<Bitmap>?
類型的。
而後在 構建佈局 DSL 中就能夠這樣使用該屬性(爲簡單起見仍是用 MVP):
class FirstFragment : Fragment() {
//'數據源'
val avatarLiveData = MutableLiveData<Bitmap>()
//'數據源發生變動'
private val target = object : SimpleTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap?, glideAnimation: GlideAnimation<in Bitmap>?) {
avatarLiveData.value = resource
}
}
//構建佈局
private val rootView by lazy {
ConstraintLayout {
layout_width = match_parent
layout_height = match_parent
ImageView {
layout_width = 40
layout_height = 40
//'和數據源綁定'
bindSrc = avatarLiveData
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return rootView
}
override fun showUser(user: User){
//'觸發加載數據源'
Glide.with(context).load(user.url).asBitmap().into(target)
}
}
複製代碼
相較於 DataBinding 中自定義 BindingAdapter 更簡單一丟丟。