在 Android 上使用常见的 Kotlin 模式

本主题重点介绍 Kotlin 语言在 Android 开发中最有用的几个方面。

使用 Fragment

以下各部分使用 Fragment 示例来重点介绍 Kotlin 的一些最佳功能。

继承

您可以使用 class 关键字在 Kotlin 中声明类。在以下示例中,LoginFragmentFragment 的子类。您可以使用 : 运算符在子类及其父类之间表示继承

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)
}

可空性和初始化

在前面的示例中,替换方法中的某些参数类型带有问号 ? 后缀。这表示为这些参数传递的参数可以为 null。请务必安全地处理其可空性

在 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 对象包含一个 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 会检索一个作用域限定为当前 FragmentViewModel

属性委托使用反射,这会增加一些性能开销。其权衡是简洁的语法,可节省开发时间。

可空性

Kotlin 提供了严格的可空性规则,可在整个应用中维护类型安全。在 Kotlin 中,对象引用默认不能包含 null 值。要将 null 值赋给变量,您必须通过在基本类型末尾添加 ? 来声明可空变量类型。

例如,以下表达式在 Kotlin 中是非法的。name 的类型为 String,且不可为空

val name: String = null

要允许 null 值,您必须使用可空的 String 类型 String?,如以下示例所示

val name: String? = null

互操作性

Kotlin 严格的规则使您的代码更安全、更简洁。这些规则降低了出现导致应用崩溃的 NullPointerException 的几率。此外,它们还减少了您在代码中需要进行的 null 检查次数。

通常,在编写 Android 应用时,您还必须调用非 Kotlin 代码,因为大多数 Android API 都是用 Java 编程语言编写的。

可空性是 Java 和 Kotlin 在行为上存在差异的关键领域。Java 对可空性语法的限制较少。

例如,Account 类有几个属性,包括一个名为 nameString 属性。Java 没有 Kotlin 关于可空性的规则,而是依靠可选的可空性注解来明确声明是否可以分配 null 值。

由于 Android 框架主要用 Java 编写,因此在调用没有可空性注解的 API 时,您可能会遇到这种情况。

平台类型

如果您使用 Kotlin 引用 Java Account 类中定义的未注解的 name 成员,编译器不知道该 String 在 Kotlin 中是映射到 String 还是 String?。这种歧义通过平台类型 String! 来表示。

String! 对 Kotlin 编译器没有特殊含义。String! 可以表示 StringString?,编译器允许您分配任一类型的值。请注意,如果您将类型表示为 String 并分配 null 值,则可能会抛出 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 注解,表明它可以保存 null 值。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?。其中一种方法是使用非 null 断言运算符 !!,如以下示例所示

val account = Account("name", "type")
val accountName = account.name!!.trim()

!! 运算符将其左侧的所有内容都视为非 null,因此在这种情况下,您将 name 视为非 null String。如果其左侧表达式的结果为 null,则您的应用将抛出 NullPointerException。此运算符快速简便,但应谨慎使用,因为它会重新将 NullPointerException 实例引入您的代码。

一个更安全的选择是使用安全调用运算符 ?.,如以下示例所示

val account = Account("name", "type")
val accountName = account.name?.trim()

使用安全调用运算符,如果 name 非 null,则 name?.trim() 的结果是没有前导或尾随空格的名称值。如果 name 为 null,则 name?.trim() 的结果为 null。这意味着您的应用在执行此语句时永远不会抛出 NullPointerException

虽然安全调用运算符可以避免潜在的 NullPointerException,但它会将 null 值传递给下一个语句。您可以通过使用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,包括 AppCompatActivityFragment,都包含可空性注解,并且某些调用(如 Fragment#getContext)具有更适合 Kotlin 的替代方案。

例如,访问 FragmentContext 几乎总是非 null,因为您在 Fragment 中进行的大多数调用都发生在 Fragment 附加到 ActivityContext 的子类)时。话虽如此,Fragment#getContext 并不总是返回非 null 值,因为在某些情况下 Fragment 未附加到 Activity。因此,Fragment#getContext 的返回类型是可空的。

由于从 Fragment#getContext 返回的 Context 是可空的(并且被注解为 @Nullable),因此您必须在 Kotlin 代码中将其视为 Context?。这意味着在访问其属性和函数之前,必须应用前面提到的运算符之一来处理可空性。对于其中一些场景,Android 包含提供此便利的替代 API。例如,Fragment#requireContext 返回一个非 null 的 Context,并且如果在 Context 为 null 时调用,则会抛出 IllegalStateException。这样,您就可以将结果 Context 视为非 null,而无需安全调用运算符或变通方法。

属性初始化

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,因此请务必尽快初始化您的属性。