本主题重点介绍在进行 Android 开发时最有用的一些 Kotlin 语言特性。
使用 Fragment
以下各节使用 Fragment 示例来突出显示 Kotlin 的一些最佳特性。
继承
在 Kotlin 中,您可以使用 class 关键字声明类。在下面的示例中,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)
}
}
当用户点击 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 进行修剪(trim),使其值不包含前导或尾随空格,可以使用 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 中进行的大多数调用都发生在 Fragment 附加到 Activity(Context 的子类)时。话虽如此,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,因此请务必尽快初始化您的属性。