本主題著重介紹在開發 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
,因此請務必盡快對屬性進行初始化調整。