本主题重点介绍在为 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
时,应尽快初始化您的属性。
以下示例演示了如何使用lateinit
在onViewCreated
中分配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
对象包含一个setOnClickListener()
函数,该函数接收OnClickListener
的实现。
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)
}
}
传递给setOnClickListener()
的匿名函数中的代码在用户点击loginButton
时执行。
伴生对象
伴生对象提供了一种机制来定义在概念上与类型相关联但未绑定到特定对象的变量或函数。伴生对象类似于在 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?
。
要指示变量永远不能为 null,请使用 @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
。如果其左侧表达式的结果为 null,则您的应用程序将抛出 NullPointerException
。此运算符快捷方便,但应谨慎使用,因为它可能会使 NullPointerException
再次出现在您的代码中。
更安全的选择是使用 *安全调用运算符* ?.
,如下例所示。
val account = Account("name", "type")
val accountName = account.name?.trim()
使用安全调用运算符,如果 name
非空,则 name?.trim()
的结果是不包含前导或尾随空格的 name 值。如果 name
为 null,则 name?.trim()
的结果为 null
。这意味着您的应用程序在执行此语句时永远不会抛出 NullPointerException
。
虽然安全调用运算符可以避免潜在的 NullPointerException
,但它确实会将空值传递给下一条语句。您可以改用 *Elvis 运算符* (?:
) 立即处理 null 案例,如下例所示。
val account = Account("name", "type")
val accountName = account.name?.trim() ?: "Default name"
如果 Elvis 运算符左侧表达式的结果为 null,则右侧的值将赋值给 accountName
。此技术对于提供否则为 null 的默认值很有用。
您还可以使用 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
中进行的大多数调用都在 Fragment
附加到 Activity
(Context
的子类)时发生。也就是说,Fragment#getContext
并不总是返回非空值,因为在某些情况下,Fragment
未附加到 Activity
。因此,Fragment#getContext
的返回类型是可空的。
由于从 Fragment#getContext
返回的 Context
是可空的(并用 @Nullable 注解),因此您必须在 Kotlin 代码中将其视为 Context?
。这意味着在访问其属性和函数之前,要应用前面提到的某个运算符来处理空值性。对于某些此类情况,Android 包含提供此便利性的替代 API。例如,Fragment#requireContext
返回一个非空 Context
,如果在 Context
为 null 时调用,则会抛出 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
异常,因此请务必尽快初始化您的属性。