使用常见的 Kotlin 模式与 Android 配合

本主题重点介绍在为 Android 开发时 Kotlin 语言的一些最有用的方面。

使用片段

以下部分使用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)
}

可空性和初始化

在前面的示例中,被覆盖方法中的一些参数的类型后缀为问号?。这表示为这些参数传递的参数可以为空。请务必安全地处理其可空性

在 Kotlin 中,必须在声明对象时初始化对象的属性。这意味着当您获得类的实例时,您可以立即引用其任何可访问的属性。Fragment中的View对象,直到调用Fragment#onCreateView后才能膨胀,因此您需要一种方法来延迟View的属性初始化。

lateinit允许您延迟属性初始化。使用lateinit时,应尽快初始化您的属性。

以下示例演示了如何使用lateinitonViewCreated中分配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 会检索一个作用域为当前 FragmentViewModel

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

空值性

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 类有一些属性,包括一个名为 nameString 属性。Java 没有 Kotlin 的空值性规则,而是依赖于可选的 *空值性注解* 来明确声明是否可以赋值空值。

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

平台类型

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

String! 对 Kotlin 编译器没有任何特殊含义。String! 可以表示 StringString?,编译器允许您赋值任一类型的的值。请注意,如果您将类型表示为 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(包括 AppCompatActivityFragment)包含空值性注解,某些调用(如 Fragment#getContext)具有更适合 Kotlin 的替代方法。

例如,访问 FragmentContext 几乎总是非空的,因为您在 Fragment 中进行的大多数调用都在 Fragment 附加到 ActivityContext 的子类)时发生。也就是说,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异常,因此请务必尽快初始化您的属性。