一个故事说起
从前山上有个和尚,需要下山找水喝,于是他提上水桶就下山了,然后他找到了一处水源,于是就在这里打水提上山喝。但是好景不长,一段时间后这处水源枯竭了,他不得不另外找一处水源。这样重复几次后,他发现似乎有更好的办法,他希望去建立一个管道,连接所有的水源,并在每个水源处,找了通知人,当一处水源有水的时候就会通知这位和尚,这样他不用盲目的寻找水源了.
虽然用了很大的劲搭建了这套基础设施,但是这位和尚认为一切都是值得的,这样他轻松了很多.
再来谈谈Flow
上面这个故事的例子在代码中就叫响应式模式,在代码里我们也希望建立一个这样模式,对我们的View进行更新,这样会方便很多
Kotlin中Flow便提供这样的功能.
在Flow中它的基本流程是
生产者:
生产添加到流中的数据。多亏了协程,流程还可以异步生成数据。
加工者:
可以修改发射到流中的每个值或流本身
消费者
使用流中的值。
Flow的创建(生产者)
首先我们来看看,使用Flow的第一步,就是创建,也是我们上面描述的第一个环节生产.
很多jetpack库都已经支持了Flow的功能. 使用起来也很方便
例如,数据库Room就支持创建Flow风格的查询方式
@Dao
interface CodelabsDao {
@Query("SELECT * FROM codelabs")
fun getAllCodelabs(): Flow<List<Codelab>>
}
每次数据库有更新都会触发Flow的生产.
自己创建Flow也是可以的。kotlin提供flow方法可以直接创建一个flow.
class UserMessagesDataSource(private val messagesApi: MesssageApi, private val refreshIntervalMs: Long = 5000) {
val latestMessages : Flow<List<Message>> = flow {
while(true) {
val userMessages = messagesApi.fetchLatestMessages()
emit(userMessages)
delay(refreshIntervalMs)
}
}
}
Flow的加工(加工者)
通常我们创建一个Flow后,生产的数据可能并不是最终符合展示到UI上的数据结构,这个时候我们需要稍微做下加工,kotlin本身提供了一些加工的方法,例如map,filter等.
val importantUserMessages: Flow<MessagesUiModel> = userMessagesDataSource.latestMessages
//加工者操作符,转换成适合ui显示的数据结构
.map {
userMessages -> userMessages.toUiModel()
}
//加工者操作符,过滤出包含重要通知的消息
.filter { messagesUiModel ->
messagesUiModel.containsImportantNotifications()
}
catch异常也非常简单,我们可以直接加一个catch在函数链上
val importantUserMessages: Flow<MessagesUiModel> = userMessagesDataSource.latestMessages
.map {
userMessages -> userMessages.toUiModel()
}
.filter { messagesUiModel ->
messagesUiModel.containsImportantNotifications()
}
//异常捕获
.catch {e ->
log("Error loading reserved event")
}
Flow的消费(消费者)
通过了生产,加工后,这个时候我们就可以开始消费了,使用collect方法进行消费操作, 通常的消费操作都是在UI上面进行的
userMessages.colloct {
messages -> listAdapter.submiList(messages)
}
这里colloct是在一个协程中执行的. 注意这里Flow我们称为冷流,它是在有消费的情况才会生产,这意味着我们调用colloct才会进行前面的生产,加工方法.
Flow的优化
我们刚开始提到的一个和尚打水的故事,实际上在和尚睡觉的时候他并不关心山下水源有没有水,也不想收到通知打扰他睡觉。这在我们程序中也一样,当我们应用退到后台,这个时候我不需要去消费Flow产生的数据,生产数据也是没有必要的,只有在前台的时候,我们才有必要去生产消费数据。目前有很多方式可以实现这个功能.
方式一 : 把Flow转换成LiveData
Flow<T>.asLiveData(): LiveData
class MessagesViewModel(repository : MessagesRepository) : ViewModel() {
val userMessages = repository.userMessages.asLiveData()
}
class MessageActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState : Bundle?) {
viewModel.userMessage.observe(this) {
messages -> listAdapter.submitList(messages)
}
}
}
这样我们就能像使用普通的LiveData一样使用Flow了,当应用退到后台,由于LiveData的天性就是对生命周期敏感,这个时候就不会消费Flow了.
方式二 : 使用Lifecycle.repeatOnLifecycle(state)
方式一是比较方便可行的,但是感觉有乱,它混合了LiveData和Flow两种技术,这里使用repeatOnLifecycle方式会更为清晰些.
class MessageActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState : Bundle?) {
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.userMessages.collect {
messages -> listAdapter.submitList(messages)
}
}
}
}
}
由于repeatOnLifecycle中执行的协程只会在Activity Destory才会resume, 所以如果要启动多个Flow的收集,我们需要launch多个协程.
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.userMessages.collect {
messages -> listAdapter.submitList(messages)
}
}
launch {
otherFlow.collect { ... }
}
}
}
方式三 : 使用flowWithLifecycle(lifecycle, state)
方式三和方式二的区别是,它仅支持一个Flow的收集.
class MessageActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState : Bundle?) {
...
lifecycleScope.launch {
viewModel.userMessages
.flowWithLifecycle(lifecycle, State.STARTED)
.collect {
messages -> listAdapter.submitList(messages)
}
}
}
}
在利用以上这些方式后,我们能够看到,当应用退到后台不会浪费内存资源了.
关于StateFlow
先说Flow本身存在的问题,再来说下StateFlow存在的意义. 例如我们有下面一个Flow
val result: Flow<Result<UiState>> = flow {
emit(repository.fetchItem())
}
我们知道Activity在旋转后会销毁,重新创建, 那么就会导致flow会重新收集一次. 这显然是不必要的,我们可以通过创建一个buffer, 持有最新的状态,供多个消费者使用,这时候也不用担心旋转后会销毁,重新创建导致flow会重新收集. StateFlow就是为这样一个需求而存在.
private val _myUiState = MutableStateFlow<MyUiState>()
val myUiState : StateFlow<MyUiState> = _myUiState
init {
viewmodelSocpe.launch {
_myUiState.value = Result.Loading
_myUiState.value = repository.fetchStuff()
}
}
kotlin还提供简单的方式去转换普通的flow到一个StateFlow, 使用stateIn
val result: StateFlow<Result<UiState>> = someFlow.stateIn(
initialValue = Result.Loading, // 初始状态
scope = viewModelScope, // 执行对应协程的Scope
started = WhileSubcribed(5000), //后面单独说
)
WhileSubcribed(5000)是啥意思???。当Activity在旋转后会销毁,重新创建这个时候我们希望Flow不用停止发送,因为时间比较短暂,但是应用退到后台我们希望停止发送Flow去节约资源,那么怎么区分这两种情况呢,就是通过WhileSubcribed 5秒超时来区分,当StateFlow停止收集后,5秒内还是会继续生产新的数据。
在退到后台的情况
以上我们看到,应用退到后台后,5秒内还是在产生新数据,5秒后就停止了.
在旋转屏幕的情况
旋转屏幕后,由于从onstop回到onstart非常快,小于5秒,我们便可以继续生产和消费数据.
本文内容来自官方文档