委托也叫代理蛋叼,是一種可以以代理方式控制目標(biāo)對象的訪問落包,設(shè)計(jì)模式中成為-代理模式部蛇。
Java中,我們實(shí)現(xiàn)一個(gè)代理模式咐蝇,會(huì)有以下對象涯鲁;
- Base接口,代理和被代理對象都需要實(shí)現(xiàn)的接口有序。
- BaseImpl類抹腿,實(shí)現(xiàn)Base接口,是被代理的類旭寿。
- Derived類警绩,代理類、委托類盅称,也實(shí)現(xiàn)了Base接口肩祥,一般以構(gòu)造方法或set方法注入BaseImpl類后室。
Java實(shí)現(xiàn)
- Base接口,被代理類和代理類都需要實(shí)現(xiàn)該接口
public interface Base {
void print();
}
- BaseImpl混狠,被代理類
public class BaseImpl implements Base {
private String msg;
public BaseImpl(String msg) {
this.msg = msg;
}
@Override
public void print() {
System.out.println("msg:" + msg);
}
}
- Derived岸霹,代理、委托類
public class Derived implements Base {
private Base base;
//構(gòu)造方法注入被代理類
public Derived(Base base) {
this.base = base;
}
@Override
public void print() {
//復(fù)寫B(tài)ase接口中的print()方法檀蹋,轉(zhuǎn)調(diào)被代理類的print()方法
this.base.print();
}
}
Java中并沒有對代理模式做特定的封裝和語法糖,而實(shí)際代理默認(rèn)很常用云芦。Kotlin為代理模式提供了語法糖俯逾,提供了by關(guān)鍵字來方便實(shí)現(xiàn)代理模式,by關(guān)鍵字用在代理類中舅逸,格式:by 被代理類實(shí)例桌肴,Kotlin會(huì)默認(rèn)生成代理類覆寫所有的Base接口抽象方法,都轉(zhuǎn)調(diào)被代理類琉历,如果需要特殊定義坠七,直接復(fù)寫目標(biāo)方法即可。
- Base接口旗笔,被代理類和代理類都需要實(shí)現(xiàn)該接口
interface Base {
fun print()
}
- BaseImpl彪置,被代理類
/**
* 被代理類
*/
class BaseImpl(private val msg: String) : Base {
override fun print() {
println("msg:$msg")
}
}
- Derived,代理蝇恶、委托類
/**
* 委托類拳魁,實(shí)現(xiàn)類接口,通過by關(guān)鍵字撮弧,代理具體實(shí)現(xiàn)潘懊,編譯會(huì)實(shí)現(xiàn)所有抽象方法,并且轉(zhuǎn)調(diào)給實(shí)現(xiàn)類贿衍,如果需要修改授舟,直接復(fù)寫即可
*/
class Derived(private val impl: Base) : Base by impl {
override fun print() {
impl.print()
}
}
接下來介紹Kotlin的委托,Kotlin的委托分為2種:
- 類委托贸辈,就是上面那種
- 屬性委托释树,將屬性的get、set方法委托給另外一個(gè)類
自定義屬性委托
自定義屬性委托擎淤,需要新建一個(gè)類躏哩,提供getValue()方法和setValue(),并且這個(gè)2個(gè)方法都是模板揉燃,每個(gè)代理類都是一樣的扫尺,只是方法內(nèi)的處理需要自定義。
class MyDelegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
//...
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
//...
}
}
屬性委托和類委托一樣炊汤,也是使用by關(guān)鍵字正驻,但是類委托是類名弊攘,by關(guān)鍵字后面跟著實(shí)現(xiàn)類實(shí)例名,而屬性委托中是屬性后姑曙,by關(guān)鍵字后面跟著代理類的類名襟交。
實(shí)例:
新建一個(gè)代理類MyDelegate,添加getValue()和setValue()方法伤靠,這2個(gè)方法捣域,我們簡單打印一下thisRef被代理類的引用和property屬性對象。Example類為被代理類宴合,給msg屬性焕梅,通過by關(guān)鍵字指定被MyDelegate代理。main()方法中卦洽,我們給Example的實(shí)例的msg屬性調(diào)用set()贞言、get()方法,就會(huì)轉(zhuǎn)調(diào)到MyDelegate定義的setValue()和getValue()
class Example {
var msg: String by MyDelegate()
}
/**
* 建立一個(gè)委托類阀蒂,編譯器會(huì)生成Example的get该窗、set方法轉(zhuǎn)調(diào)MyDelegate中的getValue和setValue,再將結(jié)果返回
*/
class MyDelegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, 這里委托了 ${property.name} 屬性"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$thisRef 的 ${property.name} 屬性賦值為 $value")
}
}
fun main(args: Array<String>) {
//屬性代理
val example = Example()
//訪問屬性的get方法
println(example.msg)
//訪問屬性的set方法
example.msg = "你好"
}
//輸出
com.wally.hellokt.parttwo.Example@1a6c5a9e, 這里委托了 msg 屬性
com.wally.hellokt.parttwo.Example@1a6c5a9e 的 msg 屬性賦值為 你好
屬性代理實(shí)踐:
安卓開發(fā)中蚤霞,Activity酗失、Fragment之間的Intent數(shù)據(jù)傳值,寫起來比較繁瑣级零,會(huì)寫很多getXXExtra()的代碼,而借助Kotlin的屬性代理滞乙,可以更優(yōu)雅的實(shí)現(xiàn)奏纪,將變量聲明和參數(shù)獲取合二為一。
我們新建一個(gè)Argument類(代理類)斩启,我們可以為Activity序调、Fragment提供拓展方法bindArgument,給屬性聲明代理給Argument兔簇,getValue()和setValue()分別從Activity发绢、Fragment中獲取Bundle實(shí)例,獲取對應(yīng)的數(shù)據(jù)即可垄琐。
/**
* 拓展Activity獲取方法
*/
fun <T> Activity.bindArgument(name: String, default: T): Argument<T> {
return Argument(name, default) {
this.intent?.extras ?: Bundle()
}
}
/**
* 拓展Fragment獲取方法
*/
fun <T> Fragment.bindArgument(name: String, default: T): Argument<T> {
return Argument(name, default) {
this.arguments ?: Bundle()
}
}
/**
* 屬性代理边酒,代理到Bundle上
* @param name 屬性名
* @param default 默認(rèn)值
* @param block 獲取Bundle對象的閉包
*/
class Argument<T>(private val name: String, private val default: T, private val block: () -> Bundle) : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T = findArgument(name, default)
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) = putArgument(name, value)
@Suppress("UNCHECKED_CAST")
private fun <U> findArgument(name: String, default: U): U = with(this.block()) {
val res: Any = when (default) {
is Long -> getLong(name, default)
is String -> getString(name, default)!!
is Int -> getInt(name, default)
is Boolean -> getBoolean(name, default)
is Float -> getFloat(name, default)
is Serializable -> getSerializable(name) ?: default
is Parcelable -> getParcelable(name) ?: default
else -> throw IllegalArgumentException("This type can be saved into Argument")
}
res as U
}
private fun <U> putArgument(name: String, value: U) = with(this.block()) {
when (value) {
is Long -> putLong(name, value)
is String -> putString(name, value)
is Int -> putInt(name, value)
is Boolean -> putBoolean(name, value)
is Float -> putFloat(name, value)
is Serializable -> putSerializable(name, value)
is Parcelable -> putParcelable(name, value)
else -> throw IllegalArgumentException("This type can be saved into Argument")
}
}
}
- 代理使用,我們從第一個(gè)Activity跳轉(zhuǎn)到第二個(gè)Activity狸窘,Intent配置了個(gè)一個(gè)問題Id墩朦,key為AskTeacherConstant.Extra.QUESTION_ID,默認(rèn)值為空字符串翻擒,聲明的變量為mQuestionId氓涣。省去了問題Id在Intent中g(shù)etStringExtra()的獲取牛哺,更加簡潔。
private val mQuestionId: String by bindArgument(AskTeacherConstant.Extra.QUESTION_ID, "")
標(biāo)準(zhǔn)委托
委托作為Kotlin的一大特性劳吠,提供了一些標(biāo)準(zhǔn)委托給予我們使用引润。
lazy懶加載委托
使用lazy委托,需要我們提供一個(gè)Block閉包痒玩,Block閉包內(nèi)寫我們變量的初始化邏輯淳附,這個(gè)Block只有第一個(gè)調(diào)用時(shí)初始化,后續(xù)調(diào)用則直接使用第一次的返回值蠢古。這樣可以很好的節(jié)省內(nèi)存奴曙。
- 格式為:
val(var) 變量名 by lazy {
初始化邏輯...
}
下面我們使用一個(gè)例子來講解,例如一個(gè)Human類便瑟,內(nèi)部有一個(gè)friendList變量缆毁,它是一個(gè)好友列表番川,加載比較耗時(shí)到涂,main()方法中我們訪問friendList變量,希望第一次獲取時(shí)才初始化颁督,第二次以及以后不會(huì)重復(fù)加載践啄,復(fù)用之前的結(jié)果。
fun main(args: Array<String>) {
//懶加載委托沉御,生成一個(gè)閉包屿讽,包裹獲取的值的代碼,調(diào)用被代理的屬性的get方法時(shí)調(diào)用吠裆,然后保存值起來伐谈,第二次調(diào)用則直接返回
val human = Human()
println("---------------- 第一次加載,才加載數(shù)據(jù) ----------------")
println(human.friendList)
println("---------------- 第二次加載试疙,直接輸出诵棵,不會(huì)重復(fù)加載 ----------------")
println(human.friendList)
}
class Human {
/**
* 好友列表
*/
//默認(rèn)模型為LazyThreadSafetyMode.SYNCHRONIZED同步,如果不需要?jiǎng)t可以使用NONE標(biāo)識(shí)不不要保證線程安全
val friendList by lazy(mode = LazyThreadSafetyMode.NONE) {
loadFriendList()
}
//這里模擬祝旷,加載比較耗時(shí)
private fun loadFriendList(): MutableList<String> {
println("---------------- 加載耗時(shí)的好友列表 ----------------")
return mutableListOf("Wally", "Barry", "Rose")
}
}
//輸出
---------------- 第一次加載履澳,才加載數(shù)據(jù) ----------------
---------------- 加載耗時(shí)的好友列表 ----------------
[Wally, Barry, Rose]
---------------- 第二次加載,直接輸出怀跛,會(huì)重復(fù)加載 ----------------
[Wally, Barry, Rose]
lazy關(guān)鍵字后有一個(gè)mode字段距贷,默認(rèn)不寫則為同步,所以是線程安全的吻谋,如果確保不會(huì)涉及多線程的問題忠蝗,則可以設(shè)置為LazyThreadSafetyMode.NONE,提高性能漓拾。
observable監(jiān)聽屬性的改變
Kotlin還提供了一個(gè)observable委托什湘,讓我們能監(jiān)聽到屬性值的改變长赞,類似MVVM中的屬性值改變,UI組件監(jiān)聽自動(dòng)改變闽撤。同樣需要我們提供一個(gè)block閉包來回調(diào)我們得哆,提供屬性值、舊值和新值給我們哟旗。
- 格式為:
val(var) 變量名 by Delegates.observable(初始化值) {
property, oldValue, newValue -> ...
}
舉一個(gè)列子贩据,例如MyPerson類中有一個(gè)name屬性,初始化值為Wally闸餐,我們讓它的值改變?yōu)锽arry饱亮,我們的委托Delegates.observable就會(huì)回調(diào)我們的Block,提供屬性值舍沙、舊值和新值給我們近上。
fun main(args: Array<String>) {
//屬性觀察,可以監(jiān)聽屬性的新拂铡、舊值壹无,但不能改變
val person = MyPerson()
person.name = "Barry"
}
class MyPerson {
var name: String by Delegates.observable("Wally") { property, oldValue, newValue ->
println("對象屬性值發(fā)生改變 -> 屬性名:${property.name},舊值:$oldValue感帅,新值:$newValue")
}
}
//輸出
對象屬性值發(fā)生改變 -> 屬性名:name斗锭,舊值:Wally,新值:Barry
vetoable監(jiān)聽屬性改變失球,并且可以攔截
vetoable和observable類似岖是,同樣可以監(jiān)聽到屬性值的改變,但是vetoable可以攔截值的改變实苞,而observable只能獲取不能攔截豺撑。也需要我們提供一個(gè)block閉包,返回true為攔截黔牵,不允許改變聪轿,返回false,允許改變荧止。
- 格式為:
val(var) 變量名 by Delegates.observable(初始化值) {
property, oldValue, newValue -> ...
//滿足某種條件屹电,返回true代表攔截,不允許改變
if(xxx) {
true
} else {
//返回false為不攔截跃巡,允許改變
false
}
舉個(gè)例子危号,和上個(gè)observable類型,監(jiān)聽Person2類中的name屬性素邪,默認(rèn)值為Wally外莲,我們讓它的值改變?yōu)锽arry,我們的委托Delegates.vetoable就會(huì)回調(diào)我們提供的block,提供屬性值偷线,舊值和新值磨确,要求我們返回一個(gè)boolean布爾值,我們判斷新值為Barry返回true声邦,不允許改變乏奥,其他情況返回false允許改變。
fun main(args: Array<String>) {
//屬性改變監(jiān)聽+攔截(保留原來的值)
val person2 = Person2()
person2.name = "Barry"
println("name的值為:${person2.name}")
person2.name = "Wally2"
println("name的值為:${person2.name}")
}
class Person2 {
var name: String by Delegates.vetoable("Wally") { property, oldValue, newValue ->
println("對象屬性值發(fā)生改變 -> 屬性名:${property.name}亥曹,舊值:$oldValue邓了,新值:$newValue")
//如果設(shè)置為Barry不允許設(shè)置
if (newValue == "Barry") {
println("!!!!!! <不允許設(shè)置為Barry> !!!!!!")
false
} else {
true
}
}
}
//輸出
對象屬性值發(fā)生改變 -> 屬性名:name,舊值:Wally媳瞪,新值:Barry
!!!!!! <不允許設(shè)置為Barry> !!!!!!
name的值為:Wally
對象屬性值發(fā)生改變 -> 屬性名:name骗炉,舊值:Wally,新值:Wally2
name的值為:Wally2
notNull非空代理
如果我們期望某個(gè)屬性值使用前必須賦值蛇受,那么我們可以對屬性值做可空類型句葵,但是可空類型會(huì)導(dǎo)致我們使用時(shí)必須判空,到處的判空代碼兢仰,代碼會(huì)顯得十分丑陋乍丈,可空屬性是用在某幾種階段可用時(shí)使用,如果我們的屬性賦值一次后就可以一直使用旨别,那么可以使用Delegates.notNull委托诗赌。對屬性使用Delegates.notNull委托,必須使用前賦值一次,否則會(huì)拋出異常占拍。
格式為:
var 變量名 by Delegates.notNull()
舉個(gè)例子锄奢,安卓開發(fā)中,一般我們會(huì)讓Application類作為單例执虹,方便后續(xù)直接使用,而App單例的實(shí)例在onCreate()中才初始化,那么就需要設(shè)置為可空屬性绞铃,那么App.instant屬性就需要每次使用時(shí)都判空,而我們知道他不會(huì)為空嫂侍,希望不用判空直接使用儿捧,那么我們就可以使用Delegates.notNull委托。
class App :Application() {
companion object {
var instant: App by Delegates.notNull()
}
override fun onCreate() {
super.onCreate()
instant = this;
}
}
class MainActivity: Activity{
override fun onCreate() {
super.onCreate()
val application = App.instant;
}
}
總結(jié)
委托在Kotlin是一個(gè)很重要的特性挑宠,通過委托菲盾,我們可以做到很多事情,例如findViewById能委托為一個(gè)委托類獲取各淀,而可以書寫得像聲明屬性一樣懒鉴。再或者讓Sp保存、獲取也可以做成變量聲明的形式,以及獲取Activity临谱、Fragment的Bunlde參數(shù)璃俗。Kotlin還為我們提供了標(biāo)準(zhǔn)委托,例如lazy懶加載讓我們的屬性在第一次獲取時(shí)才輸出悉默,但是變量可以書寫為變量聲明的方式城豁,還有observable、vetoable代理抄课,讓我們監(jiān)聽到屬性改變钮蛛,還可以作為MVVM模式下的ViewModel的補(bǔ)充。