優雅的使用Kotlin

優雅的使用Kotlin

這篇文章并不會從零開始來教你怎么使用Kotlin,Kotlin對于筆者來說也是一個新概念,由于大勢所趨(Google所有的官方Demo都開始用Kotlin,各大第三方Library也開始適配Kotlin),所以不會Koltin寸步難行呀~,經過一段時間的學習和練習,對Kotlin有了一點淺薄的理解,在此記錄下來,希望能給大家帶來幫助。

開始進入主題,我打算從下面幾個方面來給大家總結一些使用技巧和個人的理解(不足之處還請大家多多指出)

  • 類(類、接口、數據類)
  • 函數(擴展函數)
  • DSL、擴展庫和Anko
  • 協程

如何創建一個Kotlin類

我們先看看我們熟悉的Java如何創建一個普通的類:

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

下面是Kotlin聲明的類

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

看上去好像差別不是很大嘛,其實對于學過Java的人來說,Kotlin還是比較相近的,只需要了解一些新特性,上手還是很快的。

在上面的例子中,Kotlin的構造函數直接聲明在類的后面,并且變量的聲明也可以在函數的參數中聲明,最后字符串的輸出,直接使用${變量名}的形式輸出

將上面的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;
   }
}

基本上一樣,接下來我們就開始看看Kotlin怎樣常見一個類,和我們Java的類有什么具體的區別。

構造函數

相信大家對Java的構造函數已經很熟悉了:

  • 多個構造函數,構造函數之間是重載的
  • 可以不聲明構造函數,編譯器會自動生成一個無參的構造函數

Kotlin中,一個類中可以有一個主構造函數和多個次構造函數。主構造函數就是聲明在類頭的構造函數,次構造函數必須委托主構造函數,文字太枯燥,舉幾個例子就一目了然了。

//這是沒有省略所有關鍵字的類聲明,當然如果沒有可見性的修飾符(默認是public),可以省略看見性修飾符和constructor
class Player public constructor(val name :String){
    
    //如果想在構造函數中定一些變量或者創建對象,可以直接在類體中定義,也可以在init{...}代碼塊中去定義
    val id = "111" 
    
    init{
        println("$id 是在初始化的代碼塊中")
    }
   
    fun play(){
        println("$name start paly")
    }
    
    // 調用player("222").play()輸出下面字符串:
    // 111 是在初始化的代碼塊中
    // 222 start paly
}


// 主構造函數和次構造函數并存
//初始化塊中的代碼實際上會成為主構造函數的一部分。委托給主構造函數會作為次構造函數的第一條語句,因此所有初始化塊中的代碼都會在次構造函數體之前執行。即使該類沒有主構造函數,這種委托仍會隱式發生,并且仍會執行初始化塊
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構造函數小結:

  1. 可以有一個主構造函數和多個次構造函數
  2. 可以只有主構造函數和次構造函數
  3. 主、次構造函數同事存在的時候,次構造函數必須直接或者間接的委托到住構造函數
  4. 沒有聲明主構構造函數,會自動生成一個無參數的主構造函數,這點與java一樣。
  5. 在主構造函數中你可以設置默認的值。

數據類

個人覺得Kotlin的數據類使用起來真的很方便,省去我們很多工作量(媽媽從此再也不擔心我寫數據類了~~)。

先用Java寫一個數據類,方便我們后面做對比

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;
    }
}

可以看到聲明一個數據類將近需要60多行的代碼,還是在沒有重寫toString(),equals(),hashCode()等方法的前提下。

我們再來看看Kotlin如果實現一個對象類

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

Kotlin只需要一行代碼就搞定,是不是很優雅,優雅的背后是編譯器為我們做了許多的事情,編譯器會自動的從主構造函數中根據所有聲明的屬性生成以下的函數

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

實用小竅門
當我們使用定義好的類時,如果有些屬性是不必要的,可以設置默認值,那么我們就可以按照以下的方式調用(拿上面的User做例子):

//Test.kt

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

在日常的工作中基本上熟悉上面兩個知識點,就能很好的工作了,關于類還有

  • 枚舉類 (和Java使用基本一樣)
  • 密閉類 (sealed class)枚舉類的擴展,每個枚舉常量只存在一個實例,而密閉類的一個子類可以有多個實例,暫時還沒有用到。
  • 嵌套類,與Java的靜態內部類一樣
  • 內部類,和Java一樣,內部類持有一個外部類的對象引用
  • 匿名內部類。

函數

Kotlin在函數使用上增加很多的語法糖,我們可以方便、愉快的編寫代碼,也使得代碼看起來非常簡潔易懂,總是就是更加優雅

擴展函數

Kotlin允許在不改變已有類的情況下,為某個類添加新的函數,這個特性叫做擴展函數。有了這個特性,那么我們豈不是可以給任意的類添加我們想要的函數,再也不用添加XXXUtils或者XXXTool的類了,Kotlin使用了什么黑科技嗎?讓我們看看

假如我們要給Context添加一個toast的功能,可以這樣寫

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

// 當我們需要toast的時候,我們可以這樣調用

context.toast("test")

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

//聲明 toast方法的類
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();
   }
}
// 調用toast的地方
TestKt.toast(context, "test");

從上面代碼我們不難明白,其實Kotlin的擴展函數也是給我們生成一個工具類,在掉用的地方幫我們換成這個類的調用,原來如此的簡單。

說到擴展函數,就不得不提的幾個基礎庫的擴展函數,也叫做作用域函數,它們是:T.run、T.let、T.also、T.apply 以及run、width兩個函數。對于這幾個函數的使用剛開始的時候我還是很懵逼的,只是大概了解用法,以及帶來的鏈式調用的便捷,但是它們怎么什么情況下使用,我還真是搞不明白,通常情況下都是想起拿個用哪個。但是這篇文章很好的解釋了各個函數的用法和情況。

run

這是最簡單的作用域,看下源代碼,Kotlin是怎樣定義這個方法的:

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

//舉個例子,可以這樣使用

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

在這個例子中看起來好像并沒有什么卵用,但是其實還是有一些好處的(雖然我在實際開發中沒有用到過)

  • 分隔開的作用域。
  • 可以根據作用域返回的結果進行接下來的操作

普通函數和擴展函數(with VS T.run)

還是先看下這兩個函數的源碼

/**
 * 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()
}

從源碼上看基本上是沒有區別的,都是接受一個函數作為參數,然后返回這個函數的結果。它們之間唯一不同的在于with是一個普通函數,而T.run是一個擴展函數,那么問題來了,它們各自使用的優點是什么呢?舉個例子

// 對于user的處理

fun test(){
    with(user){
        id = "222"
        name = "test"
    }
    
    user.run{
        id = "222"
        name = "test"
    }
    
    // 好像并沒有什么區別,想想一下,如果user可能為null呢,該如何去實現
    
    with(user){
        this?.id = "222"
        this?.name = "test"
    }
    
    user?.run{
        id = "222"
        name = "test"
    }
}

第二種場景下,顯然T.run擴展函數更好,因為我們可以在使用它之前對可空性進行檢查。

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

如果我們對比T.run和T.let兩個函數也是非常相似的,上源碼

/**
 * 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()
}

從源碼中可以看出來,它們唯一的區別是接受的參數不一樣。

  • T.run僅僅是被當做了block:T.()擴展函數的調用塊。因此在其作用域內,T可以被this指代。在編碼過程中,大多數情況下this是可以被省略的。
  • T.let將自己本身傳遞到函數block:(T)。因此這個類似于傳遞一個lambda表達式作為參數。它可以在函數作用域內部是用哪個it來指代,所以把這個稱為傳遞it參數。

舉個例子

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

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

從上面的例子來看,似乎T.run比T.let更加優越,但是T.let有一些微妙的優勢(其實我覺得沒什么卵用,都差不多,就看你喜歡用哪個)

  1. T.let函數提供了一種更清晰的區分方式去使用給定的變量函數/成員與外部類函數/成員。
  2. 例如當this作為函數的參數傳遞時,this不能被省略,并且it寫起來比this更簡潔,更清晰。
  3. T.let允許更好地命名已轉換的已使用變量,即可以將it轉換為其他有含義名稱,而 T.run則不能,內部只能用this指代或者省略。

return this和return 函數結果(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)
}

從上面源碼可以看出來,它們唯一的不同就是T.let返回一個不同類型的值也就是函數執行的結果,而T.alse返回T類型本身

舉個例子

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似乎看上去沒有意義,因為我們可以很輕易的將它們組合成為一個模塊。仔細想想,T.also還是有一些好處的

  1. 它可以對一個對象提供非常清晰的區分過程,將其區分成更小的功能區域
  2. 在使用之前,可以進行強大的自我操作,創建一個鏈式調用的操作。

當把這個鏈式調用連接起來,就非常牛逼了。

// 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() }

一行代碼搞定,是不是很優雅

回顧所有的屬性值

通過回顧這三個屬性特征,我們可以非常清楚函數的行為。說明一下T.apply函數:

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

舉個例子

// 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 }

或者我們也可以讓無鏈對象創建鏈式調用

// 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) }

說了這么多我們該怎么選用呢,通過一張圖來說明


image

大概分析了一遍基礎庫中的最常用的擴展函數,熟練使用這些函數,能夠讓我們的代碼看上去更加簡練和易懂,最主要的還是要多用。

尾隨閉包

大家可能對這個概念有點陌生,甚至沒有聽過這個詞(我第一次聽,也是從火火老師那邊聽的),其實很簡單,上面我舉了很多例子都是用到了尾隨閉包,例如

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

尾隨閉包是kotlin的一個語法,當函數的最后一個參數為函數的時候(或者接口,接口的話只能有一個函數),可以直接省略括號,直接函數名后跟著大括號即可,舉例說明

fun test(){
    view.setOnClickListener{//因為onClick(v:View),只有一個參數,這里就直接省略了,可以直接用it來代替
        
    }
    //如果函數有多個參數
    view.setOnTouchListener{
        v,event -> //可以這樣來寫
    }
    
    //如果不用尾隨閉包,那么寫法基本和Java一樣
    view..setOnClickListener(object :View.OnClickListener{
            override fun onClick(v: View?) {
                
            }
        })
}

是不是這樣寫,優雅了很多,也更加易讀了。

inline(內聯)、infix(中綴)、高階函數

inline

Kotlin天生支持函數式編程,高階函數和lambda是其一大特色.
使用高階函數會帶來一些運行時間效率的損失:每個函數都是一個對象,別切都會捕獲一個閉包。即在那些函數體內會被訪問的變量。內部分配(對于函數對象和類)和虛擬調用會引入運行時間開銷。

使用inline修飾的函數,可以從編譯器角度將函數的函數體復制到調用出實現內聯

infix

中綴表達式是一種通用的算術或者邏輯公式表示方法,操作符以中綴形式處于操作數中間。中綴表達式允許我們使用一個單詞或者字母來當做運算符用(實質上還是函數調用),忽略調用的點和圓括號

Kotlin的中綴表達式,需要滿足以下條件

  1. 使用infix修飾
  2. 只有一個參數
  3. 參數不得接受可變數量的參數且不能有默認值
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)
}

由此可見,中綴表達式能讓代碼看起來更加接近自然語言。

DSL

DSL(domain specific language),即領域專用語言:專門解決某一特定問題的計算機語言,比如大家耳熟能詳的SQL和正則表達式

那么Kotlin DSL 是什么呢?使用Kotlin語言呢開發的,解決特定領域問題,具備獨特代碼結構的API

Kitlin DSL的具體表現都有哪些呢?上面提到過的:擴展函數、中綴函數、lambda以及要講的Anko Layout

DSL-擴展函數

想想一下這樣的場景:我們需要給每一個點擊事件添加埋點、需要判斷是否連擊(一般的按鈕是不允許連擊的),點擊的時候需要進行動畫。這種事件很像面向切面編程(AOP),在沒有接觸Kotlin的時候,大部分解決方案是用AspectJ或者Lancet來在編譯時織入代碼,進行hook函數。在Kotlin中我們使用擴展函數就可以實現這樣的功能,我做了一個例子:

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 進行打點
    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")){
        //點擊事件
    }
    
    view.clickWithAnimation{
        //點擊事件
    }
}

上面的代碼很簡單,這里就不多做說明了,可以很方便的實現這一類的點擊事件。

告別findViewById

使用Kotlin Android Extensions就可以訪問布局XML的View,就像它們在布局中定義的屬性一樣,可以使用id的名稱。Kotlin Android Extensions是Kotlin的一個插件。

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

// 先定義一個普通的布局
<?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
    }
}

是不是很優雅,再也不用寫一大堆變量,然后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的庫,可以使Android開發過程更快和更簡單,可以使代碼更加簡潔和易讀,并且能讓你不去關注復雜的Android SDK。

Anko大體包含四個方面:

  • Commons:intents、dialogs、logging....
  • Layouts:動態的Android布局
  • SQLite
  • Coroutines(協程):輕量級的線程

基本的操作流程可以去官網去看下。

怎樣預覽Anko Layout

首先需要下載插件


image

然后打開預覽

image

可以將XML轉換為Anko Layout(轉過去后一大堆紅,最好還是不要用)

image

怎么使用Anko layout舉個例子

// 方法一
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)
     //得到一個view對象
    val view2 = TestUI1.create(context)
}

使用第一種方式的好處是可以預覽Anko Layout,預覽AnkoLayout,但是也不是實時的,需要你build一下,才能預覽,個人感覺現在用處不大,看后面會不會優化一下,可以做到實時更新。

AnkoLayout 優缺點

當一個新的東西引進時,我們要思考,引進進的技術能給我們帶來什么?可以讓我們的開發變得更簡單?優化app的性能?不能只為用新技術而用新技術。

ankoLayout的優點:
可以不通過xml來反射拿到view,直接在代碼中new出來,相對于效率更高,我做過實驗,一個簡單的布局:xml方式需要15-20ms,AnkoLayout需要7-12ms,大概能減少30%的執行時間,如果是復雜的,效果很更明顯,對于我們的布局優化是一個很不錯的方式。

ankoLayout缺點:
需要學習,現在anko庫支持的不是很好,不能實時可視化布局,比較沒法

其他

雙冒號(::)

剛開始用到這個符號是取class,在kotlin中是這樣取一個類的class,而不是我們常用的XXX.class

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

剛開始的時候是懵逼的,根本不知道::是什么意思,只是簡單理解,要獲取一個類的class就使用這樣的語法。隨著對Kotlin的理解加深,也對::有了初步的理解。

Kotlin是支持函數作為參數(高階函數),而::就是把一個方法當做一個參數,而且可以省略參數,舉個例子

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委托

在委托模式中,當有兩個對象參與參與同一個請求時,接受請求的對象將請求委托給另一個對象來處理。委托模式已經證明是實現繼承的一個很好的代替方式。Kotlin中委托分為類委托屬性委托,Kotlin官方庫也封裝一些常用的委托。

類委托

其實我們平常經常使用委托模式,例如view.setOnClickListener(this/*或者listener*/),這行代碼是不是看上去異常的眼熟,對這就是給view設置點擊監聽事件,而this和listener就是一種委托模式,來咱們看下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
}

做個小結:

  1. 委托的關鍵詞是by
  2. 委托可以通過重寫方法控制委托的范圍

屬性委托

個人在思考中,感覺屬性委比類委托使用場景要多,類委托還沒想到比較好的使用場景。

系統為我們提供了一些常用的屬性委托,包括

  1. lazy 延遲屬性,只在訪問的時候初始化屬性值
  2. observable,屬性的值變更時,會有回調
  3. notNull,如果使用該屬性的時候為null,則會拋出IllegalStateException異常
  4. 把多個屬性存儲在一個map中,而不是每個竄在單獨的字段中

延遲屬性 lazy

延遲屬性只提供了只讀屬性,并且提供了三種模式來處理線程安全:

  • LazyThreadSafetyMode.SYNCHRONIZED 線程同步
  • LazyThreadSafetyMode.PUBLICATION 多線程使用,無需同步
  • LazyThreadSafetyMode.NONE 單線程場景

看個例子

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)
        }

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

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

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE // 這個是Lazy的方法,用于記錄是否初始化結束。

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

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

可觀察屬性 Observable

Delegates一共提供了三個委托方法notNullobservablevetoable

直接看例子

/**
在值發生變化的時候,會回調下面的方法
**/
var name: String by Delegates.observable("init") { prop, old, new ->
    println("old : $old")
    println("new : $new")
}


/**
vetoable委托對象可以對數值的變化進行控制。與observable唯一的不同是,參數B的返回值變為了Boolean,如果返回值為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的變更
}

自定義屬性委托

有兩種方式實現委托屬性

  • 實現接口ReadOnlyProperty或者ReadWriteProperty,從字面就可以看出來,一個是只讀的委托屬性,一個是讀寫的委托屬性,lazy就是只讀的委托屬性
  • 字符重定義 operator fun getValueoperator fun setValue

看個例子

// 第一種方式
var a: String by object : ReadWriteProperty<Any?, String> {
        var _value: String = "" //屬性委托一般都需要一個間接對象進行數據讀取與賦值
        
        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
    }
}


?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容

  • 本文是在學習和使用kotlin時的一些總結與體會,一些代碼示例來自于網絡或Kotlin官方文檔,持續更新... 對...
    竹塵居士閱讀 3,320評論 0 8
  • 嵌套類和內部類 大部分時候,類被定義成一個獨立的程序單元。在某些情況下,也會把一個類放在另一個類的內部定義,這個定...
    凌寒天下獨自舞閱讀 498評論 0 0
  • Kotlin的優勢 代碼簡潔高效、強大的when語法,不用寫分號結尾,findViewById光榮退休,空指針安全...
    Windy_816閱讀 1,311評論 1 6
  • 面向對象編程(OOP) 在前面的章節中,我們學習了Kotlin的語言基礎知識、類型系統、集合類以及泛型相關的知識。...
    Tenderness4閱讀 4,470評論 1 6
  • 第一,有沒有自主權。在工作中能不能做自己想做的事情。 第二,精專。錢不再是排在第一位,而是職業發展不好。所謂職業發...
    小鹿_33閱讀 156評論 0 0