1. 程式人生 > >為什麼更建議使用kotlin來開發

為什麼更建議使用kotlin來開發

引言

 之前讀過一篇總結性的文章,特別贊。今天就借花獻佛,重新整理了一下供大家品鑑。  自從開始使用kotlin之後,腿也不疼了,腰也不酸了,一口氣爬五樓都不是事兒了。所以吐血推薦給大家。

推薦1

Kotlin 中的字串基本 Java 中的類似,有一點區別是加入了三個引號"""來方便長篇字元的編寫。 而在 Java 中,這些都需要轉義,先看看 java 中的示例:

public void testString1() {
        String str1 = "abc";
        String str2 = "line1\n" +
                "line2\n" +
                "line3";
        String js = "function myFunction()\n" +
                "{\n" +
                "document.getElementById(\"demo\").innerHTML=\"My First JavaScript Function\";\n" +
                "}";
        System.out.println(str1);
        System.out.println(str2);
        System.out.println(js);
    }

kotlin 除了有單個雙引號的字串,還對字串的加強,引入了三個引號,"""中可以包含換行、反斜槓等等特殊字元:

/*
* kotlin 對字串的加強,三個引號"""中可以包含換行、反斜槓等等特殊字元
* */
fun testString() {
    val str1 = "abc"
    val str2 = """line1\n
        line2
        line3
        """
    val js = """
        function myFunction()
        {
            document.getElementById("demo").innerHTML="My First JavaScript Function";
        }
        """.trimIndent()
    println(str1)
    println(str2)
    println(js)
}

同時,Kotlin 中引入了字串模版,方便字串的拼接,可以用$符號拼接變數和表示式

/*
* kotlin 字串模版,可以用$符號拼接變數和表示式
* */
fun testString2() {
    val strings = arrayListOf("abc", "efd", "gfg")
    println("First content is $strings")
    println("First content is ${strings[0]}")
    println("First content is ${if (strings.size > 0) strings[0] else "null"}")
}

值得注意的是,在 Kotlin 中,美元符號$是特殊字元,在字串中不能直接顯示,必須經過轉義,方法 1 是用反斜槓,方法二是KaTeX parse error: Expected '}', got 'EOF' at end of input: {'’}

/*
* Kotlin 中,美元符號$是特殊字元,在字串中不能直接顯示,必須經過轉義,方法 1 是用反斜槓,方法二是${'$'}
* */
fun testString3() {
    println("First content is \$strings")
    println("First content is ${'$'}strings")
}

推薦2

Kotlin 中大多數控制結構都是表示式 首先,需要弄清楚一個概念語句和表示式,然後會介紹控制結構表示式的優點:

簡潔

語句和表示式是什麼? 表示式有值,並且能作為另一個表示式的一部分使用 語句總是包圍著它的程式碼塊中的頂層元素,並且沒有自己的值

Kotlin 與 Java 的區別

Java 中,所有的控制結構都是語句,也就是控制結構都沒有值 Kotlin 中,除了迴圈(for、do 和 do/while)以外,大多數控制結構都是表示式(if/when 等) Example1:if 語句 java 中,if 是語句,沒有值,必須顯式的 return

/*
* java 中的 if 語句
* */
public int max(int a, int b) {
    if (a > b) {
        return a;
    } else {
        return b;
    }
}

kotlin 中,if 是表示式,不是語句,因為表示式有值,可以作為值 return 出去

/*
* kotlin 中,if 是表示式,不是語句,類似於 java 中的三目運算子 a > b ? a : b
* */
fun max(a: Int, b: Int): Int {
    return if (a > b) a else b
}

上面的 if 中的分支最後一行語句就是該分支的值,會作為函式的返回值。這其實跟 java 中的三元運算子類似,

/*
* java 的三元運算子
* */
public int max2(int a, int b) {
    return a > b ? a : b;
}

上面是 java 中的三元運算子,kotlin 中 if 是表示式有值,完全可以替代,故 kotlin 中已沒有三元運算子了,用 if 來替代。 上面的 max 函式還可以簡化成下面的形式

/*
* kotlin 簡化版本
* */
fun max2(a: Int, b: Int) = if (a > b) a else b

Example2:when 語句 Kotlin 中的 when 非常強大,完全可以取代 Java 中的 switch 和 if/else,同時,when 也是表示式,when 的每個分支的最後一行為當前分支的值 先看一下 java 中的 switch

/*
* java 中的 switch
* */
public String getPoint(char grade) {
    switch (grade) {
        case 'A':
            return "GOOD";
        case 'B':
        case 'C':
            return "OK";
        case 'D':
            return "BAD";
        default:
            return "UN_KNOW";
    }
}

java 中的 switch 有太多限制,我們再看看 Kotlin 怎樣去簡化的

/*
* kotlin 中,when 是表示式,可以取代 Java 中的 switch,when 的每個分支的最後一行為當前分支的值
* */
fun getPoint(grade: Char) = when (grade) {
    'A' -> "GOOD"
    'B', 'C' -> {
        println("test when")
        "OK"
    }
    'D' -> "BAD"
    else -> "UN_KNOW"
}

同樣的,when 語句還可以取代 java 中的 if/else if,其是表示式有值,並且更佳簡潔

/*
* java 中的 if else
* */
public String getPoint2(Integer point) {
    if (point > 100) {
        return "GOOD";
    } else if (point > 60) {
        return "OK";
    } else if (point.hashCode() == 0x100) {
        //...
        return "STH";
    } else {
        return "UN_KNOW";
    }
}

再看看 kotlin 的版本,使用不帶引數的 when,只需要 6 行程式碼

/*
* kotlin 中,when 是表示式,可以取代 java 的 if/else,when 的每個分支的最後一行為當前分支的值
* */
fun getPoint2(grade: Int) = when {
    grade > 90 -> "GOOD"
    grade > 60 -> "OK"
    grade.hashCode() == 0x100 -> "STH"
    else -> "UN_KNOW"
}

推薦3

更好呼叫的函式:顯式引數名/預設引數值 Kotlin 的函式更加好呼叫,主要是表現在兩個方面:

  • 1,顯式的標示引數名,可以方便程式碼閱讀;
  • 2,函式可以有預設引數值,可以大大減少 Java 中的函式過載。 例如現在需要實現一個工具函式,列印列表的內容:
/*
* 列印列表的內容
* */
fun <T> joinToString(collection: Collection<T>,
                     separator: String,
                     prefix: String,
                     postfix: String): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}
/*
* 測試
* */
fun printList() {
    val list = listOf(2, 4, 0)
    // 不標明引數名
    println(joinToString(list, " - ", "[", "]"))
    // 顯式的標明引數名稱
    println(joinToString(list, separator = " - ", prefix = "[", postfix = "]"))
}

如上面的程式碼所示,函式 joinToString 想要列印列表的內容,需要傳入四個引數:列表、分隔符、字首和字尾。 由於引數很多,在後續使用該函式的時候不是很直觀的知道每個引數是幹什麼用的,這時候可以顯式的標明引數名稱,增加程式碼可讀性。 同時,定義函式的時候還可以給函式預設的引數,如下所示:

/*
* 列印列表的內容,帶有預設的引數,可以避免 java 的函式過載
* */
fun <T> joinToString2(collection: Collection<T>,
                      separator: String = ", ",
                      prefix: String = "",
                      postfix: String = ""): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}
/*
* 測試
* */
fun printList3() {
    val list = listOf(2, 4, 0)
    println(joinToString2(list, " - "))
    println(joinToString2(list, " , ", "["))
}

這樣有了預設引數後,在使用函式時,如果不傳入該引數,預設會使用預設的值,這樣可以避免 Java 中大量的函式過載。

推薦4

擴充套件函式和屬性 擴充套件函式和擴充套件屬性是 Kotlin 非常方便實用的一個功能,它可以讓我們隨意的擴充套件第三方的庫,你如果覺得別人給的 SDK 的 Api 不好用,或者不能滿足你的需求,這時候你可以用擴充套件函式完全去自定義。 例如 String 類中,我們想獲取最後一個字元,String 中沒有這樣的直接函式,你可以用.後宣告這樣一個擴充套件函式:

/*
* 擴充套件函式
* */
fun String.lastChar(): Char = this.get(this.length - 1)
/*
* 測試
* */
fun testFunExtension() {
    val str = "test extension fun";
    println(str.lastChar())
}

這樣定義好 lastChar()函式後,之後只需要 import 進來後,就可以用 String 類直接呼叫該函數了,跟呼叫它自己的方法沒有區別。這樣可以避免重複程式碼和一些靜態工具類,而且程式碼更加簡潔明瞭。 例如我們可以改造上面 tip3 中的列印列表內容的函式:

/*
* 用擴充套件函式改造 Tip3 中的列表列印內容函式
* */
fun <T> Collection<T>.joinToString3(separator: String = ", ",
                                    prefix: String = "",
                                    postfix: String = ""): String {
    val result = StringBuilder(prefix)
    for ((index, element) in withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

fun printList4() {
    val list = listOf(2, 4, 0)
    println(list.joinToString3("/"))
}

除了擴充套件函式,還可以擴充套件屬性,例如我想實現 String 和 StringBuilder 通過屬性去直接獲得最後字元:

/*
* 擴充套件屬性 lastChar 獲取 String 的最後一個字元
* */
val String.lastChar: Char
    get() = get(length - 1)
/*
* 擴充套件屬性 lastChar 獲取 StringBuilder 的最後一個字元
* */
var StringBuilder.lastChar: Char
    get() = get(length - 1)
    set(value: Char) {
        setCharAt(length - 1, value)
    }
/*
* 測試
* */
fun testExtension() {
    val s = "abc"
    println(s.lastChar)
    val sb = StringBuilder("abc")
    println(sb.lastChar)
}

定義好擴充套件屬性後,之後只需 import 完了就跟使用自己的屬性一樣方便了。

Why?Kotlin 為什麼能實現擴充套件函式和屬性這樣的特性? 在 Kotlin 中要理解一些語法,只要認識到Kotlin 語言最後需要編譯為 class 位元組碼,Java 也是編譯為 class 執行,也就是可以大致理解為 Kotlin 需要轉成 Java 一樣的語法結構, Kotlin 就是一種強大的語法糖而已,Java 不具備的功能 Kotlin 也不能越界的。

那 Kotlin 的擴充套件函式怎麼實現的呢?介紹一種萬能的辦法去理解 Kotlin 的語法:將 Kotlin 程式碼轉化成 Java 語言去理解,步驟如下: 在 Android Studio 中選擇 Tools —> Kotlin —> Show Kotlin Bytecode 這樣就把 Kotlin 轉化為 class 位元組碼了 class 碼閱讀不太友好,點選左上角的 Decompile 就轉化為 Java 再介紹一個小竅門,在前期對 Kotlin 語法不熟悉的時候,可以先用 Java 寫好程式碼,再利用 AndroidStudio 工具將 Java 程式碼轉化為 Kotlin 程式碼,步驟如下: 在 Android Studio 中選中要轉換的 Java 程式碼 —> 選擇 Code —> Convert Java File to Kotlin File 我們看看將上面的擴充套件函式轉成 Java 後的程式碼

/*
* 擴充套件函式會轉化為一個靜態的函式,同時這個靜態函式的第一個引數就是該類的例項物件
* */
public static final char lastChar(@NotNull String $receiver) {
    Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
    return $receiver.charAt($receiver.length() - 1);
}
/*
* 獲取的擴充套件屬性會轉化為一個靜態的 get 函式,同時這個靜態函式的第一個引數就是該類的例項物件
* */
public static final char getLastChar(@NotNull StringBuilder $receiver) {
    Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
    return $receiver.charAt($receiver.length() - 1);
}
/*
* 設定的擴充套件屬性會轉化為一個靜態的 set 函式,同時這個靜態函式的第一個引數就是該類的例項物件
* */
public static final void setLastChar(@NotNull StringBuilder $receiver, char value) {
    Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
    $receiver.setCharAt($receiver.length() - 1, value);
}

檢視上面的程式碼可知:對於擴充套件函式,轉化為 Java 的時候其實就是一個靜態的函式,同時這個靜態函式的第一個引數就是該類的例項物件,這樣把類的例項傳入函式以後,函式內部就可以訪問到類的公有方法。 對於擴充套件屬性也類似,獲取的擴充套件屬性會轉化為一個靜態的 get 函式,同時這個靜態函式的第一個引數就是該類的例項物件,設定的擴充套件屬性會轉化為一個靜態的 set 函式,同時這個靜態函式的第一個引數就是該類的例項物件。 函式內部可以訪問公有的方法和屬性。頂層的擴充套件函式是 static 的,不能被 override

從上面轉換的原始碼其實可以看到擴充套件函式和擴充套件屬性適用的地方和缺陷:

擴充套件函式和擴充套件屬性內只能訪問到類的公有方法和屬性,私有的和 protected 是訪問不了的 擴充套件函式不是真的修改了原來的類,定義一個擴充套件函式不是將新成員函式插入到類中,擴充套件函式的型別是"靜態的",不是在執行時決定型別,案例程式碼StaticllyExtension.kt

open class C

class D: C()

fun C.foo() = "c"

fun D.foo() = "d"

fun printFoo(c: C) {
    println(c.foo())
}

printFoo(D())

上面的案例中即使呼叫 printFoo(D())還是打印出 c,而不是 d。轉成 java 中會看到下面的程式碼,D 型別在呼叫的時候會強制轉換為 C 型別:

public static final String foo(@NotNull C $receiver) {
    Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
    return "c";
  }

  public static final String foo(@NotNull D $receiver) {
    Intrinsics.checkParameterIsNotNull($receiver, "$receiver");
    return "d";
  }

  public static final void printFoo(@NotNull C c) {
    Intrinsics.checkParameterIsNotNull(c, "c");
    String var1 = foo(c);
    System.out.println(var1);
  }
  public static final void testStatically() {
    printFoo(new C());
    printFoo((C)(new D()));
  }

宣告擴充套件函式作為類的成員變數

上面的例子擴充套件函式是作為頂層函式,如果把擴充套件函式申明為類的成員變數,即擴充套件函式的作用域就在類的內部有效,案例程式碼ExtensionsAsMembers.kt

open class D { }
class D1 : D() { }

open class C { 
	open fun D.foo() { println("D.foo in C") }

	open fun D1.foo() {
    println("D1.foo in C")
	}

	fun caller(d: D) {
  	  d.foo()   // call the extension function
	}
}

class C1 : C() {
	 override fun D.foo() { println("D.foo in C1") }

	override fun D1.foo() {
    	println("D1.foo in C1")
	}
}

fun testAsMembers() { 
	C().caller(D()) // prints "D.foo in C" 
	C1().caller(D()) // prints "D.foo in C1" - dispatch receiver is resolved virtually 
	C().caller(D1()) // prints "D.foo in C" - extension receiver is resolved statically 
	C1().caller(D1()) // prints "D.foo in C1" 
}

函式 caller 的型別是 D,即使呼叫 C().caller(D1()),列印的結果還是 D.foo in C,而不是 D1.foo in C,不是執行時來動態決定型別,成員擴充套件函式申明為 open, 一旦在子類中被 override,就呼叫不到在父類中的擴充套件函式,在子類中的作用域內的只能訪問到 override 後的函式,不能像普通函式 override 那樣通過 super 關鍵字訪問了。

下面再舉幾個擴充套件函式的例子,讓大家感受一下擴充套件函式的方便:

/*
* show toast in activity
* */
fun Activity.toast(msg: String) {
    Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}

val Context.inputMethodManager: InputMethodManager?
    get() = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager

/*
* hide soft input
* */
fun Context.hideSoftInput(view: View) {
    inputMethodManager?.hideSoftInputFromWindow(view.windowToken, 0)
}


/**
 * screen width in pixels
 */
val Context.screenWidth
    get() = resources.displayMetrics.widthPixels

/**
 * screen height in pixels
 */
val Context.screenHeight
    get() = resources.displayMetrics.heightPixels

/**
 * returns dip(dp) dimension value in pixels
 * @param value dp
 */
fun Context.dip2px(value: Int): Int = (value * resources.displayMetrics.density).toInt()

推薦5

懶初始化 by lazy 和 延遲初始化 lateinit 懶初始化 by lazy 懶初始化是指推遲一個變數的初始化時機,變數在使用的時候才去例項化,這樣會更加的高效。因為我們通常會遇到這樣的情況,一個變數直到使用時才需要被初始化,或者僅僅是它的初始化依賴於某些無法立即獲得的上下文。

/*
* 懶初始化 api 例項
* */
val purchasingApi: PurchasingApi by lazy {
    val retrofit: Retrofit = Retrofit.Builder()
            .baseUrl(API_URL)
            .addConverterFactory(MoshiConverterFactory.create())
            .build()
    retrofit.create(PurchasingApi::class.java)
}

像上面的程式碼,retrofit 生成的 api 例項會在首次使用到的時候才去例項化。需要注意的是 by lazy 一般只能修飾 val 不變的物件,不能修飾 var 可變物件。

class User(var name: String, var age: Int)

/*
* 懶初始化 by lazy
* */
val user1: User by lazy {
    User("jack", 15)
}

延遲初始化 lateinit 另外,對於 var 的變數,如果型別是非空的,是必須初始化的,不然編譯不通過,這時候需要用到 lateinit 延遲初始化,使用的時候再去例項化。

/*
* 延遲初始化 lateinit
* */
lateinit var user2: User

fun testLateInit() {
    user2 = User("Lily", 14)
}

by lazy 和 lateinit 的區別 by lazy 修飾 val 的變數 lateinit 修飾 var 的變數,且變數是非空的型別

推薦6

不用再手寫 findViewById 在 Android 的 View 中,會有很多程式碼是在宣告一個 View,然後通過 findViewById 後從 xml 中例項化賦值給對應的 View。在 kotlin 中可以完全解放出來了,利用 kotlin-android-extensions 外掛,不用再手寫 findViewById。步驟如下: 詳見案例程式碼KotlinTip6

步驟 1,在專案的 gradle 中 apply plugin: ‘kotlin-android-extensions’ 步驟 2,按照原來的習慣書寫佈局 xml 檔案 ```xml <?xml version="1.0" encoding="utf-8"?> 步驟 3,在 java 程式碼中 import 對應的佈局就可以開始使用了,View 不用提前宣告,外掛會自動根據佈局的 id 生成對應的 View 成員(其實沒有生成屬性,原理見下面)

import com.sw.kotlin.tips.R
/*
* 匯入外掛生成的 View
* */
import kotlinx.android.synthetic.main.activity_tip6.*


class KotlinTip6 : Activity() {

    /*
    * 自動根據 layout 的 id 生成對應的 view
    * */
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_tip6)
        tip6Tv.text = "Auto find view for TextView"
        tip6Img.setImageBitmap(null)
        tip6Btn.setOnClickListener {
            test()
        }
    }

    private fun test(){
        tip6Tv.text = "update"
    }

}

像上面程式碼這樣,Activity 裡的三個 View 自動生成了,不用再去宣告,然後 findViewById,然後轉型賦值,是不是減少了很多沒必要的程式碼,讓程式碼非常的乾淨。

Why?原理是什麼?外掛幫我們做了什麼? 要看原理還是將上面的程式碼轉為 java 語言來理解,參照 tips4 提供的方式轉換為如下的 java 程式碼:

public final class KotlinTip6 extends Activity {
   private HashMap _$_findViewCache;

   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131296284);
      TextView var10000 = (TextView)this._$_findCachedViewById(id.tip6Tv);
      Intrinsics.checkExpressionValueIsNotNull(var10000, "tip6Tv");
      var10000.setText((CharSequence)"Auto find view for TextView");
      ((ImageView)this._$_findCachedViewById(id.tip6Img)).setImageBitmap((Bitmap)null);
      ((Button)this._$_findCachedViewById(id.tip6Btn)).setOnClickListener((OnClickListener)(new OnClickListener() {
         public final void onClick(View it) {
            KotlinTip6.this.test();
         }
      }));
   }

   private final void test() {
      TextView var10000 = (TextView)this._$_findCachedViewById(id.tip6Tv);
      Intrinsics.checkExpressionValueIsNotNull(var10000, "tip6Tv");
      var10000.setText((CharSequence) "update");
   }

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

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

      return var2;
   }

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

如上面的程式碼所示,在編譯階段,外掛會幫我們生成檢視快取,檢視由一個 Hashmap 結構的 _$_findViewCache 變數快取, 會根據對應的 id 先從快取裡查詢,快取沒命中再去真正呼叫 findViewById 查找出來,再存在 HashMap 中。

在 Fragment 中 findViewByID 在 Fragment 中也類似,但有一點需要注意但地方,例子如下:

class Tip6Fragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = inflater?.inflate(R.layout.fragment_tip6, container, false)
        /*
        * 這時候不能在 onCreateView 方法裡用 view,需要在 onViewCreate 裡,原理是外掛用了 getView 來 findViewById
        * */
        // tip6Tv.text = "test2"
        return view
    }

    /*
    * 需要在 onViewCreate 裡,原理是外掛用了 getView 來 findViewById
    * */
    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        tip6Tv.text = "test"
    }
}

如上所示,Fragment 需要注意,不能在 onCreateView 方法裡用 view,不然會出現空指標異常,需要在 onViewCreate 裡,原理是外掛用了 getView 來 findViewById, 我們看看將上面的程式碼轉成 java 後的程式碼:

public final class Tip6Fragment extends Fragment {
   private HashMap _$_findViewCache;

   @Nullable
   public View onCreateView(@Nullable LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
      View view = inflater != null?inflater.inflate(2131296286, container, false):null;
      return view;
   }

   public void onViewCreated(@Nullable View view, @Nullable Bundle savedInstanceState) {
      super.onViewCreated(view, savedInstanceState);
      TextView var10000 = (TextView)this._$_findCachedViewById(id.tip6Tv);
      Intrinsics.checkExpressionValueIsNotNull(var10000, "tip6Tv");
      var10000.setText((CharSequence)"test");
   }

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

      View var2 = (View)this._$_findViewCache.get(Integer.valueOf(var1));
      if (var2 == null) {
         View var10000 = this.getView();
         if (var10000 == null) {
            return null;
         }

         var2 = var10000.findViewById(var1);
         this._$_findViewCache.put(Integer.valueOf(var1), var2);
      }

      return var2;
   }

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

   }

   // $FF: synthetic method
   public void onDestroyView() {
      super.onDestroyView();
      this._$_clearFindViewByIdCache();
   }
}

跟 Activity 中類似,會有一個 View 的 HashMap,關鍵不同的地方在findCachedViewByIdgetViewFragmentViewonViewCreatedgetViewonDestroyView調_findCachedViewById 裡面,需要 getView 獲得當前 Fragment 的 View, 故在 onViewCreated 中 getView 還是空的,原理就好理解了。另外在 onDestroyView 會呼叫_clearFindViewByIdCache 方法清掉快取。

推薦7

利用區域性函式抽取重複程式碼 Kotlin 中提供了函式的巢狀,在函式內部還可以定義新的函式。這樣我們可以在函式中巢狀這些提前的函式,來抽取重複程式碼。如下面的案例所示:

class User(val id: Int, val name: String, val address: String, val email: String)

fun saveUser(user: User) {
    if (user.name.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${user.id}: empty Name")
    }
    if (user.address.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${user.id}: empty Address")
    }
    if (user.email.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${user.id}: empty Email")
    }
    // save to db ...
}

上面的程式碼在判斷 name、address 等是否為空的處理其實很類似。這時候,我們可以利用在函式內部巢狀的宣告一個通用的判空函式將相同的程式碼抽取到一起:

/*
* 利用區域性函式抽取相同的邏輯,去除重複的程式碼
* */
fun saveUser2(user: User) {
    fun validate(value: String, fildName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user ${user.id}: empty $fildName")
        }
    }

    validate(user.name, "Name")
    validate(user.address, "Address")
    validate(user.email, "Email")
    // save to db ...
}

除了利用巢狀函式去抽取,此時,其實也可以用擴充套件函式來抽取,如下所示:

/*
* 利用擴充套件函式抽取邏輯
* */
fun User.validateAll() {
    fun validate(value: String, fildName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user $id: empty $fildName")
        }
    }

    validate(name, "Name")
    validate(address, "Address")
    validate(email, "Email")
}

fun saveUser3(user: User) {
    user.validateAll()
    // save to db ...
}

推薦8

使用資料類來快速實現 model 類 在 java 中要宣告一個 model 類需要實現很多的程式碼,首先需要將變數宣告為 private,然後需要實現 get 和 set 方法,還要實現對應的 hashcode equals toString 方法等,如下所示:

 public static class User {

        private String name;
        private int age;
        private int gender;
        private String address;

        public String getName() {
            return name;
        }

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

        public int getAge() {
            return age;
        }

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

        public int getGender() {
            return gender;
        }

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

        public String getAddress() {
            return address;
        }

        public void setAddress(String address) {
            this.address = address;
        }

        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", gender=" + gender +
                    ", address='" + address + '\'' +
                    '}';
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            User user = (User) o;

            if (age != user.age) return false;
            if (gender != user.gender) return false;
            if (name != null ? !name.equals(user.name) : user.name != null) return false;
            return address != null ? address.equals(user.address) : user.address == null;
        }

        @Override
        public int hashCode() {
            int result = name != null ? name.hashCode() : 0;
            result = 31 * result + age;
            result = 31 * result + gender;
            result = 31 * result + (address != null ? address.hashCode() : 0);
            return result;
        }
    }

這段程式碼 Java 需要 70 行左右,而如果用 kotlin,只需要一行程式碼就可以做到。

/*
* Kotlin 會為類的引數自動實現 get set 方法
* */
class User(val name: String, val age: Int, val gender: Int, var address: String)

/*
* 用 data 關鍵詞來宣告一個數據類,除了會自動實現 get set,還會自動生成 equals hashcode toString
* */
data class User2(val name: String, val age: Int, val gender: Int, var address: String)

對於 Kotlin 中的類,會為它的引數自動實現 get set 方法。而如果加上 data 關鍵字,還會自動生成 equals hashcode toString。原理其實資料類中的大部分程式碼都是模版程式碼,Kotlin 聰明的將這個模版程式碼的實現放在了編譯器處理的階段。

推薦9

用類委託來快速實現裝飾器模式 通過繼承的實現容易導致脆弱性,例如如果需要修改其他類的一些行為,這時候 Java 中的一種策略是採用裝飾器模式:建立一個新類,實現與原始類一樣的介面並將原來的類的例項作為一個成員變數。 與原始類擁有相同行為的方法不用修改,只需要直接轉發給原始類的例項。如下所示: 詳見案例程式碼KotlinTip9

/*
* 常見的裝飾器模式,為了修改部分的函式,卻需要實現所有的介面函式
* */
class CountingSet<T>(val innerSet: MutableCollection<T> = HashSet<T>()) : MutableCollection<T> {

    var objectAdded = 0

    override val size: Int
        get() = innerSet.size

    /*
    * 需要修改的方法
    * */
    override fun add(element: T): Boolean {
        objectAdded++
        return innerSet.add(element)
    }

    /*
    * 需要修改的方法
    * */
    override fun addAll(elements: Collection<T>): Boolean {
        objectAdded += elements.size
        return innerSet.addAll(elements)
    }

    override fun contains(element: T): Boolean {
        return innerSet.contains(element)
    }

    override fun containsAll(elements: Collection<T>): Boolean {
        return innerSet.containsAll(elements)
    }

    override fun isEmpty(): Boolean {
        return innerSet.isEmpty()
    }

    override fun clear() {
        innerSet.clear()
    }

    override fun iterator(): MutableIterator<T> {
        return innerSet.iterator()
    }

    override fun remove(element: T): Boolean {
        return innerSet.remove(element)
    }

    override fun removeAll(elements: Collection<T>): Boolean {
        return innerSet.removeAll(elements)
    }

    override fun retainAll(elements: Collection<T>): Boolean {
        return innerSet.retainAll(elements)
    }

}

如上所示,想要修改 HashSet 的某些行為函式 add 和 addAll,需要實現 MutableCollection 介面的所有方法,將這些方法轉發給 innerSet 去具體的實現。雖然只需要修改其中的兩個方法,其他程式碼都是模版程式碼。 只要是重複的模版程式碼,Kotlin 這種全新的語法糖就會想辦法將它放在編譯階段再去生成。 這時候可以用到類委託 by 關鍵字,如下所示:

/*
* 通過 by 關鍵字將介面的實現委託給 innerSet 成員變數,需要修改的函式再去 override 就可以了
* */
class CountingSet2<T>(val innerSet: MutableCollection<T> = HashSet<T>()) : MutableCollection<T> by innerSet {

    var objectAdded = 0

    override fun add(element: T): Boolean {
        objectAdded++
        return innerSet.add(element)
    }

    override fun addAll(elements: Collection<T>): Boolean {
        objectAdded += elements.size
        return innerSet.addAll(elements)
    }
}

通過 by 關鍵字將介面的實現委託給 innerSet 成員變數,需要修改的函式再去 override 就可以了,通過類委託將 10 行程式碼就可以實現上面接近 100 行的功能,簡潔明瞭,去掉了模版程式碼。

推薦10

Lambda 表示式簡化 OnClickListener 詳見案例程式碼KotlinTip10 lambda 表示式可以簡化我們的程式碼。以 Android 中常見的 OnClickListener 來說明,在 Java 中我們一般這樣設定:

 TextView textView = new TextView(context);
        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // handle click
            }
        });

Java 中需要宣告一個匿名內部類去處理,這種情況可以用 lambda 表示式來簡化。

lambda 表示式一般長這樣 { x:Int, y:Int -> x+y } 引數 -> 表示式 並且始終在大括號中 it 作為預設引數名 lambda 捕捉,當捕捉 final 變數時,它的值和 lambda 程式碼一起儲存 非 final 變數,它的值被封裝在一個特殊的包裝器中,這個包裝器的引用會和 lambda 程式碼一起儲存 我們來看看 Kotlin 中的例子:

 val textView = TextView(context)

    /*
    * 傳統方式
    * */
    textView.setOnClickListener(object : android.view.View.OnClickListener {
        override fun onClick(v: android.view.View?) {
            // handle click
        }
    })

    /*
    * lambda 的方式
    * */
    textView.setOnClickListener({ v ->
        {
            // handle click
        }
    })

當 lambda 的引數沒有使用時可以省略,省略的時候用 it 來替代

 /*
    * lambda 的引數如果沒有使用可以省略,省略的時候用 it 來替代
    * */
    textView.setOnClickListener({
        // handle click
    })

lambda 在引數的最後一個的情況可以將之提出去

 /*
    * lambda 在引數的最後一個的情況可以將之提出去
    * */
    textView.setOnClickListener() {
        // handle click
    }

lambda 提出去之後,函式如果沒有其他引數括號可以省略

/*
    * lambda 提出去之後,函式如果沒有其他引數括號可以省略
    * */
    textView.setOnClickListener {
        // handle click
    }

我們再看看如果自己去實現一個帶 lambda 引數的函式應該怎麼去定義:

interface OnClickListener {
    fun onClick()
}

class View {
    var listener: OnClickListener? = null;

    /*
    * 傳統方式
    * */
    fun setOnClickListener(listener: OnClickListener) {
        this.listener = listener
    }

    fun doSth() {
        // some case:
        listener?.onClick()
    }

    /*
    * 宣告 lambda 方式,listener: () -> Unit
    * */
    fun setOnClickListener(listener: () -> Unit) {

    }
}

在函式引數中需要宣告 lambda 的型別後,再呼叫該函式的時候就可以傳入一個 lambda 表示式了。

推薦11

with 函式來簡化程式碼 with 函式原型: inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block() with 函式並不是擴充套件函式,返回值是最後一行,可以直接呼叫物件的方法 Kotlin 中可以用 with 語句來省略同一個變數的多次宣告,例如下面的函式

/*
*列印字母表函式,在函式內 result 變數在好幾處有使用到
* */
fun alphabet(): String {
    val result = StringBuilder()
    result.append("START\n")
    for (letter in 'A'..'Z') {
        result.append(letter)
    }
    result.append("\nEND")
    return result.toString()
}

在上面的函式中,result 變量出現了 5 次,如果用 with 語句,可以將這 5 次都不用再出現了,我們來一步一步地看是怎麼實現的:

/*
* 通過 with 語句,將 result 作為引數傳入,在內部就可以通過 this 來表示 result 變量了
* */
fun alphabet2(): String {
    val result = StringBuilder()
    return with(result) {
        this.append("START\n")
        for (letter in 'A'..'Z') {
            this.append(letter)
        }
        this.append("\nEND")
        this.toString()
    }
}

通過 with 語句,將 result 作為引數傳入,在內部就可以通過 this 來表示 result 變量了,而且這個 this 是可以省略的

/*
* 通過 with 語句,將 result 引數作為引數,在內部 this 也可以省略掉
* */
fun alphabet3(): String {
    val result = StringBuilder()
    return with(result) {
        append("START\n")
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("\nEND")
        toString()
    }
}

在內部 this 省略掉後,現在只有一個 result 了,這個其實也是沒必要的,於是出現了下面的最終版本:

/*
* 通過 with 語句,可以直接將物件傳入,省掉物件的宣告
* */
fun alphabet4(): String {
    return with(StringBuilder()) {
        append("START\n")
        for (letter in 'A'..'Z') {
            append(letter)
        }
        append("\nEND")
        toString()
    }
}

像上面這樣,我們可以把同一個變數的顯式呼叫從 5 次變為 0 次,發現 Kotlin 的魅力了吧。

推薦12

apply 函式來簡化程式碼 apply 函式原型: inline fun T.apply(block: T.() -> Unit): T { block(); return this } apply 函式,在函式範圍內,可以任意呼叫該物件的任意方法,並返回該物件 除了用上面的 with 可以簡化同一個變數的多次宣告,還可以用 apply 關鍵字,我們來改造一下 tip11 中的函式: 詳見案例程式碼KotlinTip12

/*
* 用 apply 語句簡化程式碼,在 apply 的大括號裡可以訪問類的公有屬性和方法
* */
fun alphabet5() = StringBuilder().apply {
    append("START\n")
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nEND")
}.toString()

像上面這樣的,通過 apply 後,在 apply 的大括號裡可以訪問類的公有屬性和方法。這在對應類的初始化是非常方便的,例如下面的例子

/*
* 用 apply 語句簡化類的初始化,在類例項化的時候,就可以通過 apply 把需要初始化的步驟全部實現,非常的簡潔
* */
fun testApply(context: Context) {
    var imgView = ImageView(context).apply {
        setBackgroundColor(0)
        setImageBitmap(null)
    }

    var textView = TextView(context).apply {
        text = "content"
        textSize = 20.0f
        setPadding(10, 0, 0, 0)
    }

    var user = User().apply { 
        age = 15
        name = "Jack"
        val a = address
        address = "bbb"
    }
}

在類例項化的時候,就可以通過 apply 把需要初始化的步驟全部實現,非常的簡潔

推薦13

在編譯階段避免掉 NullPointerException NullPointerException 是 Java 程式設計師非常頭痛的一個問題,我們知道 Java 中分受檢異常和非受檢異常,NullPointerException 是非受檢異常,也就是說 NullPointerException 不需要顯示的去 catch 住, 往往在執行期間,程式就可能報出一個 NullPointerException 然後 crash 掉,Kotlin 作為一門高效安全的語言,它嘗試在編譯階段就把空指標問題顯式的檢測出來,把問題留在了編譯階段,讓程式更加健壯。 詳見案例程式碼KotlinTip13

Kotlin 中型別分為可空型別和不可空型別,通過?代表可空,不帶?代表不可為空

fun testNullType() {
  val a: String = "aa"
  /*
  * a 是非空型別,下面的給 a 賦值為 null 將會編譯不通過
  * */
  // a = null
  a.length

  /*
  * ?宣告是可空型別,可以賦值為 null
  * */
  var b: String? = "bb"
  b = null

  /*
  * b 是可空型別,直接訪問可空型別將編譯不通過,需要通過?.或者!!.來訪問
  * */
  // b.length
  b?.length
  b!!.length
}

對於一個不可為空型別:如果直接給不可為空型別賦值一個可能為空的物件就在編譯階段就不能通過 對於一個可空型別:通過?宣告,在訪問該型別的時候直接訪問不能編譯通過,需要通過?.或者!!.

?. 代表著如果該型別為空的話就返回 null 不做後續的操作,如果不為空的話才會去訪問對應的方法或者屬性 !!. 代表著如果該型別為空的話就丟擲 NullPointerException,如果不為空就去訪問對應的方法或者屬性, 所以只有在很少的特定場景才用這種符號,代表著程式不處理這種異常的 case 了,會像 java 程式碼一樣丟擲 NullPointerException。 而且程式碼中一定不用出現下面這種程式碼,會讓程式碼可讀性很差而且如果有空指標異常,我們也不能馬上發現是哪空了:

 /*
不推薦這樣的寫法:鏈式的連續用!!.
*/ 
val user = User() user!!.name!!.subSequence(0,5)!!.length 
對應一個可空型別,每次對它的訪問都需要帶上?.判斷kotlin val user: User? = User()

/*
每次訪問都用用?.判斷
*/ 
user?.name user?.age user?.toString() 
/*但這樣多了很多程式碼,kotlin 做了一些優化,
或者提前判斷是否為空,如果不為空在這個分支裡會自動轉化為非空型別就可以直接訪問了
*/
 if (user != null) { user.name user.age user.toString() }

通過 if 提前判斷型別是否為空,如果不為空在這個分支裡會*自動轉化為非空型別就可以直接訪問了。 let 語句簡化對可空物件對訪問 let 函式原型: inline fun <T, R> T.let(block: (T) -> R): R = block(this) let 函式預設當前這個物件作為閉包的 it 引數,返回值是函式裡面最後一行,或者指定 return。 上面的程式碼還可以用?.let 語句進行,如下所示:

  /*
    * 通過 let 語句,在?.let 之後,如果為空不會有任何操作,只有在非空的時候才會執行 let 之後的操作
    * */
    user?.let {
        it.name
        it.age
        it.toString()
    }

通過 let 語句,在?.let 之後,如果為空不會有任何操作,只有在非空的時候才會執行 let 之後的操作

Elvis 操作符 ?: 簡化對空值的處理 如果值可能為空,對空值的處理可能會比較麻煩,像下面這樣:

/*
* 對空值的處理
* */
fun testElvis(input: String?, user: User?) {
    val a: Int?
    if (input == null) {
        a = -1
    } else {
        a = input?.length
    }

    if (user == null) {
        var newOne = User()
        newOne.save()
    } else {
        user.save()
    }
}

Elvis 操作符?:能夠簡化上面的操作,?:符號會在符號左邊為空的情況才會進行下面的處理,不為空則不會有任何操作。跟?.let 正好相反,例如我們可以用兩行程式碼來簡化上面從操作:

/**
 * Elvis 操作符 ?: 簡化對空值的處理
 */
fun testElvis2(input: String?, user: User?) {
    val b = input?.length ?: -1;
    user?.save() ?: User().save()
}

推薦14

運算子過載 Kotlin 支援對運算子的過載,這對於對一些物件的操作更加靈活直觀。

使用 operator 來修飾 plus\minus 函式 可過載的二元算術符 A * B times A / B div A % B mod A + B plus A - B minus 以下面對座標點 Point 的案例說明怎麼去過載運算子:

class Point(val x: Int, val y: Int) {

    /*
    * plus 函式過載對 Point 物件的加法運算子
    * */
    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }

    /*
    * minus 函式過載對 Point 物件的減法運算子
    * */
    operator fun minus(other: Point): Point {
        return Point(x - other.x, y - other.y)
    }

    override fun toString(): String {
        return "[x:$x, y:$y]"
    }

}

如上所示,通過 plus 函式過載對 Point 物件的加法運算子,通過 minus 函式過載對 Point 物件的減法運算子,然後就可以用+、-號對兩個物件進行操作了:

fun testOperator() {
    val point1 = Point(10, 10)
    val point2 = Point(4, 4)
    val point3 = point1 + point2
    println(point3)
    println(point1 - point2)
}

推薦15

高階函式簡化程式碼 高階函式:以另一個函式作為引數或者返回值的函式 函式型別 (Int, String) -> Unit 引數型別->返回型別 Unit 不能省略 val list = listOf(2, 5, 10)

    /*
    * 傳入函式來過濾
    * */
    println(list.filter { it > 4 })

    /*
    * 定義函式型別
    * */
    val sum = { x: Int, y: Int -> x + y }
    val action = { println(42) }

    val sum2: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
    val action2: () -> Unit = { println(42) }

函式作為引數 函式作為引數,即高階函式中,函式的引數可以是一個函式型別,例如要定義一個函式,該函式根據傳入的操作函式來對 2 和 3 做相應的處理。

/*
* 定義對 2 和 3 的操作函式
* */
fun twoAndThree(operator: (Int, Int) -> Int) {
    val result = operator(2, 3)
    println("Result:$result")
}

fun test03() {
    twoAndThree { a, b -> a + b }
    twoAndThree { a, b -> a * b }
}

operator 是函式型別,函式的具體型別為(Int, Int) -> Int,即輸入兩個 Int 返回一個 Int 值。定義完了後就可以像上面這樣使用了。 再舉一個例子,實現 String 類的字元過濾:

/*
* 函式作為引數,實現 String 類的字元過濾
* */
fun String.filter(predicate: (Char) -> Boolean): String {
    val sb = StringBuilder()
    for (index in 0 until length) {
        val element = get(index)
        if (predicate(element)) sb.append(element)
    }
    return sb.toString()
}

fun test04() {
    println("12eafsfsfdbzzsa".filter { it in 'a'..'f' })
}

像上面這樣 predicate 是函式型別,它會根據傳入的 char 來判斷得到一個 Boolean 值。

函式作為返回值 函式作為返回值也非常實用,例如我們的需求是根據不同的快遞型別返回不同計價公式,普通快遞和高階快遞的計價規則不一樣,這時候我們可以將計價規則函式作為返回值:

enum class Delivery { STANDARD, EXPEDITED }

/*
* 根據不同的運輸型別返回不同的快遞方式
* */
fun getShippingCostCalculator(delivery: Delivery): (Int) -> Double {
    if (delivery == Delivery.EXPEDITED) {
        return { 6 + 2.1 * it }
    }
    return { 1.3 * it }
}

fun test05() {
    val calculator1 = getShippingCostCalculator(Delivery.EXPEDITED)
    val calculator2 = getShippingCostCalculator(Delivery.STANDARD)
    println("Ex costs ${calculator1(5)}")
    println("St costs ${calculator2(5)}")
}

如果是普通快遞,採用 1.3 it 的規則計算價格,如果是高階快遞按照 6 + 2.1 it 計算價格,根據不同的型別返回不同的計價函式。

推薦16

用 Lambda 來簡化策略模式 策略模式是常見的模式之一,java 的例子如下。

/**
     * 定義策略介面
     */
    public interface Strategy {
        void doSth();
    }

    /**
     * A 策略
     */
    public static class AStrategy implements Strategy {
        @Override
        public void doSth() {
            System.out.println("Do A Strategy");
        }
    }

    /**
     * B 策略
     */
    public static class BStrategy implements Strategy {
        @Override
        public void doSth() {
            System.out.println("Do B Strategy");
        }
    }

    /**
     * 策略實施者
     */
    public static class Worker {

        private Strategy strategy;

        public Worker(Strategy strategy) {
            this.strategy = strategy;
        }

        public void work() {
            System.out.println("START");
            if (strategy != null) {
                strategy.doSth();
            }
            System.out.println("END");
        }
    }

如上面的例子所示,有 A、B 兩種策略,Worker 根據不同的策略做不同的工作,使用策略時:

Worker worker1 = new Worker(new AStrategy());
Worker worker2 = new Worker(new BStrategy());
worker1.work();
worker2.work();

在 java 中實現這種策略模式難免需要先定義好策略的介面,然後根據介面實現不同的策略, 在 Kotlin 中完全可以用用 Lambda 來簡化策略模式,上面的例子用 Kotlin 實現:

/**
 * 策略實施者
 * @param strategy lambda 型別的策略
 */
class Worker(private val strategy: () -> Unit) {
    fun work() {
        println("START")
        strategy.invoke()
        println("END")
    }
}

/*
* 測試
* */
fun testStrategy() {
    val worker1 = Worker({
        println("Do A Strategy")
    })
    val bStrategy = {
        println("Do B Strategy")
    }
    val worker2 = Worker(bStrategy)
    worker1.work()
    worker2.work()
}

不需要先定義策略的介面,直接把策略以 lambda 表示式的形式傳進來就行了。