大家好,今天是Kotlin Flow响应式编程三部曲的最后一篇。
其实回想一下我写这个Kotlin Flow三部曲的初衷,主要还是因为我自己想学这方面的知识。
虽然Kotlin我已经学了很多年了,但是对于Flow我却一直没怎么接触过。可能是因为工作当中一直用不上吧,我现在工作的主语言依然还是Java。
而我一直都是这个样子,写博客基本上不是为了谁而写的,大部分都只是因为我自己想学。但是学了不用很快又会忘记,所以经常就会通过文章的形式把它记录下来,算是助人又助己了。
而Kotlin Flow在可预见的时间里,我也上不太可能能在工作当中用得到,所以这个系列也就基本是属于我个人的学习笔记了。
今天的这一篇文章,我准备讲一讲StateFlow和SharedFlow的知识。内容和前面的两篇文章有一定的承接关系,所以如果你还没有看过前面两篇文章的话,建议先去参考 Kotlin Flow响应式编程,基础知识入门 和 Kotlin Flow响应式编程,操作符函数进阶 。
/ Flow的生命周期管理 /
首先,我们接着在 Kotlin Flow响应式编程,基础知识入门 这篇文章中编写的计时器例子来继续学习。
之前在编写这个例子的时候我有提到过,首要目的就是要让它能跑起来,以至于在一些细节方面的写法甚至都错误的。
那么今天我们就要来看一看,之前的计时器到底错在哪里了。
如果只是直观地从界面上看,好像一切都是可以正常工作的。但是,假如我们再添加一些日志来进行观察的话,问题就会浮出水面了。
那么我们在MainActivity中添加一些日志,如下所示:
class MainActivity : AppCompatActivity() {
private val mainViewModel by viewModels<MainViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView = findViewById<TextView>(R.id.text_view)
val button = findViewById<Button>(R.id.button)
button.setOnClickListener {
lifecycleScope.launch {
mainViewModel.timeFlow.collect { time ->
textView.text = time.toString()
Log.d("FlowTest", "Update time $time in UI.")
}
}
}
}
}
这里每当计时器更新一次的时候,我们同时打印一行日志来方便进行进行观察。
另外,MainViewModel中的代码这里我也贴上吧,虽然它是完全没有改动的:
class MainViewModel : ViewModel() {
val timeFlow = flow {
var time = 0
while (true) {
emit(time)
delay(1000)
time++
}
}
}
运行程序看一看效果:
一开始的时候,界面上计时器每更新一次,同时控制台也会打印一行日志,这还算是正常。
可接下来,当我们按下Home键回到桌面后,控制台的日志依然会持续打印。好家伙,这还得了?
这说明,即使我们的程序已经不在前台了,UI更新依然在持续进行当中。这是非常危险的事情,因为在非前台的情况下更新UI,某些场景下是会导致程序崩溃的。
也就是说,我们并没有很好地管理Flow的生命周期,它没有与Activity的生命周期同步,而是始终在接收着Flow上游发送过来的数据。
那这个问题要怎么解决呢?lifecycleScope除了launch函数可以用于启动一个协程之外,还有几个与Activity生命周期关联的launch函数可以使用。比如说,launchWhenStarted函数就是用于保证只有在Activity处于Started状态的情况下,协程中的代码才会执行。
那么我们用launchWhenStarted函数来改造一下上述代码:
class MainActivity : AppCompatActivity() {
private val mainViewModel by viewModels<MainViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textView = findViewById<TextView>(R.id.text_view)
val button = findViewById<Button>(R.id.button)
button.setOnClickListener {
lifecycleScope.launchWhenStarted {
mainViewModel.timeFlow.collect { time ->
textView.text = time.toString()
Log.d("FlowTest", "Update time $time in UI.")
}
}
}
}
}
变动就只有这一处,我们使用launchWhenStarted函数替换了之前的launch函数,其余部分都是保持不变的。
现在重新运行一下程序,效果如下图所示: