一篇关于Flow的教程

175 阅读5分钟

一个故事说起

从前山上有个和尚,需要下山找水喝,于是他提上水桶就下山了,然后他找到了一处水源,于是就在这里打水提上山喝。但是好景不长,一段时间后这处水源枯竭了,他不得不另外找一处水源。这样重复几次后,他发现似乎有更好的办法,他希望去建立一个管道,连接所有的水源,并在每个水源处,找了通知人,当一处水源有水的时候就会通知这位和尚,这样他不用盲目的寻找水源了. image.png 虽然用了很大的劲搭建了这套基础设施,但是这位和尚认为一切都是值得的,这样他轻松了很多.

再来谈谈Flow

上面这个故事的例子在代码中就叫响应式模式,在代码里我们也希望建立一个这样模式,对我们的View进行更新,这样会方便很多 image.png Kotlin中Flow便提供这样的功能.

在Flow中它的基本流程是

生产者:

生产添加到流中的数据。多亏了协程,流程还可以异步生成数据。

加工者:

可以修改发射到流中的每个值或流本身

消费者

使用流中的值。

image.png

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)
            }
        }
    }
}

在利用以上这些方式后,我们能够看到,当应用退到后台不会浪费内存资源了. image.png

关于StateFlow

先说Flow本身存在的问题,再来说下StateFlow存在的意义. 例如我们有下面一个Flow

val result: Flow<Result<UiState>> = flow {
    emit(repository.fetchItem())
}

我们知道Activity在旋转后会销毁,重新创建, 那么就会导致flow会重新收集一次. 这显然是不必要的,我们可以通过创建一个buffer, 持有最新的状态,供多个消费者使用,这时候也不用担心旋转后会销毁,重新创建导致flow会重新收集. StateFlow就是为这样一个需求而存在.

image.png

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秒内还是会继续生产新的数据。 在退到后台的情况 image.png

以上我们看到,应用退到后台后,5秒内还是在产生新数据,5秒后就停止了.

在旋转屏幕的情况

image.png

旋转屏幕后,由于从onstop回到onstart非常快,小于5秒,我们便可以继续生产和消费数据.

本文内容来自官方文档

developer.android.com/kotlin/flow