对变量进行延迟初始化
在 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
语句少了对新的子类的条件处理,从而避免了运行时错误。