文章目录
在 Android 开发中使用常见的 Kotlin 模式
介绍 Kotlin 语言在 Android 开发过程中最有用的一些方面
使用 Fragment
继承
LoginFragment
是 Fragment
的子类,在子类与其父类之间使用 :
运算符指示继承:
class 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 性和初始化
在前面的示例中,被替换的方法中某些参数的类型以问号 ?
为后缀。这表示为这些参数传递的实际参数可以为 null。请务必安全地处理其可为 null 性
在 Kotlin 中,您必须在声明对象时初始化对象的属性。这意味着,当您获取类的实例时,可以立即引用它的任何可访问属性。不过,在调用 Fragment#onCreateView
之前,Fragment
中的 View
对象尚未准备好进行扩充,所以您需要一种方法来推迟 View
的属性初始化
可以使用 lateinit
推迟属性初始化。使用 lateinit
时,您应尽快初始化属性
如何使用 lateinit
在 onViewCreated
中分配 View
对象:
private lateinit var usernameEditText: EditText
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
usernameEditText = view.findViewById(R.id.username_edit_text)
}
注意:如果您在初始化属性之前对其进行访问,Kotlin 会抛出 UninitializedPropertyAccessException
SAM转换(Single Abstract Method)
Button
对象包含一个 setOnClickListener()
函数,该函数接受 OnClickListener
的实现
OnClickListener
具有单一抽象方法 onClick()
,您必须实现该方法。因为 setOnClickListener()
始终将 OnClickListener
当作参数,又因为 OnClickListener
始终都有相同的单一抽象方法,所以此实现在 Kotlin 中可以使用匿名函数来表示。此过程称为单一抽象方法转换或 SAM 转换,SAM 转换可使代码明显变得更简洁。以下示例展示了如何使用 SAM 转换来为 Button
实现 OnClickListener
:
binding.loginButton.setOnClickListener {
Toast.makeText(activity, "click me", Toast.LENGTH_LONG).show()
}
当用户点击 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
属性委托使用反射,这样会增加一些性能开销。这种代价换来的是简洁的语法,可让您节省开发时间
可为 null 性
Kotlin 提供了严格的可为 null 性规则,可在您的整个应用中维护类型安全。在 Kotlin 中,默认情况下,对对象的引用不能包含 null 值。如需为变量赋 null 值,必须通过将 ?
添加到基本类型的末尾以声明可为 null 变量类型
name
的类型为 String
,不可为 null:
val name: String = null
如需允许 null 值,必须使用可为 null String
类型 String?
,如以下示例所示:
val name: String? = null
互操作性
平台类型
如果您使用 Kotlin 引用在 Java Account
类中定义的不带注解的 name
成员,编译器将不知道 String
映射到 Kotlin 中的 String
还是 String?
。这种不明确性通过平台类型 String!
表示。
String!
对 Kotlin 编译器而言没有特殊的含义。String!
可以表示 String
或 String?
,编译器可让您赋予任一类型的值。请注意,如果您将类型表示为 String
并赋予 null 值,则系统可能会抛出 NullPointerException
。
为了解决此问题,每当您用 Java 编写代码时,都应使用可为 null 性注解。这些注解对 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 中被视为不可为 null String
。
可为 null 性注解包含在所有新增的 Android API 以及许多现有的 Android API 中。许多 Java 库已添加可为 null 性注解,以便为 Kotlin 和 Java 开发者提供更好的支持
处理可为 null 性
如果您不确定 Java 类型,则应将其视为可为 null。例如,Account
类的 name
成员不带注解,因此您应假定它是一个可为 null 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 变更
由于从 Fragment#getContext
返回的 Context
可为 null(并且带有 @Nullable 注释),因此您必须在 Kotlin 代码中将其视为 Context?
。这意味着,在访问其属性和函数之前,需要应用前面提到的某个运算符来处理可为 null 性问题。对于一些这样的情况,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
}
}
上面的示例中,在构建 LoginFragment
时初始化 index
不过,某些属性可能无法在对象构建期间进行初始化。例如,您可能要从 Fragment
中引用 View
,这意味着,必须先扩充布局。构建 Fragment
时不会发生扩充,而是在调用 Fragment#onCreateView
时进行扩充。
应对这种情况的一种方法是将视图声明为可为 null 并尽快对其进行初始化,如以下示例所示:
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)
}
}
当然可以使用lateinit关键字,可以避免在构建对象时初始化属性。
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)
}
}
如果在属性进行初始化之前对其进行了引用,Kotlin 会抛出 UninitializedPropertyAccessException
,因此请务必尽快初始化属性
注意:诸如数据绑定之类的视图绑定解决方案可以消除对 findViewById
的手动调用,这些解决方案有助于减少您需要考虑的 null 安全问题数量