促進 Android 開發的 10 個 Kotlin 特性

已發表: 2022-03-11

介紹

前段時間,Tomasz 介紹了 Android 上的 Kotlin 開發。 提醒您:Kotlin 是由 Jetbrains 開發的一種新的編程語言,Jetbrains 是最受歡迎的 Java IDE 之一 IntelliJ IDEA 背後的公司。 與 Java 一樣,Kotlin 是一種通用語言。 由於它符合 Java 虛擬機 (JVM) 字節碼,因此可以與 Java 並行使用,並且不會帶來性能開銷。

在本文中,我將介紹促進 Android 開發的 10 大有用功能。

注意:在撰寫本文時,實際版本為 Android Studio 2.1.1。 和 Kotlin 1.0.2。

科特林

厭倦了永無止境的 Java 代碼? 嘗試 Kotlin 並節省您的時間和理智。
鳴叫

Kotlin 設置

由於 Kotlin 是由 JetBrains 開發的,因此它在 Android Studio 和 IntelliJ 中都得到了很好的支持。

第一步是安裝 Kotlin 插件。 成功執行此操作後,將可以使用新操作將您的 Java 轉換為 Kotlin。 兩個新選項是:

  1. 創建一個新的 Android 項目並在項目中設置 Kotlin。
  2. 將 Kotlin 支持添加到現有的 Android 項目。

要了解如何創建新的 Android 項目,請查看官方分步指南。 要將 Kotlin 支持添加到新創建或現有項目中,請在 Mac 上使用Command + Shift + A或在 Windows/Linux 上使用Ctrl + Shift + A打開查找操作對話框,然後調用Configure Kotlin in Project操作。

要創建一個新的 Kotlin 類,請選擇:

  • File > New > Kotlin file/class ,或
  • File > New > Kotlin activity

或者,您可以創建一個 Java 類並使用上述操作將其轉換為 Kotlin。 請記住,您可以使用它來轉換任何類、接口、枚舉或註解,這可以用來輕鬆地將 Java 與 Kotlin 代碼進行比較。

另一個節省大量輸入的有用元素是 Kotlin 擴展。 要使用它們,您必須在模塊build.gradle文件中應用另一個插件:

 apply plugin: 'kotlin-android-extensions'

警告:如果您使用 Kotlin 插件操作來設置您的項目,它會將以下代碼放入您的頂級build.gradle文件中:

 buildscript { ext.kotlin_version = '1.0.2' repositories { jcenter() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } }

這將導致擴展程序不起作用。 要解決這個問題,只需將該代碼複製到您希望使用 Kotlin 的每個項目模塊中。

如果您正確設置了所有內容,您應該能夠像在標準 Android 項目中一樣運行和測試您的應用程序,但現在使用的是 Kotlin。

使用 Kotlin 節省時間

因此,讓我們從描述 Kotlin 語言的一些關鍵方面開始,並提供有關如何通過使用它而不是 Java 來節省時間的提示。

功能 #1:靜態佈局導入

Android 中最常見的樣板代碼之一是使用findViewById()函數來獲取對活動或片段中視圖的引用。

有一些解決方案,例如 Butterknife 庫,可以節省一些輸入,但 Kotlin 又邁出了一步,允許您通過一次導入從佈局中導入對視圖的所有引用。

例如,考慮以下活動 XML 佈局:

 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:andro xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="co.ikust.kotlintest.MainActivity"> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content"/> </RelativeLayout>

以及隨附的活動代碼:

 package co.ikust.kotlintest import android.support.v7.app.AppCompatActivity import android.os.Bundle import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) helloWorldTextView.text = "Hello World!" } }

要獲取佈局中具有定義 ID 的所有視圖的引用,請使用 Android Kotlin 擴展 Anko。 請記住輸入此導入語句:

 import kotlinx.android.synthetic.main.activity_main.*

請注意,您不需要在 Kotlin 中的行尾寫分號,因為它們是可選的。

佈局中的TextView被導入為TextView實例,其名稱等於視圖的 ID。 不要被用於設置標籤的語法所迷惑:

 helloWorldTextView.text = "Hello World!"

我們將很快介紹這一點。

注意事項

  • 確保導入正確的佈局,否則導入的視圖引用將具有null值。
  • 使用片段時,請確保在onCreateView()函數調用之後使用導入的視圖引用。 在onCreateView()函數中導入佈局,並使用視圖引用在onViewCreated()中設置 UI。 在onCreateView()方法完成之前不會分配引用。

特性 #2:使用 Kotlin 編寫 POJO 類

使用 Kotlin 可以節省最多時間的方法是編寫用於保存數據的 POJO(Plain Old Java Object)類。 例如,在 RESTful API 的請求和響應正文中。 在依賴 RESTful API 的應用程序中,會有很多這樣的類。

在 Kotlin 中,為您做了很多事情,而且語法簡潔。 例如,考慮 Java 中的以下類:

 public class User { private String firstName; private String lastName; public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } }

使用 Kotlin 時,您不必再次編寫 public 關鍵字。 默認情況下,一切都是公共範圍的。 例如,如果要聲明一個類,只需編寫:

 class MyClass { }

上面 Kotlin 中的 Java 代碼的等價物:

 class User { var firstName: String? = null var lastName: String? = null }

嗯,這樣可以節省很多打字時間,不是嗎? 讓我們來看看 Kotlin 代碼。

Kotlin 節省了大量的打字工作

在 Kotlin 中定義變量時,有兩種選擇:

  • 可變變量,由var關鍵字定義。
  • 不可變變量,由val關鍵字定義。

接下來要注意的是語法與 Java 有點不同。 首先,聲明變量名,然後跟上類型。 此外,默認情況下,屬性是非空類型,這意味著它們不能接受null值。 要定義一個接受null值的變量,必須在類型之後添加一個問號。 稍後我們將在 Kotlin 中討論這一點和零安全性。

另一個需要注意的重要事情是 Kotlin 沒有為類聲明字段的能力。 只能定義屬性。 因此,在這種情況下, firstNamelastName是已分配默認 getter/setter 方法的屬性。 如前所述,在 Kotlin 中,默認情況下它們都是公共的。

可以編寫自定義訪問器,例如:

 class User { var firstName: String? = null var lastName: String? = null val fullName: String? get() firstName + " " + lastName }

從外部看,在語法方面,屬性的行為類似於 Java 中的公共字段:

 val userName = user.firstName user.firstName = "John"

請注意,新屬性fullName是只讀的(由val關鍵字定義)並具有自定義 getter; 它只是附加名字和姓氏。

Kotlin 中的所有屬性都必須在聲明時或在構造函數中時賦值。 在某些情況下不方便; 例如,對於將通過依賴注入初始化的屬性。 在這種情況下,可以使用lateinit修飾符。 這是一個例子:

 class MyClass { lateinit var firstName : String; fun inject() { firstName = "John"; } }

有關屬性的更多詳細信息可以在官方文檔中找到。

特性#3:類繼承和構造函數

Kotlin 在構造函數方面也有更簡潔的語法。

構造函數

Kotlin 類有一個主構造函數和一個或多個輔助構造函數。 定義主構造函數的示例:

 class User constructor(firstName: String, lastName: String) { }

主構造函數位於類定義中的類名之後。 如果主構造函數沒有任何註釋或可見性修飾符,則可以省略構造函數關鍵字:

 class Person(firstName: String) { }

請注意,主構造函數不能有任何代碼; 任何初始化都必須在init代碼塊中完成:

 class Person(firstName: String) { init { //perform primary constructor initialization here } }

此外,主構造函數可用於定義和初始化屬性:

 class User(var firstName: String, var lastName: String) { // ... }

就像常規屬性一樣,從主構造函數定義的屬性可以是不可變的 ( val ) 或可變的 ( var )。

類也可能有輔助構造函數; 定義一個的語法如下:

 class User(var firstName: String, var lastName) { constructor(name: String, parent: Person) : this(name) { parent.children.add(this) } }

請注意,每個輔助構造函數都必須委託給主構造函數。 這類似於使用this關鍵字的 Java:

 class User(val firstName: String, val lastName: String) { constructor(firstName: String) : this(firstName, "") { //... } }

實例化類時,請注意 Kotlin 沒有new關鍵字,Java 也是如此。 要實例化上述User類,請使用:

 val user = User("John", "Doe)

介紹繼承

在 Kotlin 中,所有類都從Any擴展而來,這類似於 Java 中的Object 。 默認情況下,類是關閉的,就像 Java 中的最終類一樣。 因此,為了擴展一個類,它必須被聲明為openabstract

 open class User(val firstName, val lastName) class Administrator(val firstName, val lastName) : User(firstName, lastName)

請注意,您必須委託給擴展類的默認構造函數,這類似於在 Java 中的新類的構造函數中調用super()方法。

有關類的更多詳細信息,請查看官方文檔。

功能 #4:Lambda 表達式

隨 Java 8 引入的 Lambda 表達式是它最喜歡的特性之一。 然而,Android 上的情況並不那麼樂觀,因為它仍然只支持 Java 7,而且看起來 Java 8 不會很快得到支持。 因此,Retrolambda 等變通方法將 lambda 表達式引入 Android。

使用 Kotlin,不需要額外的庫或變通方法。

Kotlin 中的函數

讓我們先快速回顧一下 Kotlin 中的函數語法:

 fun add(x: Int, y: Int) : Int { return x + y }

函數的返回值可以省略,在這種情況下,函數將返回Int 。 值得重申的是,Kotlin 中的一切都是對象,從Any擴展而來,並且沒有原始類型。

函數的參數可以有一個默認值,例如:

 fun add(x: Int, y: Int = 1) : Int { return x + y; }

在這種情況下,可以通過僅傳遞x參數來調用add()函數。 等效的 Java 代碼將是:

 int add(int x) { Return add(x, 1); } int add(int x, int y) { return x + y; }

調用函數時的另一個好處是可以使用命名參數。 例如:

 add(y = 12, x = 5)

有關函數的更多詳細信息,請查看官方文檔。

在 Kotlin 中使用 Lambda 表達式

Kotlin 中的 Lambda 表達式可以被視為 Java 中的匿名函數,但語法更簡潔。 作為一個例子,讓我們展示如何在 Java 和 Kotlin 中實現點擊監聽器。

在 Java 中:

 view.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Toast.makeText(v.getContext(), "Clicked on view", Toast.LENGTH_SHORT).show(); } };

在科特林:

 view.setOnClickListener({ view -> toast("Click") })

哇! 只需一行代碼! 我們可以看到 lambda 表達式被花括號包圍。 參數首先聲明,主體在->符號之後。 使用單擊偵聽器,未指定視圖參數的類型,因為它可以被推斷出來。 主體只是對用於顯示 Toast 的toast()函數的調用,這是 Kotlin 提供的。

此外,如果不使用參數,我們可以將它們排除在外:

 view.setOnClickListener({ toast("Click") })

Kotlin 優化了 Java 庫,任何接收接口的函數都可以使用函數參數(而不是接口)調用參數的一種方法。

此外,如果函數是最後一個參數,則可以將其移出括號:

 view.setOnClickListener() { toast("Click") }

最後,如果函數只有一個參數是函數,則可以省略括號:

 view.setOnClickListener { toast("Click") }

有關更多信息,請查看 Antonio Leiva 的 Kotlin for Android 開發人員一書和官方文檔。

擴展功能

Kotlin 與 C# 類似,提供了通過使用擴展函數來擴展現有類的能力。 例如,將計算String的 MD5 哈希的擴展方法:

 fun String.md5(): ByteArray { val digester = MessageDigest.getInstance("MD5") digester.update(this.toByteArray(Charset.defaultCharset())) return digester.digest() }

請注意,函數名稱前面是擴展類的名稱(在本例中為String ),並且擴展類的實例可通過this關鍵字獲得。

擴展函數等價於 Java 實用函數。 Java 中的示例函數如下所示:

 public static int toNumber(String instance) { return Integer.valueOf(instance); }

示例函數必須放在 Utility 類中。 這意味著擴展函數不會修改原始擴展類,而是一種編寫實用方法的便捷方式。

功能 #5:空安全

在 Java 中你最忙的事情之一可能是NullPointerException 。 Null-safety 是一項已集成到 Kotlin 語言中的功能,並且非常隱含,您通常不必擔心。 官方文檔指出NullPointerExceptions的唯一可能原因是:

  • 顯式調用 throw NullPointerException
  • 使用!! 運算符(我稍後會解釋)。
  • 外部 Java 代碼。
  • 如果在UninitializedPropertyAccessException之前在構造函數中訪問了lateinit屬性,則會拋出 UninitializedPropertyAccessException。

默認情況下,如果 Kotlin 中的所有變量和屬性沒有顯式聲明為可空,則它們都被視為non-null (無法保存null值)。 如前所述,要定義一個接受null值的變量,必須在類型後添加一個問號。 例如:

 val number: Int? = null

但是,請注意以下代碼不會編譯:

 val number: Int? = null number.toString()

這是因為編譯器執行null檢查。 要編譯,必須添加一個null檢查:

 val number: Int? = null if(number != null) { number.toString(); }

此代碼將成功編譯。 在這種情況下,Kotlin 在後台所做的是,該number在 if 塊內變為nun-nullInt而不是Int? )。

可以使用安全調用運算符( ?. ) 簡化null檢查:

 val number: Int? = null number?.toString()

僅當數字不為null時才會執行第二行。 您甚至可以使用著名的Elvis 運算符( ?: ):

 val number Int? = null val stringNumber = number?.toString() ?: "Number is null"

如果?:左側的表達式不為null ,則計算並返回。 否則,返回右側表達式的結果。 另一個巧妙的事情是,您可以在 Elvis 運算符的右側使用throwreturn ,因為它們是 Kotlin 中的表達式。 例如:

 fun sendMailToUser(user: User) { val email = user?.email ?: throw new IllegalArgumentException("User email is null") //... }

這 !! 操作員

如果你想像在 Java 中一樣拋出NullPointerException ,你可以用!! 操作員。 以下代碼將拋出NullPointerException

 val number: Int? = null number!!.toString()

鑄件

使用as關鍵字進行投射:

 val x: String = y as String

這被認為是“不安全”的強制轉換,因為如果無法進行強制轉換,它將拋出ClassCastException ,就像 Java 那樣。 有一個“安全”轉換運算符返回null值而不是拋出異常:

 val x: String = y as? String

有關轉換的更多詳細信息,請查看官方文檔的 Type Casts 和 Casts 部分,有關null安全性的更多詳細信息,請查看 Null-Safety 部分。

lateinit屬性

在某些情況下,使用lateinit屬性可能會導致類似於NullPointerException的異常。 考慮以下類:

 class InitTest { lateinit var s: String; init { val len = this.s.length } }

此代碼將在沒有警告的情況下編譯。 但是,一旦創建了TestClass的實例,就會拋出UninitializedPropertyAccessException ,因為屬性s在初始化之前就被訪問了。

功能 #6:函數with()

with()函數很有用,它隨 Kotlin 標準庫提供。 如果您需要訪問對象的許多屬性,它可以用來節省一些輸入。 例如:

 with(helloWorldTextView) { text = "Hello World!" visibility = View.VISIBLE }

它接收一個對象和一個擴展函數作為參數。 代碼塊(在花括號中)是指定為第一個參數的對象的擴展函數的 lambda 表達式。

功能 #7:運算符重載

使用 Kotlin,可以為一組預定義的運算符提供自定義實現。 要實現運算符,必須提供具有給定名稱的成員函數或擴展函數。

例如,要實現乘法運算符,必須提供名稱為times(argument)的成員函數或擴展函數:

 operator fun String.times(b: Int): String { val buffer = StringBuffer() for (i in 1..b) { buffer.append(this) } return buffer.toString() }

上面的示例顯示了String上二元*運算符的實現。 例如,以下表達式會將值“TestTestTestTest”分配給newString變量:

 val newString = "Test" * 4

由於可以使用擴展函數,這意味著可以更改所有對象的運算符的默認行為。 這是一把雙刃劍,應謹慎使用。 有關可以重載的所有運算符的函數名稱列表,請查看官方文檔。

與 Java 相比的另一個重大區別是==!=運算符。 運算符==轉換為:

 a?.equals(b) ?: b === null

而運算符!=轉換為:

 !(a?.equals(b) ?:

這意味著,使用==不會像在 Java 中那樣進行身份檢查(比較對象的實例是否相同),但其行為方式與equals()方法以及null檢查相同。

要執行身份檢查,必須在 Kotlin 中使用運算符===!==

特性 #8:委託屬性

某些屬性有一些共同的行為。 例如:

  • 首次訪問時初始化的延遲初始化屬性。
  • 在觀察者模式中實現 Observable 的屬性。
  • 存儲在地圖中的屬性,而不是作為單獨的字段。

為了使這種情況更容易實現,Kotlin 支持Delegated Properties

 class SomeClass { var p: String by Delegate() }

這意味著屬性p的 getter 和 setter 函數由另一個類的實例Delegate處理。

String屬性的委託示例:

 class Delegate { operator fun getValue(thisRef: Any?, property: KProperty<*>): String { return "$thisRef, thank you for delegating '${property.name}' to me!" } operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { println("$value has been assigned to '${property.name} in $thisRef.'") } }

上面的示例在分配或讀取屬性時打印一條消息。

可以為可變 ( var ) 和只讀 ( val ) 屬性創建委託。

對於只讀屬性,必須實現getValue方法。 它有兩個參數(取自官方文檔):

  • 接收者 - 必須是屬性所有者的相同類型或超類型(對於擴展屬性,它是被擴展的類型)。
  • 元數據 - 必須是KProperty<*>類型或其超類型。

此函數必須返回與屬性或其子類型相同的類型。

對於可變屬性,委託必須另外提供一個名為setValue的函數,該函數採用以下參數:

  • 接收者 - 與getValue()相同。
  • 元數據 - 與getValue()相同。
  • 新值 - 必須與屬性或其超類型具有相同的類型。

Kotlin 附帶了一些標準代表,涵蓋了最常見的情況:

  • 懶惰的
  • 可觀察的
  • 可否決

懶惰的

Lazy 是一個標準委託,它接受一個 lambda 表達式作為參數。 傳遞的 lambda 表達式在第一次調用getValue()方法時執行。

默認情況下,惰性屬性的評估是同步的。 如果你不關心多線程,你可以使用lazy(LazyThreadSafetyMode.NONE) { … }來獲得額外的性能。

可觀察的

Delegates.observable()用於在觀察者模式中表現得像 Observables 的屬性。 它接受兩個參數,初始值和一個具有三個參數(屬性、舊值和新值)的函數。

每次調用setValue()方法時,都會執行給定的 lambda 表達式:

 class User { var email: String by Delegates.observable("") { prop, old, new -> //handle the change from old to new value } }

可否決

這個標準委託是一種特殊的 Observable,可以讓您決定是否存儲分配給屬性的新值。 它可用於在分配值之前檢查某些條件。 與Delegates.observable()一樣,它接受兩個參數:初始值和一個函數。

不同之處在於該函數返回一個布爾值。 如果它返回true ,則分配給該屬性的新值將被存儲或以其他方式丟棄。

 var positiveNumber = Delegates.vetoable(0) { d, old, new -> new >= 0 }

給定的示例將僅存儲分配給屬性的正數。

更多詳細信息,請查看官方文檔。

功能 #9:將對象映射到地圖

一個常見的用例是將屬性的值存儲在地圖中。 這經常發生在使用 RESTful API 並解析 JSON 對象的應用程序中。 在這種情況下,地圖實例可以用作委託屬性的委託。 官方文檔中的一個例子:

 class User(val map: Map<String, Any?>) { val name: String by map val age: Int by map }

在這個例子中, User有一個帶地圖的主構造函數。 這兩個屬性將從映射中獲取與屬性名稱相同的鍵映射的值:

 val user = User(mapOf( "name" to "John Doe", "age" to 25 ))

新用戶實例的 name 屬性將被賦值為“John Doe”,age 屬性值為 25。

這也適用於結合MutableMap的 var 屬性:

 class MutableUser(val map: MutableMap<String, Any?>) { var name: String by map var age: Int by map }

功能 #10:集合和功能操作

借助 Kotlin 中對 lambdas 的支持,可以將集合利用到一個新的水平。

首先,Kotlin 區分了可變集合和不可變集合。 例如, Iterable接口有兩個版本:

  • 可迭代
  • 可變可迭代

CollectionListSetMap接口也是如此。

例如,如果至少一個元素與給定的謂詞匹配,則any操作都會返回true

 val list = listOf(1, 2, 3, 4, 5, 6) assertTrue(list.any { it % 2 == 0 })

有關可以對集合執行的功能操作的詳細列表,請查看此博客文章。

結論

我們剛剛觸及了 Kotlin 所提供功能的皮毛。 對於那些有興趣進一步閱讀和了解更多信息的人,請查看:

  • Antonio Leiva 的 Kotlin 博客文章和書籍。
  • 來自 JetBrains 的官方文檔和教程。

總而言之,Kotlin 通過使用直觀和簡潔的語法為您提供了在編寫原生 Android 應用程序時節省時間的能力。 它仍然是一種年輕的編程語言,但在我看來,它現在足夠穩定,可以用於構建生產應用程序。

使用 Kotlin 的好處:

  • Android Studio 的支持是無縫且出色的。
  • 將現有的 Java 項目轉換為 Kotlin 很容易。
  • Java 和 Kotlin 代碼可以共存於同一個項目中。
  • 應用程序中沒有速度開銷。

缺點:

  • Kotlin 會將其庫添加到生成的.apk中,因此最終的.apk大小將大約大 300KB。
  • 如果濫用,運算符重載會導致代碼不可讀。
  • 使用 Kotlin 時,IDE 和自動完成的行為比使用純 Java Android 項目時要慢一些。
  • 編譯時間可能會更長一些。