Kotlin-強(qiáng)大的委托

委托也叫代理蛋叼,是一種可以以代理方式控制目標(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種:

  1. 類委托贸辈,就是上面那種
  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ǔ)充。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末剖膳,一起剝皮案震驚了整個(gè)濱河市魏颓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吱晒,老刑警劉巖甸饱,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異仑濒,居然都是意外死亡叹话,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進(jìn)店門墩瞳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來驼壶,“玉大人,你說我怎么就攤上這事喉酌∪劝迹” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵泪电,是天一觀的道長般妙。 經(jīng)常有香客問我,道長相速,這世上最難降的妖魔是什么碟渺? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮突诬,結(jié)果婚禮上苫拍,老公的妹妹穿的比我還像新娘。我一直安慰自己旺隙,他們只是感情好绒极,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著催束,像睡著了一般集峦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天塔淤,我揣著相機(jī)與錄音摘昌,去河邊找鬼。 笑死高蜂,一個(gè)胖子當(dāng)著我的面吹牛聪黎,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播备恤,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼稿饰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了露泊?” 一聲冷哼從身側(cè)響起喉镰,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惭笑,沒想到半個(gè)月后侣姆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡沉噩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年捺宗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片川蒙。...
    茶點(diǎn)故事閱讀 40,503評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蚜厉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出畜眨,到底是詐尸還是另有隱情昼牛,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布胶果,位于F島的核電站匾嘱,受9級(jí)特大地震影響斤斧,放射性物質(zhì)發(fā)生泄漏早抠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一撬讽、第九天 我趴在偏房一處隱蔽的房頂上張望蕊连。 院中可真熱鬧,春花似錦游昼、人聲如沸甘苍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽载庭。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間囚聚,已是汗流浹背靖榕。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留顽铸,地道東北人茁计。 一個(gè)月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像谓松,于是被迫代替她去往敵國和親星压。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評論 2 359