本主題著重介紹在開發 Android 時 Kotlin 語言最實用的部分。
使用片段
下列各節使用 Fragment 範例來突出 Kotlin 的部分最佳功能。
繼承
您可以使用 class 關鍵字在 Kotlin 中宣告類別。在以下範例中,LoginFragment 是 Fragment 的子類別。您可以在子類別和其父項之間使用 : 運算子,來指出繼承:
class LoginFragment : Fragment()
在這個類別宣告中,LoginFragment 負責呼叫其父類別 Fragment 的建構函式。
在 LoginFragment 中,您可以覆寫許多生命週期回呼,以回應 Fragment 中的狀態變更。如要覆寫函式,請使用 override 關鍵字,正如以下範例所示:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.login_fragment, container, false)
}
如要參照父項類別中的函式,請使用 super 關鍵字,正如以下範例所示:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
是否可為空值和初始化
在前例中,已覆寫的方法中部分參數的類型附加了問號 ?。這表示傳遞給這些參數的引數可以是空值。請務必安全處理是否可為空值特性。
在 Kotlin 中,您必須在宣告物件時對物件的屬性進行初始化調整。這表示當您取得類別的例項時,您可以立即參照其任何一項可存取的屬性。不過,Fragment 中的 View 物件在呼叫 Fragment#onCreateView 之前尚未加載完畢,因此您需要為 View 把屬性初始化延後的方法。
lateinit 可把屬性初始化延後。使用 lateinit 時,應盡快對屬性進行初始化調整。
以下範例說明如何在 onViewCreated 中使用 lateinit 來指派 View 物件:
class LoginFragment : Fragment() {
private lateinit var usernameEditText: EditText
private lateinit var passwordEditText: EditText
private lateinit var loginButton: Button
private lateinit var statusTextView: TextView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
usernameEditText = view.findViewById(R.id.username_edit_text)
passwordEditText = view.findViewById(R.id.password_edit_text)
loginButton = view.findViewById(R.id.login_button)
statusTextView = view.findViewById(R.id.status_text_view)
}
...
}
SAM 轉換
您可以實作 OnClickListener 介面,來監聽 Android 中的點擊事件。Button 物件包含實作 OnClickListener 的 setOnClickListener() 函式。
OnClickListener 設有單抽象方法 onClick()。您必須加以實作該方法。由於 setOnClickListener() 一律使用 OnClickListener 做為引數,且 OnClickListener 一律會使用相同的單抽象方法,所以在實作這個方法時,可以在 Kotlin 中以匿名函式表示。這項程序稱為單抽象方法轉換或 SAM 轉換。
SAM 轉換可以讓程式碼看起來清晰得多。以下範例說明如何使用 SAM 轉換來為 Button 實作 OnClickListener:
loginButton.setOnClickListener {
val authSuccessful: Boolean = viewModel.authenticate(
usernameEditText.text.toString(),
passwordEditText.text.toString()
)
if (authSuccessful) {
// Navigate to next screen
} else {
statusTextView.text = requireContext().getString(R.string.auth_failed)
}
}
使用者點選 loginButton 時,傳送至 setOnClickListener() 的匿名函式中的程式碼就會執行。
伴生物件
伴生物件提供用於定義變數或函式的機制,而這些變數或函式的概念會與類型連結,但不會與特定物件相關聯。伴生物件類似於對變數和方法使用 Java 的 static 關鍵字。
在以下範例中,TAG 是 String 常數。您不需要為 LoginFragment 的每個執行個體指定 String 的唯一執行個體,因此您應該在夥伴物件中定義該執行個體:
class LoginFragment : Fragment() {
...
companion object {
private const val TAG = "LoginFragment"
}
}
您可在檔案頂層定義 TAG,但這個檔案也可能包含許多在頂層定義的變數、函式和類別。夥伴物件可協助連結變數、函式和類別定義,而不會參照該類別的任何執行個體。
屬性委派
對屬性進行初始化調整時,您可以重複使用一些在 Android 中較常見的模式,例如在 Fragment 中存取 ViewModel。為避免產生重複的程式碼,您可以使用 Kotlin 的「屬性委派」語法。
private val viewModel: LoginViewModel by viewModels()
屬性委派是常見的實作做法,可在整個應用程式中重複使用。Android KTX 為您提供了一些屬性委派。例如,viewModels 會擷取範圍限定在目前 Fragment 的 ViewModel。
屬性委派使用反射,所以會產生一些效能負擔。這樣的好處是獲得簡潔的語法,節省開發時間。
是否可為空值
Kotlin 採用嚴格的是否可為空值規則,以在整個應用程式內維持類型的安全。在 Kotlin 中,根據預設,物件的參照不可包含空值。如要將空值指派給變數,您必須在基本類型的末端加上 ?,以宣告「可為空值」變數類型。
例如,以下運算式在 Kotlin 中無效。name 是 String 類型,且不可為空值:
val name: String = null
如要允許空值,您必須使用可為空值的 String 類型 String?,如以下範例所示:
val name: String? = null
互通性
Kotlin 採用的嚴格規則能使程式碼更安全、更簡潔。這些規則降低了讓 NullPointerException 導致應用程式當機的機率。此外,這些規則也會減少需在程式碼中執行的空值檢查次數。
通常,在編寫 Android 應用程式時,您也必須呼叫非 Kotlin 程式碼,因為大部分 Android API 皆以 Java 程式設計語言編寫。
是否可為空值是體現 Java 和 Kotlin 行為差別的主要方面。Java 採用較寬鬆的是否可為空值語法。
例如,Account 類別包含幾個屬性,包括稱為 name 的 String 屬性。Java 沒有採用 Kotlin 的是否可為空值規則,而是依賴可選的「是否可為空值註解」,來明確宣告您是否可以指派空值。
由於 Android 架構主要以 Java 編寫,所以如果您呼叫的 API 不含是否可為空值註解,就可能會遇到這種情況。
平台類型
如果您使用 Kotlin 來參照在 Java Account 類別中定義的未加註 name 成員,編譯器不會知道 String 對應至 Kotlin 中的 String 或 String?。這種不確定性是透過「平台類型」String! 表示。
String! 對 Kotlin 編譯器沒有特殊意義。String! 可以表示 String 或 String?,而編譯器可讓您指派任一類型的值。請注意,如果您將類型表示為 String 並指派空值,系統很可能會擲回 NullPointerException。
如要解決這個問題,建議您在 Java 中編寫程式碼時,使用是否可為空值註解。這些註解對 Java 和 Kotlin 開發人員都有用。
例如,以下是在 Java 中定義的 Account 類別:
public class Account implements Parcelable {
public final String name;
public final String type;
private final @Nullable String accessId;
...
}
其中一個成員變數 accessId 以 @Nullable 加註,表示其可保留空值。這樣,Kotlin 就會將 accessId 視為 String?。
如要表示變數不可為空值,請使用 @NonNull 註解:
public class Account implements Parcelable {
public final @NonNull String name;
...
}
在這種情況下,name 會在 Kotlin 中視為非空值的 String。
所有新 Android API 及許多現有 Android API 都包含是否可為空值註解。許多 Java 程式庫新增了是否可為空值註解,讓 Kotlin 和 Java 開發人員能夠獲得更好的支援。
處理是否可為空值
如果您不能確定 Java 類型,則應將該類型判定為可為空值。例如,Account 類別的 name 成員並未加註,因此請將其視為可為空值的 String
如果您想剪輯 name,讓其值不包含開頭或結尾的空白字元,您可以使用 Kotlin 的 trim 函式。您可以透過幾種方式安全地剪輯 String?。其中一種方式是使用「非空值斷言運算子」!!,如以下範例所示:
val account = Account("name", "type")
val accountName = account.name!!.trim()
!! 運算子會將左側的所有資訊視為非空值,因此在這種情況下,您會將 name 視為非空值的 String。如果運算子左側的運算式結果為空值,應用程式就會擲回 NullPointerException。這個運算子快速又簡單,但請謹慎使用,因為它可將 NullPointerException 的執行個體重新導入您的程式碼內。
使用「安全呼叫運算子」 ?. 則更安全,如以下範例所示:
val account = Account("name", "type")
val accountName = account.name?.trim()
使用安全呼叫運算子時,如果 name 不是空值,則 name?.trim() 的結果是名稱值,開頭或結尾不含空白字元。如果 name 為空值,則 name?.trim() 的結果是 null。這意味著,應用程式在執行這個陳述式時,絕對不會擲回 NullPointerException。
安全呼叫運算子雖避免了 NullPointerException,但會將空值傳送至下一個陳述式。您可以使用「Elvis 運算子」(?:) 立即處理空值的案件,如以下範例所示:
val account = Account("name", "type")
val accountName = account.name?.trim() ?: "Default name"
如果 Elvis 運算子左側的運算式結果為空值,則右側的值會指派給 accountName。這個技巧對提供本為空值的預設值很有用。
您也可以使用 Elvis 運算子提前從函式傳回結果,如以下範例所示:
fun validateAccount(account: Account?) {
val accountName = account?.name?.trim() ?: "Default name"
// account cannot be null beyond this point
account ?: return
...
}
Android API 變更
Android API 越來越支援 Kotlin。許多 Android 最常見的 API (包括 AppCompatActivity 和 Fragment) 都含有是否可為空值註解,而某些呼叫 (例如 Fragment#getContext) 則有更多支援 Kotlin 的替代方案。
例如,存取 Fragment 的 Context 幾乎都是非空值,因為您是在 Fragment 附加至 Activity (Context 的子類別) 時,在 Fragment 中發出大部分呼叫。話雖如此,Fragment#getContext 不一定會傳回非空值,因為在特定情境中,Fragment 並沒有附加至 Activity。因此,Fragment#getContext 的傳回類型可為空值。
由於從 Fragment#getContext 傳回的 Context 可為空值 (且以 @Nullable 加註),所以您必須在 Kotlin 程式碼中將其視為 Context?。這意味著,在存取屬性和函式之前,請先套用上述一個運算子來處理是否可為空值問題。針對其中部分情境,Android 包含提供這種便利的替代性 API。例如,Fragment#requireContext 會傳回非空值的 Context,且在 Context 為空值時,如果發出呼叫,則會擲回 IllegalStateException。這樣一來,您就能將產生的 Context 視為非空值,而不需要使用安全呼叫運算子或變通方案。
屬性初始化
根據預設,系統尚未對 Kotlin 中的屬性進行初始化調整。在對這些屬性的封閉式類別進行初始化調整時,也須對屬性進行初始化調整。
您可以透過幾種方式對屬性進行初始化調整。以下範例說明如何在類別宣告中指派值來對 index 變數進行初始化調整:
class LoginFragment : Fragment() {
val index: Int = 12
}
您也可以在初始化器區塊中定義這個初始化作業:
class LoginFragment : Fragment() {
val index: Int
init {
index = 12
}
}
在以上範例中,index 已在建構 LoginFragment 時完成初始化調整。
不過,部分屬性可能無法在建構物件時完成初始化調整。例如,您可能想要在 Fragment 中參照 View,意味著必須先加載版面配置。在建構 Fragment 時,版面配置尚未加載,而是在呼叫 Fragment#onCreateView 時加載。
解決這個問題的一種方式,就是將檢視畫面宣告為可為空值,並盡快完成初始化調整,如以下範例所示:
class LoginFragment : Fragment() {
private var statusTextView: TextView? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
statusTextView = view.findViewById(R.id.status_text_view)
statusTextView?.setText(R.string.auth_failed)
}
}
雖然達到了預期效果,但您在參照 View 的是否可為空值特性時,必須管理這種特性。更好的做法是,使用 lateinit 來完成 View 的初始化調整,如以下範例所示:
class LoginFragment : Fragment() {
private lateinit var statusTextView: TextView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
statusTextView = view.findViewById(R.id.status_text_view)
statusTextView.setText(R.string.auth_failed)
}
}
在建構物件時,lateinit 關鍵字可避免對屬性進行初始化調整。如果在初始化之前參照屬性,Kotlin 將擲回 UninitializedPropertyAccessException,因此請務必盡快對屬性進行初始化調整。