前言
即學(xué)即用Android Jetpack系列Blog的目的是通過學(xué)習(xí)Android Jetpack
完成一個簡單的Demo沽瞭,本文是即學(xué)即用Android Jetpack系列Blog的第二篇坛梁。
Google在2018年推出Android Jetpack
憾儒,本人最近在學(xué)習(xí)Android Jetpack
闸餐,如果你有研究過Android Jetpack
,你會發(fā)現(xiàn)Livedata幻妓,ViewModel和Livecycles等一系列Android Jetpack
組件非常適用于實現(xiàn)MVVM仔引,因此鄙皇,在進行Android Jetpack
的下一步研究之前先巴,我們有必要學(xué)習(xí)一下MVVM設(shè)計模式以及Android中實現(xiàn)MVVM的Data Binding
組件其爵。
語言:kotlin
我的Demo:https://github.com/mCyp/Hoo
目錄
一、介紹
1. MVVM介紹
MVVM(全稱Model-View-ViewModel)同MVC
和MVP
一樣伸蚯,是邏輯分層解偶的模式(如果你還不了解MVC
和MVP
摩渺,建議還是提前了解一下)。
1.1 結(jié)構(gòu)圖
從上圖我們可以了解到MVVM的三要素朝卒,他們分別是:
- View層:xml证逻、Activity乐埠、Fragment抗斤、Adapter和View等
- Model層:數(shù)據(jù)源(本地數(shù)據(jù)和網(wǎng)絡(luò)數(shù)據(jù)等)
- ViewModel層:View層處理數(shù)據(jù)以及邏輯處理
2. Data Binding介紹
Data Binding
不算特別新的東西,2015年Google就推出了丈咐,但即便是現(xiàn)在瑞眼,很多人都沒有學(xué)習(xí)過它,我就是這些工程師中的一位棵逊,因為我覺得MVP已經(jīng)足夠幫我處理日常的業(yè)務(wù)伤疙,Android Jetpack
的出現(xiàn)辆影,是我研究Data Binding
的一個契機徒像。
在進行下文之前蛙讥,我有必要聲明一下旁涤,MVVM
和Data Binding
是兩個不同的概念菌羽,MVVM是一種架構(gòu)模式,而Data Binding是一個實現(xiàn)數(shù)據(jù)和UI綁定的框架注祖,是構(gòu)建MVVM模式的一個工具份蝴。
2.1 學(xué)習(xí)姿勢
我依然認為官方文檔是最好的學(xué)習(xí)途徑:
官方文檔:Data Binding Library
谷歌實驗室:官方教程
官方Demo地址:https://github.com/googlecodelabs/android-databinding
二、實戰(zhàn)
在這里氓轰,我打算先在上一節(jié)即學(xué)即用Android Jetpack - Navigation的基礎(chǔ)代碼上進行拓展(如有涉及到Navigation
的代碼婚夫,我會注明),本文會在登錄和注冊模塊的基礎(chǔ)上進行講解署鸡,后期如有需要案糙,會拓展到其他模塊。
效果圖靴庆,和之前的有點不一樣:
第一步 在app模塊下的build.gradle
文件添加內(nèi)容
android {
...
dataBinding {
enabled true
}
}
第二步 構(gòu)建LoginModel
創(chuàng)建登錄的LoginModel
时捌,LoginModel
主要負責登錄邏輯的處理以及兩個輸入框內(nèi)容改變的時候數(shù)據(jù)更新的處理:
class LoginModel constructor(name: String, pwd: String, context: Context) {
val n = ObservableField<String>(name)
val p = ObservableField<String>(pwd)
var context: Context = context
/**
* 用戶名改變回調(diào)的函數(shù)
*/
fun onNameChanged(s: CharSequence) {
n.set(s.toString())
}
/**
* 密碼改變的回調(diào)函數(shù)
*/
fun onPwdChanged(s: CharSequence, start: Int, before: Int, count: Int) {
p.set(s.toString())
}
fun login() {
if (n.get().equals(BaseConstant.USER_NAME)
&& p.get().equals(BaseConstant.USER_PWD)
) {
Toast.makeText(context, "賬號密碼正確", Toast.LENGTH_SHORT).show()
val intent = Intent(context, MainActivity::class.java)
context.startActivity(intent)
}
}
}
我相信同學(xué)們可能會對ObservableField
存在疑惑,那么ObservableField
是什么呢炉抒?它其實是一個可觀察的域奢讨,通過泛型來使用,可以使用的方法也就三個:
方法 | 作用 |
---|---|
ObservableField(T value) |
構(gòu)造函數(shù)焰薄,設(shè)置可觀察的域 |
T get() |
獲取可觀察的域的內(nèi)容拿诸,可以使用UI控件監(jiān)測它的值 |
set(T value) |
設(shè)置可觀察的域,設(shè)置成功之后塞茅,會通知UI控件進行更新 |
不過亩码,除了使用ObservableField
之外,Data Binding
為我們提供了基本類型的ObservableXXX
(如ObservableInt
)和存放容器的ObservableXXX
(如ObservableList<T>
)等野瘦,同樣描沟,如果你想讓你自定義的類變成可觀察狀態(tài),需要實現(xiàn)Observable
接口鞭光。
我們再回頭看看LoginModel
這個類吏廉,它其實只有分別用來觀察name
和pwd
的成員變量n
和p
,外加一個處理登錄邏輯的方法惰许,非常簡單席覆。
第三步 創(chuàng)建布局文件
引入Data Binding
之后的布局文件的使用方式會和以前的布局使用方式有很大的不同,且聽我一一解釋:
標簽名 | 作用 |
---|---|
layout | 用作布局的根節(jié)點啡省,只能包裹一個View標簽娜睛,且不能包裹merge標簽髓霞。 |
data | Data Binding的數(shù)據(jù),只能存在一個data標簽畦戒。 |
variable |
data 中使用方库,數(shù)據(jù)的變量標簽,type 屬性指明變量的類障斋,如com.joe.jetpackdemo.viewmodel.LoginModel 纵潦。name 屬性指明變量的名字,方便布局中使用垃环。 |
import |
data 中使用邀层,需要使用靜態(tài)方法和靜態(tài)常量,如需要使用View.Visble屬性的時候遂庄,則需導(dǎo)入<import type="android.view.View"/> 寥院。type 屬性指明類的路徑,如果兩個import 標簽導(dǎo)入的類名相同涛目,則可以使用alias 屬性聲明別名秸谢,使用的時候直接使用別名即可。 |
include |
View標簽中使用霹肝,作用同普通布局中的include 一樣估蹄,需要使用bind:<參數(shù)名> 傳遞參數(shù) |
我們再看一下LoginFragment
下的fragment_login.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>
<!--需要的viewModel,通過mBinding.vm=mViewMode注入-->
<variable
name="model"
type="com.joe.jetpackdemo.viewmodel.LoginModel"/>
<variable
name="activity"
type="androidx.fragment.app.FragmentActivity"/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/txt_cancel"
android:onClick="@{()-> activity.onBackPressed()}"
/>
<TextView
android:id="@+id/txt_title"
app:layout_constraintTop_toTopOf="parent"
.../>
<EditText
android:id="@+id/et_account"
android:text="@{model.n.get()}"
android:onTextChanged="@{(text, start, before, count)->model.onNameChanged(text)}"
...
/>
<EditText
android:id="@+id/et_pwd"
android:text="@{model.p.get()}"
android:onTextChanged="@{model::onPwdChanged}"
...
/>
<Button
android:id="@+id/btn_login"
android:text="Sign in"
android:onClick="@{() -> model.login()}"
android:enabled="@{(model.p.get().isEmpty()||model.n.get().isEmpty()) ? false : true}"
.../>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
variable
有兩個:
-
model
:類型為com.joe.jetpackdemo.viewmodel.LoginModel
,綁定用戶名詳見et_account
EditText中的android:text="@{model.n.get()}"
沫换,當EditText輸入框內(nèi)容變化的時候有如下處理android:onTextChanged="@{(text, start, before, count)->model.onNameChanged(text)}"
臭蚁,以及登錄按鈕處理android:onClick="@{() -> model.login()}"
。 -
activity
:類型為androidx.fragment.app.FragmentActivity
讯赏,主要用來返回按鈕的事件處理垮兑,詳見txt_cancel
TextView的android:onClick="@{()-> activity.onBackPressed()}"
。
對于以上的內(nèi)容待逞,我仍然有知識點需要講解:
1. 屬性的引用
如果想使用ViewModel中成員變量甥角,如直接使用model.p
。
2. 事件綁定
事件綁定包括方法引用
和監(jiān)聽綁定
:
-
方法引用
:參數(shù)類型和返回類型要一致识樱,參考et_pwd
EditText的android:onTextChanged
引用。 -
監(jiān)聽綁定
:相比較于方法引用
震束,監(jiān)聽綁定
的要求就沒那么高了怜庸,我們可以使用自行定義的函數(shù),參考et_account
EditText的android:onTextChanged
引用垢村。
3. 表達式
如果你注意到了btn_login
Button在密碼沒有內(nèi)容的時候是灰色的:
是因為它在android:enabled
使用了表達式:@{(model.p.get().isEmpty()||model.n.get().isEmpty()) ? false : true}
割疾,它的意思是用戶名和密碼為空的時候登錄的enable
屬性為false,這是普通的三元表達式嘉栓,除了上述的||
和三元表達式之外宏榕,Data Binding
還支持:
- 運算符 + - / * %
- 字符串連接 +
- 邏輯與或 && ||
- 二進制 & | ^
- 一元 + - ! ~
- 移位 >> >>> <<
- 比較 == > < >= <= (Note that < needs to be escaped as <)
- instanceof
- Grouping ()
- Literals - character, String, numeric, null
- Cast
- 方法調(diào)用
- 域訪問
- 數(shù)組訪問
- 三元操作符
除了上述之外拓诸,Data Binding
新增了空合并操作符??
,例如android:text="@{user.displayName ?? user.lastName}"
麻昼,它等價于android:text="@{user.displayName != null ? user.displayName : user.lastName}"
奠支。
第四步 生成綁定類
我們的布局文件創(chuàng)建完畢之后,點擊Build
下面的Make Project
抚芦,讓系統(tǒng)幫我生成綁定類倍谜,生成綁定的類如下:
下面我們只需在
LoginFragment
完成綁定即可,綁定操作既可以使用上述生成的FragmentLoginBinding
也可以使用自帶的DataBindingUtil
完成:
1. 使用DataBindingUtil
我們可以看一下DataBindingUtil
的一些常用Api:
函數(shù)名 | 作用 |
---|---|
setContentView |
用來進行Activity下面的綁定 |
inflate |
用來進行Fragment下面的綁定 |
bind |
用來進行View的綁定 |
LoginFragment
綁定代碼如下:
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: FragmentLoginBinding = DataBindingUtil.inflate(
inflater
, R.layout.fragment_login
, container
, false
)
loginModel = LoginModel("","",context!!)
binding.model = loginModel
binding.activity = activity
return binding.root
}
2. 使用生成的FragmentLoginBinding
使用方法與第一種類似叉抡,僅需將生成方式改成val binding = FragmentLoginBinding.inflate( inflater , container , false )
即可
運行一下代碼尔崔,開始圖的效果就出現(xiàn)了。
三褥民、更多
Data Binding
還有一些有趣的功能季春,為了讓同學(xué)們了解到更多的知識,我們在這里有必要探討一下:
1. 布局中屬性的設(shè)置
1.1 有屬性有setter的情況
如果XXXView類有成員變量borderColor
消返,并且XXXView類有setBoderColor(int color)
方法鹤盒,那么在布局中我們就可以借助Data Binding
直接使用app:borderColor
這個屬性,不太明白侦副?沒關(guān)系侦锯,以DrawerLayout
為例,DrawerLayout
沒有聲明app:scrimColor
秦驯、app:drawerListener
尺碰,但是DrawerLayout
有mScrimColor:int
、mListener:DrawerListener
這兩個成員變量并且具有這兩個屬性的setter
的方法译隘,他就可以直接使用app:scrimColor
亲桥、app:drawerListener
這兩個屬性,代碼如下:
<android.support.v4.widget.DrawerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:scrimColor="@{@color/scrim}"
app:drawerListener="@{fragment.drawerListener}">
1.2 沒有setter但是有相關(guān)方法
還用XXXView為例固耘,它有成員變量borderColor
题篷,這次設(shè)置borderColor
的方法是setBColor
(總有程序員亂寫方法名~),強行用app:borderColor
顯然是行不通的厅目,可以這樣用的前提是必須有setBoderColor(int color)
方法番枚,顯然setBColor
不匹配,但我們可以通過BindingMethods
注解實現(xiàn)app:borderColor
的使用损敷,代碼如下:
@BindingMethods(value = [
BindingMethod(
type = 包名.XXXView::class,
attribute = "app:borderColor",
method = "setBColor")])
1.3 自定義屬性
這次不僅沒setter
方法葫笼,甚至連成員變量都需要自帶(條件越來越刻苦~),這次我們的目標就是給EditText添加文本監(jiān)聽器拗馒,先在LoginModel
中自定義一個監(jiān)聽器并使用@BindingAdapter
注解:
// SimpleWatcher 是簡化了的TextWatcher
val nameWatcher = object : SimpleWatcher() {
override fun afterTextChanged(s: Editable) {
super.afterTextChanged(s)
n.set(s.toString())
}
}
@BindingAdapter("addTextChangedListener")
fun addTextChangedListener(editText: EditText, simpleWatcher: SimpleWatcher) {
editText.addTextChangedListener(simpleWatcher)
}
這樣我們就可以在布局文件中對EditText愉快的使用app:addTextChangedListener
屬性了:
<EditText
android:id="@+id/et_account"
android:text="@{model.n.get()}"
app:addTextChangedListener="@{model.nameWatcher}"
...
/>
效果與我們之前使用的時候一樣
2. 雙向綁定
使用雙向綁定可以簡化我們的代碼路星,比如我們上面的EditText在實現(xiàn)雙向綁定之后既不需要添加SimpleWatcher
也不需要用方法調(diào)用,怎么實現(xiàn)呢诱桂?代碼如下:
<EditText
android:id="@+id/et_account"
android:text="@={model.n.get()}"
...
/>
僅僅在將@{model.n.get()}
替換為@={model.n.get()}
洋丐,多了一個=
號而已呈昔,需要注意的是,屬性必須是可觀察的友绝,可以使用上面提到的ObservableField
堤尾,也可以自定義實現(xiàn)BaseObservable
接口,雙向綁定的時候需要注意無限循環(huán)九榔,更多關(guān)于雙向綁定還請查看官方文檔哀峻。
四、總結(jié)
Data Binding
的介紹可能沒有那么全面哲泊,基本使用沒什么問題了剩蟀,想要了解更多可以查看官方文檔呦~,本人水平有限切威,難免理解有誤差育特,歡迎指正。Over~
參考文章:
??如果覺得本文不錯先朦,可以查看Android Jetpack
系列的其他文章:
第一篇:《即學(xué)即用Android Jetpack - Navigation》
第三篇:《即學(xué)即用Android Jetpack - ViewModel & LiveData》
第四篇:《即學(xué)即用Android Jetpack - Room》
第五篇:《即學(xué)即用Android Jetpack - Paging》
第六篇:《即學(xué)即用Android Jetpack - WorkManger》
第七篇:《即學(xué)即用Android Jetpack - Startup》
第八篇:《即學(xué)即用Android Jetpack - Paging 3》
項目總結(jié)篇:《學(xué)習(xí)Android Jetpack? 實戰(zhàn)和教程這里全都有缰冤!》