对变量进行延迟初始化
在 Kotlin 中,变量不可为空的特性增加了程序的空安全性,但有时也会带来一些麻烦。比如,当一个类中的某些成员变量需要在该类实例化之后才能被初始化时,我们只好将这些成员变量声明为可空类型。
并且在后续使用该变量时,你必须添加上非空判断保护才行(如 ?. 空安全调用或 !!. 非空断言),即使你能确定该变量在使用前已被初始化,在使用时不为空。
以之前的 UIBestPractice 项目为例,我们来到 MainActivity 中,你会发现对 binding 的访问密密麻麻的都是 ?.。
kotlin
class MainActivity : AppCompatActivity() {
...
private var binding: ActivityMainBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 初始化视图绑定对象
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding?.root)
initMsg()
binding?.toolbar?.let { nonNullToolbar ->
setSupportActionBar(nonNullToolbar)
supportActionBar?.title = "UIBestPractice"
}
val layoutManager = LinearLayoutManager(this)
binding?.recyclerView?.layoutManager = layoutManager
adapter = MsgAdapter(msgList)
binding?.recyclerView?.adapter = adapter
binding?.send?.setOnClickListener {
val content = binding?.inputText?.text.toString()
if (content.isNotEmpty()) {
val msg = Msg(content, Msg.TYPE_SENT)
msgList.add(msg)
adapter.notifyItemInserted(msgList.size - 1)
binding?.recyclerView?.scrollToPosition(msgList.size - 1)
binding?.inputText?.setText("")
}
}
}
...
}
可以看到 binding 是一个成员变量,它的初始化工作发生在 onCreate() 方法中,不发生在 MainActivity 构造时(构造函数或 init 块中)。所以我们不得不将它的类型声明为 ActivityMainBinding?,并且同时赋值为 null。
这样做之后,那后续访问 binding 时,binding 必定不为空。但由于其类型是可为空的,所以我们必须进行判空处理,否则无法通过编译,这就使得代码中充满了 ?.。
当类中加入了更多这类的成员变量实例,这个问题会愈发的明显。但好在我们是有解决方法的,就是利用 Kotlin 提供的延迟初始化。
延迟初始化使用 lateinit 关键字,它告诉编译器:我们等会对这个非空类型的变量进行初始化,并且是使用之前进行初始化,所以你也不用提示我,声明该变量时没有被初始化了。
我们来对当前的代码进行优化,结果如下所示:
kotlin
class MainActivity : AppCompatActivity() {
...
private lateinit var binding: ActivityMainBinding // 使用 lateinit 声明
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
initMsg()
binding.toolbar.let { nonNullToolbar ->
setSupportActionBar(nonNullToolbar)
supportActionBar?.title = "UIBestPractice"
}
val layoutManager = LinearLayoutManager(this)
binding.recyclerView.layoutManager = layoutManager
adapter = MsgAdapter(msgList)
binding.recyclerView.adapter = adapter
binding.send.setOnClickListener {
val content = binding.inputText.text.toString()
if (content.isNotEmpty()) {
val msg = Msg(content, Msg.TYPE_SENT)
msgList.add(msg)
adapter.notifyItemInserted(msgList.size - 1)
binding.recyclerView.scrollToPosition(msgList.size - 1)
binding.inputText.setText("")
}
}
}
...
}
我们在声明 binding 变量时,使用了 lateinit var 来声明。这样,我们就可以将其类型声明为非空的 ActivityMainBinding 了,并且不用在一开始就给它赋值为 null。这样在后续使用 binding 变量时,可以直接通过 . 来访问它,而无需使用 ?. 操作符进行判空处理,代码是不是看起来清爽多了?
但如果你在 binding 初始化之前就访问了它,程序在运行时会抛出异常:kotlin.UninitializedPropertyAccessException: lateinit property binding has not been initialized
所以当你声明了一个延迟初始化的成员变量时,一定要确保在使用该全局变量之前,它就已经被初始化了。
另外,你可以通过 ::propertyName.isInitialized 来判断某个延迟初始化变量是否已经被初始化了,可以避免重复的初始化工作。虽然我们这里不需要这个操作,但还是看看:
kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!::binding.isInitialized) {
binding = ActivityMainBinding.inflate(layoutInflater)
}
setContentView(binding.root)
...
}
使用密封类优化代码
密封类是 Kotlin 中一个特性,它用于表示受限制的类继承结构。当你希望一个类的所有直接子类可知,并且不希望在别的地方有新的子类继承这个类,你就可以使用密封类。 它常常可以和 RecyclerView 适配器中的 ViewHolder 或者表示状态的类(如网络请求结果)一起使用,以提高代码的类型安全性。
我们来看看它的作用。假设我们需要表示某个操作的结果,可以创建一个 Result.kt 文件,代码如下:
kotlin
interface Result
class Success(val msg: String) : Result
class Failure(val error: Exception) : Result
上述代码中,我们首先定义了一个 Result 接口,用于表示某次操作的结果。然后定义两个类去实现它,一个表示成功的结果,另一个表示失败的结果。
我们再来定义一个 getResultMsg() 方法,用于获取结果的信息,代码如下:
kotlin
fun getResultMsg(result: Result) = when (result) {
is Success -> result.msg
is Failure -> result.error.message
else -> throw IllegalArgumentException("Unknown Result type encountered")
}
方法会接收一个 Result 对象,然后通过 when 语句判断它的具体类型,并返回对应的消息。但我们不得不加上 else 语句,否则会因为缺少条件分支,导致编译不通过,因为编译器不知道 when 语句是否覆盖了所有可能的情况。而实际上,这个 else 条件是永远无法满足的,所以我们在这个抛了一个异常,只是为了满足语法规则。
更麻烦的是,如果我们新增了一个 Result 接口的实现类,比如 Unknown(表示未知结果),而忘了去 getResultMsg() 方法的 when 语句中对新类型添加条件判断分支。这样会导致当我们传入了 Unknown 对象时,就会来到 else 分支,抛出异常导致程序崩溃。这种错误在编译时是无法被发现的。
不过,Kotlin 的密封类可以解决以上问题。
密封类的关键字是 sealed class。使用的话,很简单,以前面的 Result 接口为例:
kotlin
sealed class Result // 声明为 sealed class
class Success(val msg: String) : Result()
class Failure(val error: Exception) : Result()
fun getResultMsg(result: Result) = when (result) {
is Success -> result.msg
is Failure -> result.error.message
}
可以看到 getResultMsg 方法的 when 语句中,不再需要 else 条件分支了。
那为什么去掉 else 条件分支也能编译通过呢? 因为当 when 语句的判断对象是一个密封类对象时,Kotlin 编译器会自动检查该密封类的所有直接子类,并强制 要求你将每一个已知的直接子类都作为一个分支条件进行处理,这样就可以保证:即使没有 else 条件判断,也不会漏写分支条件。
现在,我们再新增一个 Unknown 类,继承自 Result 密封类,你会发现 when 语句报错了,报错信息为:'when' expression must be exhaustive, add necessary 'is Unknown' branch or 'else' branch instead,这个编译时错误正是密封类的强大之处,让我们不会遗漏掉已知情况。
kotlin
sealed class Result
class Success(val msg: String) : Result()
class Failure(val error: Exception) : Result()
class Unknown(val info: String? = null) : Result()
所以我们为了程序能够运行,只能在 when 语句中添加上处理 Unknown 类型的条件分支:
kotlin
fun getResultMsg(result: Result) = when (result) {
is Success -> result.msg
is Failure -> "Error is ${result.error.message}"
is Unknown -> "Unknown Result,${result.info}"
}
另外,密封类及其所有子类只能定义在同一个包和同一个模块,这是被密封类底层的实现机制所限制的,这样才能让 Kotlin 编译器知道一个密封类的所有直接子类。
了解完密封类的作用后,我们来看看它是如何与 RecyclerView 适配器中的 ViewHolder 一起使用的,以优化多视图类型的处理。
假设我们的 MsgAdapter 需要展示两种不同的消息,分别是收到的消息和发送的消息。我们会定义两个不同的 ViewHolder。
来到 MsgAdapter 中,代码如下:
kotlin
class MsgAdapter(private val msgList: List<Msg>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
inner class LeftViewHolder(private val msgLeftItemBinding: MsgLeftItemBinding) :
RecyclerView.ViewHolder(msgLeftItemBinding.root) {
fun bind(msg: Msg) {
msgLeftItemBinding.leftMsg.text = msg.content
}
}
inner class RightViewHolder(private val msgRightItemBinding: MsgRightItemBinding) :
RecyclerView.ViewHolder(msgRightItemBinding.root) {
fun bind(msg: Msg) {
msgRightItemBinding.rightMsg.text = msg.content
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val msg = msgList[position]
when (holder) {
is LeftViewHolder -> holder.bind(msg)
is RightViewHolder -> holder.bind(msg)
else -> throw IllegalArgumentException()
}
}
...
}
可以看到,onBindViewHolder 方法的 when 语句中的 else 条件分支就是没必要的,只是抛出了一个异常,现在我们借助密封类来改造一下。
新建一个 MsgViewHolder.kt 文件,代码如下:
kotlin
sealed class MsgViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
class LeftViewHolder(private val msgLeftItemBinding: MsgLeftItemBinding) :
MsgViewHolder(msgLeftItemBinding.root) {
fun bind(msg: Msg) {
msgLeftItemBinding.leftMsg.text = msg.content
}
}
class RightViewHolder(private val msgRightItemBinding: MsgRightItemBinding) :
MsgViewHolder(msgRightItemBinding.root) {
fun bind(msg: Msg) {
msgRightItemBinding.rightMsg.text = msg.content
}
}
我们定义了一个密封类 MsgViewHolder,让它继承自 RecyclerView.ViewHolder,然后让 LeftViewHolder 和 RightViewHolder 继承自 MsgViewHolder。
然后再来修改 MsgAdapter 适配器,代码如下:
kotlin
class MsgAdapter(private val msgList: List<Msg>) : RecyclerView.Adapter<MsgViewHolder>() {
...
override fun onBindViewHolder(holder: MsgViewHolder, position: Int) {
val msg = msgList[position]
when (holder) {
is LeftViewHolder -> holder.bind(msg)
is RightViewHolder -> holder.bind(msg)
}
}
...
}
我们将 RecyclerView.Adapter 的泛型指定为了刚刚定义的 MsgViewHolder 密封类,这样,在重写 onBindViewHolder 方法时,其 holder 参数的类型就可以变为 MsgViewHolder,然后因为 MsgViewHolder 是一个密封类,所以我们就可以把 else 分支删除了。并且之后新增了 MsgViewHolder 的子类,编译器会提醒我们 when 语句少了对新的子类的条件处理,从而避免了运行时错误。