優(yōu)雅的使用Kotlin

優(yōu)雅的使用Kotlin

這篇文章并不會(huì)從零開(kāi)始來(lái)教你怎么使用Kotlin闷尿,Kotlin對(duì)于筆者來(lái)說(shuō)也是一個(gè)新概念母赵,由于大勢(shì)所趨(Google所有的官方Demo都開(kāi)始用Kotlin,各大第三方Library也開(kāi)始適配Kotlin)授药,所以不會(huì)Koltin寸步難行呀~士嚎,經(jīng)過(guò)一段時(shí)間的學(xué)習(xí)和練習(xí),對(duì)Kotlin有了一點(diǎn)淺薄的理解悔叽,在此記錄下來(lái)莱衩,希望能給大家?guī)?lái)幫助。

開(kāi)始進(jìn)入主題娇澎,我打算從下面幾個(gè)方面來(lái)給大家總結(jié)一些使用技巧和個(gè)人的理解(不足之處還請(qǐng)大家多多指出)

  • 類(lèi)(類(lèi)笨蚁、接口、數(shù)據(jù)類(lèi))
  • 函數(shù)(擴(kuò)展函數(shù))
  • DSL趟庄、擴(kuò)展庫(kù)和Anko
  • 協(xié)程

類(lèi)

如何創(chuàng)建一個(gè)Kotlin類(lèi)

我們先看看我們熟悉的Java如何創(chuàng)建一個(gè)普通的類(lèi):

class Player{
    private String name;
    public Player(String name){
        this.name = name;
    }
    
    public void play(){
        system.out.println(name+" start play");
    }
}

下面是Kotlin聲明的類(lèi)

class Player(val name :String){
    fun play(){
        println("$name start paly")
    }
}

看上去好像差別不是很大嘛括细,其實(shí)對(duì)于學(xué)過(guò)Java的人來(lái)說(shuō),Kotlin還是比較相近的戚啥,只需要了解一些新特性奋单,上手還是很快的。

在上面的例子中猫十,Kotlin的構(gòu)造函數(shù)直接聲明在類(lèi)的后面览濒,并且變量的聲明也可以在函數(shù)的參數(shù)中聲明,最后字符串的輸出拖云,直接使用${變量名}的形式輸出

將上面的Kotlin代碼反編譯成Java代碼如下:

public final class Player {
   @NotNull
   private final String name;

   public final void play() {
      String var1 = this.name + " start paly";
      System.out.println(var1);
   }

   @NotNull
   public final String getName() {
      return this.name;
   }

   public Player(@NotNull String name) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
   }
}

基本上一樣贷笛,接下來(lái)我們就開(kāi)始看看Kotlin怎樣常見(jiàn)一個(gè)類(lèi),和我們Java的類(lèi)有什么具體的區(qū)別宙项。

構(gòu)造函數(shù)

相信大家對(duì)Java的構(gòu)造函數(shù)已經(jīng)很熟悉了:

  • 多個(gè)構(gòu)造函數(shù)瘤运,構(gòu)造函數(shù)之間是重載的
  • 可以不聲明構(gòu)造函數(shù)诬留,編譯器會(huì)自動(dòng)生成一個(gè)無(wú)參的構(gòu)造函數(shù)

Kotlin中蝗砾,一個(gè)類(lèi)中可以有一個(gè)主構(gòu)造函數(shù)和多個(gè)次構(gòu)造函數(shù)。主構(gòu)造函數(shù)就是聲明在類(lèi)頭的構(gòu)造函數(shù)洞就,次構(gòu)造函數(shù)必須委托主構(gòu)造函數(shù),文字太枯燥拢驾,舉幾個(gè)例子就一目了然了。

//這是沒(méi)有省略所有關(guān)鍵字的類(lèi)聲明改基,當(dāng)然如果沒(méi)有可見(jiàn)性的修飾符(默認(rèn)是public)繁疤,可以省略看見(jiàn)性修飾符和constructor
class Player public constructor(val name :String){
    
    //如果想在構(gòu)造函數(shù)中定一些變量或者創(chuàng)建對(duì)象,可以直接在類(lèi)體中定義秕狰,也可以在init{...}代碼塊中去定義
    val id = "111" 
    
    init{
        println("$id 是在初始化的代碼塊中")
    }
   
    fun play(){
        println("$name start paly")
    }
    
    // 調(diào)用player("222").play()輸出下面字符串:
    // 111 是在初始化的代碼塊中
    // 222 start paly
}


// 主構(gòu)造函數(shù)和次構(gòu)造函數(shù)并存
//初始化塊中的代碼實(shí)際上會(huì)成為主構(gòu)造函數(shù)的一部分稠腊。委托給主構(gòu)造函數(shù)會(huì)作為次構(gòu)造函數(shù)的第一條語(yǔ)句,因此所有初始化塊中的代碼都會(huì)在次構(gòu)造函數(shù)體之前執(zhí)行鸣哀。即使該類(lèi)沒(méi)有主構(gòu)造函數(shù)架忌,這種委托仍會(huì)隱式發(fā)生,并且仍會(huì)執(zhí)行初始化塊
class Player(val name: String) {
    val id = "111"

    init {
        println("$id 是在初始化的代碼塊中")
    }

    constructor(gender: String, name: String) : this("123") {
        println("我是 $gender 性")
    }

    fun play() {
        println("$name start paly")
    }

}


Kotlin構(gòu)造函數(shù)小結(jié):

  1. 可以有一個(gè)主構(gòu)造函數(shù)和多個(gè)次構(gòu)造函數(shù)
  2. 可以只有主構(gòu)造函數(shù)和次構(gòu)造函數(shù)
  3. 主我衬、次構(gòu)造函數(shù)同事存在的時(shí)候叹放,次構(gòu)造函數(shù)必須直接或者間接的委托到住構(gòu)造函數(shù)
  4. 沒(méi)有聲明主構(gòu)構(gòu)造函數(shù),會(huì)自動(dòng)生成一個(gè)無(wú)參數(shù)的主構(gòu)造函數(shù)挠羔,這點(diǎn)與java一樣井仰。
  5. 在主構(gòu)造函數(shù)中你可以設(shè)置默認(rèn)的值。

數(shù)據(jù)類(lèi)

個(gè)人覺(jué)得Kotlin的數(shù)據(jù)類(lèi)使用起來(lái)真的很方便破加,省去我們很多工作量(媽媽從此再也不擔(dān)心我寫(xiě)數(shù)據(jù)類(lèi)了~~)俱恶。

先用Java寫(xiě)一個(gè)數(shù)據(jù)類(lèi),方便我們后面做對(duì)比

public class User {
    private String id;
    private String name;
    private int gender;
    private String avatar;
    private int age;

    public User(String id) {
        this.id = id;
    }
    

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getGender() {
        return gender;
    }

    public void setGender(int gender) {
        this.gender = gender;
    }

    public String getAvatar() {
        return avatar;
    }

    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

可以看到聲明一個(gè)數(shù)據(jù)類(lèi)將近需要60多行的代碼范舀,還是在沒(méi)有重寫(xiě)toString(),equals(),hashCode()等方法的前提下合是。

我們?cè)賮?lái)看看Kotlin如果實(shí)現(xiàn)一個(gè)對(duì)象類(lèi)

data class User(val id: String, var name: String = "", var gender: Int = 0, var avatar: String = "", var age: Int = 0)

Kotlin只需要一行代碼就搞定,是不是很優(yōu)雅锭环,優(yōu)雅的背后是編譯器為我們做了許多的事情聪全,編譯器會(huì)自動(dòng)的從主構(gòu)造函數(shù)中根據(jù)所有聲明的屬性生成以下的函數(shù)

  • equals()/hasCode()
  • toString()
  • copy()函數(shù)

實(shí)用小竅門(mén)
當(dāng)我們使用定義好的類(lèi)時(shí),如果有些屬性是不必要的辅辩,可以設(shè)置默認(rèn)值荔烧,那么我們就可以按照以下的方式調(diào)用(拿上面的User做例子):

//Test.kt

class Test{
    
    fun main(args: Array<String>) {
        //姿勢(shì)一,只賦值id屬性
        User("111")
        //姿勢(shì)二,打亂順序汽久,再也不用擔(dān)心在參數(shù)居多的情況下鹤竭,搞不清參數(shù)的含義了
         User(name = "liyachao", id = "222")
    }
}

在日常的工作中基本上熟悉上面兩個(gè)知識(shí)點(diǎn),就能很好的工作了景醇,關(guān)于類(lèi)還有

  • 枚舉類(lèi) (和Java使用基本一樣)
  • 密閉類(lèi) (sealed class)枚舉類(lèi)的擴(kuò)展臀稚,每個(gè)枚舉常量只存在一個(gè)實(shí)例,而密閉類(lèi)的一個(gè)子類(lèi)可以有多個(gè)實(shí)例三痰,暫時(shí)還沒(méi)有用到吧寺。
  • 嵌套類(lèi)窜管,與Java的靜態(tài)內(nèi)部類(lèi)一樣
  • 內(nèi)部類(lèi),和Java一樣稚机,內(nèi)部類(lèi)持有一個(gè)外部類(lèi)的對(duì)象引用
  • 匿名內(nèi)部類(lèi)幕帆。

函數(shù)

Kotlin在函數(shù)使用上增加很多的語(yǔ)法糖赖条,我們可以方便失乾、愉快的編寫(xiě)代碼,也使得代碼看起來(lái)非常簡(jiǎn)潔易懂纬乍,總是就是更加優(yōu)雅

擴(kuò)展函數(shù)

Kotlin允許在不改變已有類(lèi)的情況下碱茁,為某個(gè)類(lèi)添加新的函數(shù),這個(gè)特性叫做擴(kuò)展函數(shù)仿贬。有了這個(gè)特性纽竣,那么我們豈不是可以給任意的類(lèi)添加我們想要的函數(shù),再也不用添加X(jué)XXUtils或者XXXTool的類(lèi)了茧泪,Kotlin使用了什么黑科技嗎蜓氨?讓我們看看

假如我們要給Context添加一個(gè)toast的功能,可以這樣寫(xiě)

fun Context.toast(str: String) = Toast.makeText(this, str, Toast.LENGTH_LONG).show()

// 當(dāng)我們需要toast的時(shí)候队伟,我們可以這樣調(diào)用

context.toast("test")

是不是很神奇语盈,我們通過(guò)Android Studio反編譯看下.java文件是怎樣的

//聲明 toast方法的類(lèi)
public final class TestKt {
   public static final void toast(@NotNull Context $receiver, @NotNull String str) {
      Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
      Intrinsics.checkParameterIsNotNull(str, "str");
      Toast.makeText($receiver, (CharSequence)str, 1).show();
   }
}
// 調(diào)用toast的地方
TestKt.toast(context, "test");

從上面代碼我們不難明白,其實(shí)Kotlin的擴(kuò)展函數(shù)也是給我們生成一個(gè)工具類(lèi)缰泡,在掉用的地方幫我們換成這個(gè)類(lèi)的調(diào)用刀荒,原來(lái)如此的簡(jiǎn)單。

說(shuō)到擴(kuò)展函數(shù)棘钞,就不得不提的幾個(gè)基礎(chǔ)庫(kù)的擴(kuò)展函數(shù)缠借,也叫做作用域函數(shù),它們是:T.run宜猜、T.let泼返、T.also、T.apply 以及run姨拥、width兩個(gè)函數(shù)绅喉。對(duì)于這幾個(gè)函數(shù)的使用剛開(kāi)始的時(shí)候我還是很懵逼的,只是大概了解用法叫乌,以及帶來(lái)的鏈?zhǔn)秸{(diào)用的便捷柴罐,但是它們?cè)趺词裁辞闆r下使用,我還真是搞不明白憨奸,通常情況下都是想起拿個(gè)用哪個(gè)革屠。但是這篇文章很好的解釋了各個(gè)函數(shù)的用法和情況。

run

這是最簡(jiǎn)單的作用域,看下源代碼似芝,Kotlin是怎樣定義這個(gè)方法的:

/**
 * Calls the specified function [block] and returns its result.
 * 其實(shí)就是提供了一個(gè)作用域的方法那婉,返回函數(shù)block的結(jié)果
 */
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

//舉個(gè)例子,可以這樣使用

fun test(){
    var mood = "I am sad"
    
    run {
        val mod = "I am happy"
        Toast.makeText($receiver, (CharSequence)str, 1)
    }.show()
}

在這個(gè)例子中看起來(lái)好像并沒(méi)有什么卵用党瓮,但是其實(shí)還是有一些好處的(雖然我在實(shí)際開(kāi)發(fā)中沒(méi)有用到過(guò))

  • 分隔開(kāi)的作用域详炬。
  • 可以根據(jù)作用域返回的結(jié)果進(jìn)行接下來(lái)的操作

普通函數(shù)和擴(kuò)展函數(shù)(with VS T.run)

還是先看下這兩個(gè)函數(shù)的源碼

/**
 * Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

從源碼上看基本上是沒(méi)有區(qū)別的,都是接受一個(gè)函數(shù)作為參數(shù)寞奸,然后返回這個(gè)函數(shù)的結(jié)果呛谜。它們之間唯一不同的在于with是一個(gè)普通函數(shù),而T.run是一個(gè)擴(kuò)展函數(shù)蝇闭,那么問(wèn)題來(lái)了呻率,它們各自使用的優(yōu)點(diǎn)是什么呢硬毕?舉個(gè)例子

// 對(duì)于user的處理

fun test(){
    with(user){
        id = "222"
        name = "test"
    }
    
    user.run{
        id = "222"
        name = "test"
    }
    
    // 好像并沒(méi)有什么區(qū)別呻引,想想一下,如果user可能為null呢吐咳,該如何去實(shí)現(xiàn)
    
    with(user){
        this?.id = "222"
        this?.name = "test"
    }
    
    user?.run{
        id = "222"
        name = "test"
    }
}

第二種場(chǎng)景下逻悠,顯然T.run擴(kuò)展函數(shù)更好,因?yàn)槲覀兛梢栽谑褂盟皩?duì)可空性進(jìn)行檢查韭脊。

this和it區(qū)別(T.run VS T.let)

如果我們對(duì)比T.run和T.let兩個(gè)函數(shù)也是非常相似的童谒,上源碼

/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

從源碼中可以看出來(lái),它們唯一的區(qū)別是接受的參數(shù)不一樣沪羔。

  • T.run僅僅是被當(dāng)做了block:T.()擴(kuò)展函數(shù)的調(diào)用塊饥伊。因此在其作用域內(nèi),T可以被this指代蔫饰。在編碼過(guò)程中琅豆,大多數(shù)情況下this是可以被省略的。
  • T.let將自己本身傳遞到函數(shù)block:(T)篓吁。因此這個(gè)類(lèi)似于傳遞一個(gè)lambda表達(dá)式作為參數(shù)茫因。它可以在函數(shù)作用域內(nèi)部是用哪個(gè)it來(lái)指代,所以把這個(gè)稱(chēng)為傳遞it參數(shù)杖剪。

舉個(gè)例子

user?.run{
    println("my name is $name)
}

user?.let{
    println("my name is $it.name")
}

從上面的例子來(lái)看冻押,似乎T.run比T.let更加優(yōu)越,但是T.let有一些微妙的優(yōu)勢(shì)(其實(shí)我覺(jué)得沒(méi)什么卵用盛嘿,都差不多洛巢,就看你喜歡用哪個(gè))

  1. T.let函數(shù)提供了一種更清晰的區(qū)分方式去使用給定的變量函數(shù)/成員與外部類(lèi)函數(shù)/成員。
  2. 例如當(dāng)this作為函數(shù)的參數(shù)傳遞時(shí)次兆,this不能被省略狼渊,并且it寫(xiě)起來(lái)比this更簡(jiǎn)潔,更清晰。
  3. T.let允許更好地命名已轉(zhuǎn)換的已使用變量狈邑,即可以將it轉(zhuǎn)換為其他有含義名稱(chēng)城须,而 T.run則不能,內(nèi)部只能用this指代或者省略米苹。

return this和return 函數(shù)結(jié)果(T.let VS T.also)

照例糕伐,我們先看下它們的源碼

/**
 * Calls the specified function [block] with `this` value as its argument and returns `this` value.
 */
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

從上面源碼可以看出來(lái)蘸嘶,它們唯一的不同就是T.let返回一個(gè)不同類(lèi)型的值也就是函數(shù)執(zhí)行的結(jié)果良瞧,而T.alse返回T類(lèi)型本身

舉個(gè)例子

val original = "abc"
// Evolve the value and send to the next chain
original.let {
    println("The original String is $it") // "abc"
    it.reversed() // evolve it as parameter to send to next let
}.let {
    println("The reverse String is $it") // "cba"
    it.length  // can be evolve to other type
}.let {
    println("The length of the String is $it") // 3
}
// Wrong
// Same value is sent in the chain (printed answer is wrong)
original.also {
    println("The original String is $it") // "abc"
    it.reversed() // even if we evolve it, it is useless
}.also {
    println("The reverse String is ${it}") // "abc"
    it.length  // even if we evolve it, it is useless
}.also {
    println("The length of the String is ${it}") // "abc"
}
// Corrected for also (i.e. manipulate as original string
// Same value is sent in the chain 
original.also {
    println("The original String is $it") // "abc"
}.also {
    println("The reverse String is ${it.reversed()}") // "cba"
}.also {
    println("The length of the String is ${it.length}") // 3
}

T.also似乎看上去沒(méi)有意義为肮,因?yàn)槲覀兛梢院茌p易的將它們組合成為一個(gè)模塊戒悠。仔細(xì)想想,T.also還是有一些好處的

  1. 它可以對(duì)一個(gè)對(duì)象提供非常清晰的區(qū)分過(guò)程寒矿,將其區(qū)分成更小的功能區(qū)域
  2. 在使用之前突琳,可以進(jìn)行強(qiáng)大的自我操作,創(chuàng)建一個(gè)鏈?zhǔn)秸{(diào)用的操作符相。

當(dāng)把這個(gè)鏈?zhǔn)秸{(diào)用連接起來(lái)拆融,就非常牛逼了蠢琳。

// Normal approach
fun makeDir(path: String): File  {
    val result = File(path)
    result.mkdirs()
    return result
}
// Improved approach
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdirs() }

一行代碼搞定,是不是很優(yōu)雅

回顧所有的屬性值

通過(guò)回顧這三個(gè)屬性特征镜豹,我們可以非常清楚函數(shù)的行為傲须。說(shuō)明一下T.apply函數(shù):

  1. 擴(kuò)展函數(shù)
  2. 傳遞this作為參數(shù)
  3. 返回this(自己本身)

舉個(gè)例子

// Normal approach
fun createInstance(args: Bundle) : MyFragment {
    val fragment = MyFragment()
    fragment.arguments = args
    return fragment
}
// Improved approach
fun createInstance(args: Bundle) 
              = MyFragment().apply { arguments = args }

或者我們也可以讓無(wú)鏈對(duì)象創(chuàng)建鏈?zhǔn)秸{(diào)用

// Normal approach
fun createIntent(intentData: String, intentAction: String): Intent {
    val intent = Intent()
    intent.action = intentAction
    intent.data=Uri.parse(intentData)
    return intent
}
// Improved approach, chaining
fun createIntent(intentData: String, intentAction: String) =
        Intent().apply { action = intentAction }
                .apply { data = Uri.parse(intentData) }

說(shuō)了這么多我們?cè)撛趺催x用呢,通過(guò)一張圖來(lái)說(shuō)明


image

大概分析了一遍基礎(chǔ)庫(kù)中的最常用的擴(kuò)展函數(shù)趟脂,熟練使用這些函數(shù)泰讽,能夠讓我們的代碼看上去更加簡(jiǎn)練和易懂,最主要的還是要多用昔期。

尾隨閉包

大家可能對(duì)這個(gè)概念有點(diǎn)陌生已卸,甚至沒(méi)有聽(tīng)過(guò)這個(gè)詞(我第一次聽(tīng),也是從火火老師那邊聽(tīng)的)硼一,其實(shí)很簡(jiǎn)單累澡,上面我舉了很多例子都是用到了尾隨閉包,例如

fun test(){
    User("111").let{//這里就是一個(gè)尾隨閉包
        
    }
}

尾隨閉包是kotlin的一個(gè)語(yǔ)法般贼,當(dāng)函數(shù)的最后一個(gè)參數(shù)為函數(shù)的時(shí)候(或者接口愧哟,接口的話(huà)只能有一個(gè)函數(shù)),可以直接省略括號(hào)具伍,直接函數(shù)名后跟著大括號(hào)即可翅雏,舉例說(shuō)明

fun test(){
    view.setOnClickListener{//因?yàn)閛nClick(v:View)圈驼,只有一個(gè)參數(shù)人芽,這里就直接省略了,可以直接用it來(lái)代替
        
    }
    //如果函數(shù)有多個(gè)參數(shù)
    view.setOnTouchListener{
        v,event -> //可以這樣來(lái)寫(xiě)
    }
    
    //如果不用尾隨閉包绩脆,那么寫(xiě)法基本和Java一樣
    view..setOnClickListener(object :View.OnClickListener{
            override fun onClick(v: View?) {
                
            }
        })
}

是不是這樣寫(xiě)萤厅,優(yōu)雅了很多,也更加易讀了靴迫。

inline(內(nèi)聯(lián))惕味、infix(中綴)、高階函數(shù)

inline

Kotlin天生支持函數(shù)式編程玉锌,高階函數(shù)和lambda是其一大特色.
使用高階函數(shù)會(huì)帶來(lái)一些運(yùn)行時(shí)間效率的損失:每個(gè)函數(shù)都是一個(gè)對(duì)象名挥,別切都會(huì)捕獲一個(gè)閉包。即在那些函數(shù)體內(nèi)會(huì)被訪(fǎng)問(wèn)的變量主守。內(nèi)部分配(對(duì)于函數(shù)對(duì)象和類(lèi))和虛擬調(diào)用會(huì)引入運(yùn)行時(shí)間開(kāi)銷(xiāo)禀倔。

使用inline修飾的函數(shù),可以從編譯器角度將函數(shù)的函數(shù)體復(fù)制到調(diào)用出實(shí)現(xiàn)內(nèi)聯(lián)

infix

中綴表達(dá)式是一種通用的算術(shù)或者邏輯公式表示方法参淫,操作符以中綴形式處于操作數(shù)中間救湖。中綴表達(dá)式允許我們使用一個(gè)單詞或者字母來(lái)當(dāng)做運(yùn)算符用(實(shí)質(zhì)上還是函數(shù)調(diào)用),忽略調(diào)用的點(diǎn)和圓括號(hào)

Kotlin的中綴表達(dá)式涎才,需要滿(mǎn)足以下條件

  1. 使用infix修飾
  2. 只有一個(gè)參數(shù)
  3. 參數(shù)不得接受可變數(shù)量的參數(shù)且不能有默認(rèn)值
infix fun Int.add(i:Int):Int = this + i

infix fun Int.加(i:Int):Int = this + i

fun main(args: Array<String>) {

    println(5 add 10)
    println(5 加 10)
}

由此可見(jiàn)鞋既,中綴表達(dá)式能讓代碼看起來(lái)更加接近自然語(yǔ)言。

DSL

DSL(domain specific language),即領(lǐng)域?qū)S谜Z(yǔ)言:專(zhuān)門(mén)解決某一特定問(wèn)題的計(jì)算機(jī)語(yǔ)言邑闺,比如大家耳熟能詳?shù)腟QL和正則表達(dá)式

那么Kotlin DSL 是什么呢跌前?使用Kotlin語(yǔ)言呢開(kāi)發(fā)的,解決特定領(lǐng)域問(wèn)題陡舅,具備獨(dú)特代碼結(jié)構(gòu)的API

Kitlin DSL的具體表現(xiàn)都有哪些呢舒萎?上面提到過(guò)的:擴(kuò)展函數(shù)、中綴函數(shù)蹭沛、lambda以及要講的Anko Layout

DSL-擴(kuò)展函數(shù)

想想一下這樣的場(chǎng)景:我們需要給每一個(gè)點(diǎn)擊事件添加埋點(diǎn)臂寝、需要判斷是否連擊(一般的按鈕是不允許連擊的),點(diǎn)擊的時(shí)候需要進(jìn)行動(dòng)畫(huà)摊灭。這種事件很像面向切面編程(AOP)咆贬,在沒(méi)有接觸Kotlin的時(shí)候,大部分解決方案是用AspectJ或者Lancet來(lái)在編譯時(shí)織入代碼帚呼,進(jìn)行hook函數(shù)掏缎。在Kotlin中我們使用擴(kuò)展函數(shù)就可以實(shí)現(xiàn)這樣的功能,我做了一個(gè)例子:

object TriggerTime {
    var triggerLastTime: Long = 0
    const val DELAY_TIME = 300
}

inline fun <T : View> T.clickWithAnimation(onClickListener: View.OnClickListener) = setOnClickListener {
    if (clickEnable) {
        it.animate().scaleX(0.8f).scaleY(0.8f).setDuration(80).withEndAction {
            it.animate().scaleX(1f).scaleY(1f).setDuration(80).start()
        }.start()
        onClickListener.onClick(it)
    }
}

inline fun <T : View> T.clickWithEvent(hashMap: HashMap<String, String>, noinline block: (T) -> Unit) {
    // hashmap 進(jìn)行打點(diǎn)
    setOnClickListener {
        if (clickEnable)
            block.invoke(it as T)
    }
}

var <T : View> T.clickEnable: Boolean
    get() {
        var flag = false
        val currentClickTime = System.currentTimeMillis()
        if (currentClickTime - TriggerTime.triggerLastTime >= DELAY_TIME) {
            flag = true
        }
        TriggerTime.triggerLastTime = currentClickTime
        return flag
    }
    set(value) {

    }
    
//使用方法

fun test(view:View){
    view.clickWithEvent(hashMapOf("a" to "b")){
        //點(diǎn)擊事件
    }
    
    view.clickWithAnimation{
        //點(diǎn)擊事件
    }
}

上面的代碼很簡(jiǎn)單煤杀,這里就不多做說(shuō)明了眷蜈,可以很方便的實(shí)現(xiàn)這一類(lèi)的點(diǎn)擊事件。

告別findViewById

使用Kotlin Android Extensions就可以訪(fǎng)問(wèn)布局XML的View沈自,就像它們?cè)诓季种卸x的屬性一樣酌儒,可以使用id的名稱(chēng)。Kotlin Android Extensions是Kotlin的一個(gè)插件枯途。

將Kotlin Android Extensions集成到module中
只需要在module中的build.gradle上部添加apply plugin: 'kotlin-android-extensions'即可忌怎,十分方便,接下來(lái)舉個(gè)例子酪夷,就知道怎樣使用了

// 先定義一個(gè)普通的布局
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/test"/>

</androidx.constraintlayout.widget.ConstraintLayout>
// 下面是activity
// 這句很重要榴啸,只有添加這句,才能直接使用id
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : FragmentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 直接使用xml 中的id 就可以直接使用view晚岭,很神奇吧
        test.visibility = View.VISIBLE
    }
}

是不是很優(yōu)雅鸥印,再也不用寫(xiě)一大堆變量,然后findViewById坦报,就可以直接使用库说,不僅要知其然,還要知其所以然燎竖。讓我們看下反編譯后的java代碼

public final class MainActivity extends FragmentActivity {
   private HashMap _$_findViewCache;

   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131296284);
      this.getWindow().setSoftInputMode(48);
      View var10000 = this._$_findCachedViewById(id.test);
      Intrinsics.checkExpressionValueIsNotNull(var10000, "test");
      var10000.setVisibility(0);
   }

   public View _$_findCachedViewById(int var1) {
      if (this._$_findViewCache == null) {
         this._$_findViewCache = new HashMap();
      }

      View var2 = (View)this._$_findViewCache.get(var1);
      if (var2 == null) {
         var2 = this.findViewById(var1);
         this._$_findViewCache.put(var1, var2);
      }

      return var2;
   }

   public void _$_clearFindViewByIdCache() {
      if (this._$_findViewCache != null) {
         this._$_findViewCache.clear();
      }

   }
}

上面代碼 不用我講解璃弄,大家也一定明白其中原理了。

DSL-Anko布局

Anko是什么鬼构回?看下官方的解釋

Anko is a Kotlin library which makes Android application development faster and easier. It makes your code clean and easy to read, and lets you forget about rough edges of the Android SDK for Java.

大概意思就是:是一款Kotlin的庫(kù)夏块,可以使Android開(kāi)發(fā)過(guò)程更快和更簡(jiǎn)單疏咐,可以使代碼更加簡(jiǎn)潔和易讀,并且能讓你不去關(guān)注復(fù)雜的Android SDK脐供。

Anko大體包含四個(gè)方面:

  • Commons:intents浑塞、dialogs、logging....
  • Layouts:動(dòng)態(tài)的Android布局
  • SQLite
  • Coroutines(協(xié)程):輕量級(jí)的線(xiàn)程

基本的操作流程可以去官網(wǎng)去看下政己。

怎樣預(yù)覽Anko Layout

首先需要下載插件


image

然后打開(kāi)預(yù)覽

image

可以將XML轉(zhuǎn)換為Anko Layout(轉(zhuǎn)過(guò)去后一大堆紅酌壕,最好還是不要用)

image

怎么使用Anko layout舉個(gè)例子

// 方法一
class TestUI : AnkoComponent<Activity> {
    override fun createView(ui: AnkoContext<Activity>): View = ui.apply {
        frameLayout {
            lparams(matchParent, matchParent)
            textView {
                text = "Hello Anko!!"
            }.lparams(wrapContent, wrapContent) {
                gravity = Gravity.CENTER
            }
        }
    }.view
}

// 方法二
object TestUI1 {
    fun createView(context: Context): View = context.UI {
        frameLayout {
            lparams(matchParent, matchParent)
            textView {
                text = "Hello Anko!!"
            }.lparams(wrapContent, wrapContent) {
                gravity = Gravity.CENTER
            }
        }
    }.view
}


// 使用

fun test(){
    //直接在activity的onCreate中使用,代替setContentView(view)
     TestUI().setContentView(activity)
     //得到一個(gè)view對(duì)象
    val view2 = TestUI1.create(context)
}

使用第一種方式的好處是可以預(yù)覽Anko Layout歇由,預(yù)覽AnkoLayout卵牍,但是也不是實(shí)時(shí)的,需要你build一下沦泌,才能預(yù)覽糊昙,個(gè)人感覺(jué)現(xiàn)在用處不大,看后面會(huì)不會(huì)優(yōu)化一下谢谦,可以做到實(shí)時(shí)更新释牺。

AnkoLayout 優(yōu)缺點(diǎn)

當(dāng)一個(gè)新的東西引進(jìn)時(shí),我們要思考回挽,引進(jìn)進(jìn)的技術(shù)能給我們帶來(lái)什么没咙?可以讓我們的開(kāi)發(fā)變得更簡(jiǎn)單??jī)?yōu)化app的性能千劈?不能只為用新技術(shù)而用新技術(shù)祭刚。

ankoLayout的優(yōu)點(diǎn):
可以不通過(guò)xml來(lái)反射拿到view,直接在代碼中new出來(lái)队塘,相對(duì)于效率更高袁梗,我做過(guò)實(shí)驗(yàn)宜鸯,一個(gè)簡(jiǎn)單的布局:xml方式需要15-20ms憔古,AnkoLayout需要7-12ms,大概能減少30%的執(zhí)行時(shí)間淋袖,如果是復(fù)雜的鸿市,效果很更明顯,對(duì)于我們的布局優(yōu)化是一個(gè)很不錯(cuò)的方式即碗。

ankoLayout缺點(diǎn):
需要學(xué)習(xí)焰情,現(xiàn)在anko庫(kù)支持的不是很好,不能實(shí)時(shí)可視化布局剥懒,比較沒(méi)法

其他

雙冒號(hào)(::)

剛開(kāi)始用到這個(gè)符號(hào)是取class内舟,在kotlin中是這樣取一個(gè)類(lèi)的class,而不是我們常用的XXX.class

System.out.print(Test::class.java)

剛開(kāi)始的時(shí)候是懵逼的初橘,根本不知道::是什么意思验游,只是簡(jiǎn)單理解充岛,要獲取一個(gè)類(lèi)的class就使用這樣的語(yǔ)法。隨著對(duì)Kotlin的理解加深耕蝉,也對(duì)::有了初步的理解崔梗。

Kotlin是支持函數(shù)作為參數(shù)(高階函數(shù)),而::就是把一個(gè)方法當(dāng)做一個(gè)參數(shù)垒在,而且可以省略參數(shù)蒜魄,舉個(gè)例子

fun main() {
    foo2("xxx", this::bar2)     //right
    foo2("xxx", this::bar1)     //wrong, compile fail
}

fun bar1() {
    print("bar1")
}

fun foo2(content: String, body: (String) -> Unit) {
    body(content)
}

fun bar2(string: String) {
    print(string)
}

Kotlin委托

在委托模式中,當(dāng)有兩個(gè)對(duì)象參與參與同一個(gè)請(qǐng)求時(shí)场躯,接受請(qǐng)求的對(duì)象將請(qǐng)求委托給另一個(gè)對(duì)象來(lái)處理谈为。委托模式已經(jīng)證明是實(shí)現(xiàn)繼承的一個(gè)很好的代替方式。Kotlin中委托分為類(lèi)委托屬性委托踢关,Kotlin官方庫(kù)也封裝一些常用的委托峦阁。

類(lèi)委托

其實(shí)我們平常經(jīng)常使用委托模式,例如view.setOnClickListener(this/*或者listener*/)耘成,這行代碼是不是看上去異常的眼熟榔昔,對(duì)這就是給view設(shè)置點(diǎn)擊監(jiān)聽(tīng)事件,而this和listener就是一種委托模式瘪菌,來(lái)咱們看下kotlin的委托模式

interface A {
    fun print()
}

val a = object : A {
    override fun print() {
        println("this a")
    }
}

fun main(args: Array<String>) {
    var aa = object :A by a{}

    aa.print()// print: this a
}

做個(gè)小結(jié):

  1. 委托的關(guān)鍵詞是by
  2. 委托可以通過(guò)重寫(xiě)方法控制委托的范圍

屬性委托

個(gè)人在思考中撒会,感覺(jué)屬性委比類(lèi)委托使用場(chǎng)景要多,類(lèi)委托還沒(méi)想到比較好的使用場(chǎng)景师妙。

系統(tǒng)為我們提供了一些常用的屬性委托诵肛,包括

  1. lazy 延遲屬性,只在訪(fǎng)問(wèn)的時(shí)候初始化屬性值
  2. observable默穴,屬性的值變更時(shí)怔檩,會(huì)有回調(diào)
  3. notNull,如果使用該屬性的時(shí)候?yàn)閚ull,則會(huì)拋出IllegalStateException異常
  4. 把多個(gè)屬性存儲(chǔ)在一個(gè)map中蓄诽,而不是每個(gè)竄在單獨(dú)的字段中

延遲屬性 lazy

延遲屬性只提供了只讀屬性薛训,并且提供了三種模式來(lái)處理線(xiàn)程安全:

  • LazyThreadSafetyMode.SYNCHRONIZED 線(xiàn)程同步
  • LazyThreadSafetyMode.PUBLICATION 多線(xiàn)程使用,無(wú)需同步
  • LazyThreadSafetyMode.NONE 單線(xiàn)程場(chǎng)景

看個(gè)例子

fun test(){
    val a:String by lazy{
        println("get value")
        "a"
    }
}

看下源碼

public fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
        when (mode) {
            LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
            LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
            LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
        }

//最簡(jiǎn)單的是UnsafeLazyImpl仑氛,單線(xiàn)程模式
internal class UnsafeLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer  //拿到初始化的lambda方法
    private var _value: Any? = UNINITIALIZED_VALUE //定義了一個(gè)非法值

    override val value: T
        get() {         //這行應(yīng)該和上面一行連起來(lái)看介袜,就是重寫(xiě)該屬性的get()方法
            if (_value === UNINITIALIZED_VALUE) {
                _value = initializer!!() //初始化
                initializer = null
            }
            @Suppress("UNCHECKED_CAST")
            return _value as T //返回初始化后的值
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE // 這個(gè)是Lazy的方法鸠珠,用于記錄是否初始化結(jié)束飞盆。

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."  //重寫(xiě)toString方法

    private fun writeReplace(): Any = InitializedLazyImpl(value) //未找到方法作用
}

可觀(guān)察屬性 Observable

Delegates一共提供了三個(gè)委托方法notNull城看、observable轰胁、vetoable

直接看例子

/**
在值發(fā)生變化的時(shí)候榛斯,會(huì)回調(diào)下面的方法
**/
var name: String by Delegates.observable("init") { prop, old, new ->
    println("old : $old")
    println("new : $new")
}


/**
vetoable委托對(duì)象可以對(duì)數(shù)值的變化進(jìn)行控制提佣。與observable唯一的不同是每篷,參數(shù)B的返回值變?yōu)榱薆oolean,如果返回值為true則接受變化仓技,如果為false則不接受變化。
**/
var age: Int by Delegates.vetoable(18) { prop, old, new ->
    println("old : $old")
    println("new : $new")
    val accept=new>16
    println("accept : $accept")
    accept //只接受>16的變更
}

自定義屬性委托

有兩種方式實(shí)現(xiàn)委托屬性

  • 實(shí)現(xiàn)接口ReadOnlyProperty或者ReadWriteProperty地沮,從字面就可以看出來(lái)嗜浮,一個(gè)是只讀的委托屬性,一個(gè)是讀寫(xiě)的委托屬性摩疑,lazy就是只讀的委托屬性
  • 字符重定義 operator fun getValueoperator fun setValue

看個(gè)例子

// 第一種方式
var a: String by object : ReadWriteProperty<Any?, String> {
        var _value: String = "" //屬性委托一般都需要一個(gè)間接對(duì)象進(jìn)行數(shù)據(jù)讀取與賦值
        
        override fun getValue(thisRef: Any?, property: KProperty<*>): String {
         
            print("in getValue")
            print("$thisRef")
            print("$property")
            return _value
        }
        override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {

            println("in setValue")
            _value = value
        }
    }


// 第二種方式
class ExtrasDelegate<out T>(private val extraName: String, private val defaultValue: T) {

    private var extra: T? = null

    operator fun getValue(thisRef: AppCompatActivity, property: KProperty<*>): T {
        extra = getExtra(extra, extraName, thisRef)
        return extra ?: defaultValue
    }
}


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末周伦,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子未荒,更是在濱河造成了極大的恐慌专挪,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,542評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件片排,死亡現(xiàn)場(chǎng)離奇詭異寨腔,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)率寡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)迫卢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人冶共,你說(shuō)我怎么就攤上這事乾蛤。” “怎么了捅僵?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,021評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵家卖,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我庙楚,道長(zhǎng)上荡,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,682評(píng)論 1 284
  • 正文 為了忘掉前任馒闷,我火速辦了婚禮酪捡,結(jié)果婚禮上叁征,老公的妹妹穿的比我還像新娘。我一直安慰自己逛薇,他們只是感情好捺疼,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,792評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著永罚,像睡著了一般啤呼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上尤蛮,一...
    開(kāi)封第一講書(shū)人閱讀 49,985評(píng)論 1 291
  • 那天媳友,我揣著相機(jī)與錄音,去河邊找鬼产捞。 笑死醇锚,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的坯临。 我是一名探鬼主播焊唬,決...
    沈念sama閱讀 39,107評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼看靠!你這毒婦竟也來(lái)了赶促?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,845評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤挟炬,失蹤者是張志新(化名)和其女友劉穎鸥滨,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體谤祖,經(jīng)...
    沈念sama閱讀 44,299評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡婿滓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,612評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了粥喜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凸主。...
    茶點(diǎn)故事閱讀 38,747評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖额湘,靈堂內(nèi)的尸體忽然破棺而出卿吐,到底是詐尸還是另有隱情,我是刑警寧澤锋华,帶...
    沈念sama閱讀 34,441評(píng)論 4 333
  • 正文 年R本政府宣布嗡官,位于F島的核電站,受9級(jí)特大地震影響供置,放射性物質(zhì)發(fā)生泄漏谨湘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,072評(píng)論 3 317
  • 文/蒙蒙 一芥丧、第九天 我趴在偏房一處隱蔽的房頂上張望紧阔。 院中可真熱鬧,春花似錦续担、人聲如沸擅耽。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,828評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)乖仇。三九已至,卻和暖如春询兴,著一層夾襖步出監(jiān)牢的瞬間乃沙,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,069評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工诗舰, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留警儒,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,545評(píng)論 2 362
  • 正文 我出身青樓眶根,卻偏偏與公主長(zhǎng)得像蜀铲,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子属百,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,658評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容