Introduction to Flow
Google recommends using Kotlin Flow in the MVVM architecture, which shows that its development prospects are very good.
Kotlin Flow can be used to replace Rxjava and LiveData. It is very powerful. It is part of the functions provided by the Kotlin coroutine library. Therefore, if Kotlin coroutines have been referenced in our project, there is no need to introduce additional Flow-related dependency.
In the coroutine, the suspending function can only return at most one value , while the data stream Flow can emit multiple values in sequence , for example, we can receive updates from the database in real time through the data stream. Dataflow uses suspend functions to produce and consume values asynchronously , that is, Dataflow can safely make a network request to produce the next value without blocking the main thread.
Data flow Flow consists of three important roles:
- Data provider : generate data and add it to the data flow
- Mediator (optional) : can modify the value sent to the data stream, or modify the data stream itself
- Data Consumers : Consume values from data streams
Create data flow
flow
The builder function creates a new stream, and we can then use emit
the function to send new values into the stream.
val latestNews: Flow<List<NewsData>> = flow {
while (true) {
val latestNews = newsApi.fetchLatestNews()
emit(latestNews)
delay(5000)
}
}
modify data flow
Mediations can utilize intermediate operators to modify the data flow without using values , for example:
- filter : filter the value to be operated
- map : Continue to pass backwards after processing the value
- flatMapLatest : converted to a new stream, needs to return a converted new stream. (If the next value comes, the conversion of the previous value has not been completed, and the conversion of the previous value will be cancelled)
- onEach : each value received
val news: Flow<List<NewsData>> = myRemoteDataSource.latestNews
// 先过滤列表数据大于3的数据(大于3才能通过)
.filter {
it.size >= 3
}
// 对结果进行加工后继续向后传递
.map {
list ->
// 调用 list.filter 进一步筛选出 id==1 的新闻
list.filter {
it.id == 1 }
}
// 转换成一个新的流,需要返回一个转换后的新的流。(如果下个值来了,上一个值变换还没结束,上一个值的转换会被取消)
.flatMapLatest {
flow {
emit(it)
}
}
.onEach {
// todo 获取到筛选后 新闻列表(结果) 数据
}
collect data flow
The data provider is only triggered to refresh the latest data when the data stream is collected. Dataflows are always cold and execute lazily unless the flow is specified with another intermediate operator.
fun getNewsData() {
viewModelScope.launch(Dispatchers.Main) {
remoteRepository.news
.catch {
// todo 收集异常
}
.collect {
// 收到到的数据
}
}
}
Stream collection may stop for the following reasons:
- The coroutine that collected the data is canceled, which also stops the activity of the data provider.
- The data provider finishes emitting the data item. In this case, the data flow will be closed and the
collect
calling coroutine will continue to execute.
catch exception
To handle exceptions, you can use catch
the operator , such as:
fun getNewsData() {
viewModelScope.launch {
remoteRepository.news
.catch {
// todo 在这里收集异常
}
.collect {
newsData.value = it
}
}
}
In addition, the operationcatch
can also be performed to emit new data items to the data stream. For example, if we find an exception upstream , we can continue to call the emit function to send new data (or previously cached data), such as:emit
class MyRemoteRepository @Inject constructor(
private val myRemoteDataSource: MyRemoteDataSource,
) {
// 返回 id 等于 1 的新闻
val news: Flow<List<NewsData>> = myRemoteDataSource.latestNews
.map {
news ->
// 筛选出 id==1 的新闻
news.filter {
it.id == 1 }
}
.onEach {
// todo 获取到筛选后 新闻列表(结果) 数据
}
.catch {
// 如果在 上游 收集到异常,我们可以继续调用 emit 函数发送新的数据(或者之前缓存的数据)
// 例如:emit(lastCachedNews())
}
}
Coroutine scope switching
By default, flow
the upstream data provider will CoroutineContext
execute , that is, by default, the downstream and upstream will run in the same coroutine scope .
Also, it cannot perform emit
operations .
If you need to change the coroutine scope of the data flow, you can use the intermediate operator flowOn
operator .
flowOn
Changes the scope of upstream dataflows CoroutineContext
, but does not affect downstream dataflows .
If there are multiple flowOn
operators , each operator changes the upstream data flow at the current position.
// 上游数据流代码,上游数据流将会在 Dispatchers.IO 作用域上执行:
class MyRemoteRepository @Inject constructor(
private val myRemoteDataSource: MyRemoteDataSource,
) {
val news: Flow<List<NewsData>> = myRemoteDataSource.latestNews
.flowOn(Dispatchers.IO)
.catch {
}
}
// 下游数据流代码,下游数据流将会在 Dispatchers.Main 作用域上执行
fun getNewsData() {
viewModelScope.launch(Dispatchers.Main) {
remoteRepository.news.collect {
}
}
}
Flow Demo demo
The following code will demonstrate how to continuously get the latest news list through Flow.
Step 1: Create a News class and define the news format
data class NewsData(var id: Int, var content: String)
Step 2: Create a NewsApi interface for requesting the latest news list
interface NewsApi {
/**
* 请求最新的新闻列表
*/
suspend fun fetchLatestNews(): List<NewsData>
companion object {
fun create(): NewsApi {
return NewsApiImpl()
}
}
}
Step 3: Create a DataSource class with a latestNews variable inside, as the upstream data provider of Flow , request news data through newsApi every 5 seconds, and call the emit method to send the news data
class MyRemoteDataSource @Inject constructor(
private val newsApi: NewsApi,
) {
val latestNews: Flow<List<NewsData>> = flow {
while (true) {
val latestNews = newsApi.fetchLatestNews()
emit(latestNews)
delay(5000)
}
}
}
Step 4: Create the RemoteRepository class, which has a news variable inside. As the data intermediate processor of Flow , filtering data, switching upstream scopes, and collecting upstream exceptions can all be processed here.
class MyRemoteRepository @Inject constructor(
private val myRemoteDataSource: MyRemoteDataSource,
) {
// 返回 id 等于 1 的新闻
val news: Flow<List<NewsData>> = myRemoteDataSource.latestNews
.map {
news ->
// 筛选出 id==1 的新闻
news.filter {
it.id == 1 }
}
.onEach {
// todo 获取到筛选后 新闻列表(结果) 数据
}
.flowOn(Dispatchers.IO) // 上游数据流将会在 Dispatchers.IO 作用域上执行
.catch {
// 如果在 上游 收集到异常,我们可以继续调用 emit 函数发送新的数据(或者之前缓存的数据)
// 例如:emit(lastCachedNews())
}
}
Step 5: Create a ViewModel class, define a member method getNewsData() as the downstream data receiver of Flow , and define a LiveData variable to monitor the latest news data.
@HiltViewModel
class MainViewModel @Inject constructor(
private val remoteRepository: MyRemoteRepository,
) : ViewModel() {
val newsData = MutableLiveData<List<NewsData>>()
fun getNewsData() {
viewModelScope.launch(Dispatchers.Main) {
remoteRepository.news
.catch {
// todo 收集异常
}
.collect {
newsData.value = it
}
}
}
}
Step 6: Write MainActivity code, receive the latest news data and print it out
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val mMainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initObserve()
}
private fun initObserve() {
mMainViewModel.newsData.observe(this) {
println("newsData=${
Gson().toJson(it)}")
}
}
fun click(view: View) {
mMainViewModel.getNewsData()
}
}
Step 7: Create the implementation class of NewsApi
class NewsApiImpl : NewsApi {
override suspend fun fetchLatestNews(): List<NewsData> {
val list = ArrayList<NewsData>()
list.add(NewsData(1, "news 1"))
list.add(NewsData(2, "news 2"))
list.add(NewsData(3, "news 3"))
return list
}
}
Step 8: Write dependency injection class (di)
@InstallIn(SingletonComponent::class)
@Module
class MainModule {
@Singleton
@Provides
fun provideAppDatabase(@ApplicationContext context: Context): NewsApi {
return NewsApi.create()
}
}
// 另外,别忘了在 Application 中加上 @HiltAndroidApp 注解
@HiltAndroidApp
class MainApplication : Application()
Step 9: Since the demo uses Hilt, we need to add the following dependencies:
// Project-build.gradle
buildscript {
ext {
hiltVersion = '2.41'
}
dependencies {
classpath "com.google.dagger:hilt-android-gradle-plugin:$hiltVersion"
}
}
// app-build.gradle
plugins {
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
dependencies {
kapt "com.google.dagger:hilt-android-compiler:$rootProject.hiltVersion"
implementation "com.google.dagger:hilt-android:$rootProject.hiltVersion"
}
Step 10: In order to use the API in the activity or fragment by viewModels()
, we also need to introduce the following dependencies:
// app-build.gradle
dependencies {
implementation "androidx.activity:activity-ktx:1.4.0"
implementation "androidx.fragment:fragment-ktx:1.4.1"
}
StateFlw
A StateFlow is a state-container-style observable flow that emits current and new state updates to its collectors . The current state value can also be read through its value
property .
StateFlow
Great for classes that need to keep mutable state observable .
Flow
It is a cold data flow, but StateFlow
a hot data flow. The hot flow has the following characteristics:
StateFlow.collect
Calling collectdata doesn't trigger any数据提供方(上游)
tags in the- If the upstream data stream is already in the active (sending) state, even if there is no call anywhere
StateFlow.collect
, the upstream stream will continue to be active (no Gc Root reference will naturally be recycled) - It allows sharing by multiple observers (hence shared data streams)
When a 新的数据接收方
begins collect
feeding data from a stream, it receives the most recent state and any subsequent states in the stream .
Note : If the new data received by StateFlow.value is the same as the previous old data, the downstream will not receive the data update notification.
StateFlow 和 LiveData
StateFlow
and LiveData
have similarities, both are observable data container classes .
But there are also differences:
StateFlow
Need to pass the initial state to the constructor insteadLiveData
of- When the View enters
STOPPED
the state ,LiveData.observe()
it will automatically unregister the consumer, but the operation of collecting data fromStateFlow
or any other data flow will not stop automatically. To achieve the same behavior as LiveData,Lifecycle.repeatOnLifecycle
the data flow needs to be collected from the block
StateFlow
A simple usage of is as follows:
// ViewModel: (_uiState.value更新的地方属于上游)
@HiltViewModel
class MainViewModel @Inject constructor(
private val remoteRepository: MyRemoteRepository,
) : ViewModel() {
// 定义一个私有的 MutableStateFlow 变量(可变)
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
// UI 从此 StateFlow 收集以获取其状态更新
val uiState: StateFlow<LatestNewsUiState> = _uiState
fun getNewsData() {
viewModelScope.launch {
remoteRepository.news.collect {
// 接受到最新的新闻列表数据后,将数据赋值给 StateFlow 的 value
_uiState.value = LatestNewsUiState.Success(it)
}
}
}
}
// Activity: (mMainViewModel.uiState.collect的地方属于下游)
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val mMainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initObserve()
}
private fun initObserve() {
mMainViewModel.viewModelScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mMainViewModel.uiState.collect {
// 注意:如果 StateFlow.value 接收的新数据和前一个旧数据一样时,下游并不会接收到数据的更新通知
when (it) {
is LatestNewsUiState.Success -> {
println("获取新闻成功,news=${
Gson().toJson(it.news)}")
}
is LatestNewsUiState.Error -> {
println("获取新闻失败,error=${
Gson().toJson(it.exception)}")
}
}
}
}
}
}
fun click(view: View) {
mMainViewModel.getNewsData()
}
}
In the following code, the class responsible for MutableStateFlow
updating is 数据提供方(上游)
, and the class StateFlow.collect
from is 数据使用方(下游)
.
In addition, repeatOnLifecycle
the interface will only be updated when the interface is active. To use this API, the following dependencies need to be introduced:
// Project-build.gradle
buildscript {
ext {
lifecycleVersion = '2.4.1'
}
}
// app-build.gradle
dependencies {
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$rootProject.lifecycleVersion"
}
Flow to StateFlow
To transform any stream of data into StateFlow
, you can use the stateIn
intermediate operator.
stateIn
There are two overloaded functions, generally we use the second one:
// 函数1:
public suspend fun <T> Flow<T>.stateIn(scope: CoroutineScope): StateFlow<T>
// 函数2:
public fun <T> Flow<T>.stateIn(
scope: CoroutineScope,
started: SharingStarted,
initialValue: T
): StateFlow<T>
Among them, function 1 is a suspend function, and only needs to pass a scope parameter, function 2 is a non-suspend function, and needs to pass three parameters, the meaning of the three parameters is as follows:
1. scope
: The scope of the coroutine where the shared stream starts
2. started
: strategy to control the start and end of sharing
3. initialValue
: The initial value of the stream
And started
there are three possible values:
SharingStarted.Eagerly
: Immediately start the upstream data flow, and terminate the upstream flow when thescope
specified scope is endedSharingStarted.Lazily
: Start the upstream data flow after the first subscriber appears, and terminate the upstream flow when thescope
specified scope is endedSharingStarted.WhileSubscribed(stopTimeoutMillis)
: Start the upstream data flow after the first subscriber appears. If there is no downstream collection, the upstream data flow will be canceled after the specified time (default is 0)
stateIn
Use as follows:
// ViewModel:
@HiltViewModel
class MainViewModel @Inject constructor(
private val remoteRepository: MyRemoteRepository,
) : ViewModel() {
// 将 news(Flow冷流) 转为 StateFlow 热流
val mStateFlow: StateFlow<List<NewsData>> = remoteRepository.news
.stateIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
initialValue = emptyList()
)
}
// Activity:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val mMainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
// 点击开始收集上游数据
fun click(view: View) {
mMainViewModel.viewModelScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mMainViewModel.mStateFlow.collect {
println("收集到数据,it=${
Gson().toJson(it)}")
}
}
}
}
}
The ViewModel in the example defines a variable that converts the cold Flow flow to StateFlow through the stateIn operator. The StateFlow starts to start the upstream data flow after the first subscriber appears. If there is no downstream collection, it will be 5 seconds later The upstream data stream is canceled, and the initialValue is initially set to an empty list. In the code of MainActivity, clicking the button will trigger the upstream stream to start sending data, and the downstream stream will also start receiving data.
WhileSubscribed
5000 is passed in to implement 5
the function of terminating the coroutine if there is still no subscriber after waiting for seconds. This method has the following functions:
- After the app goes to the background, all data updates from other layers stop after 5 seconds to save battery
- When the screen is rotated, the upstream stream will not be aborted because the resubscription time is within 5s
SharedFlow
SharedFlow
The configuration is more flexible, supports configuration replay
, buffer size, etc., StateFlow
is SharedFlow
a specialized version, replay
fixed to 1, and the default buffer size is 0
We can use shareIn
the function which will return a hot data stream SharedFlow
that emitsSharedFlow
data to all from it .数据接收方(下游)
Let's first look at the constructor of ShareFlow:
public fun <T> MutableSharedFlow(
replay: Int = 0,
extraBufferCapacity: Int = 0,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T>
It mainly has 3 parameters:
replay
: When there is a new subscriberCollect
, send some data that has already been sent to it, the default is 0extraBufferCapacity
: In additionreplay
,SharedFlow
how much data is cached, the default is 0onBufferOverflow
: Indicates the cache policy, that is,ShareFlow
how to , and the default is to suspend
The difference between StateFolw and SharedFlow
StateFolw
SharedFlow
and are heat flow.
StateFlow
Essentially, it replay
is 1 and has no buffer SharedFlow
, so the default value will be obtained first when subscribing for the first time.
StateFlow
It will only return when the value has been updated and the value has changed, that is to say, if the updated value has not changed, the Collect
method will not call back, ShareFlow
but will call back.
Here is a simple example of using SharedFlow:
// ViewModel:
@HiltViewModel
class MainViewModel @Inject constructor(
private val remoteRepository: MyRemoteRepository,
) : ViewModel() {
// 定义一个私有的 MutableSharedFlow 变量(可变),当有新的订阅者时,会先发送1个之前发送过的数据给订阅者
private val mMutableSharedFlow = MutableSharedFlow<List<NewsData>>(replay = 1)
// 不可变的 shareFlow
val shareFlow: SharedFlow<List<NewsData>> = mMutableSharedFlow
fun getNewsData() {
viewModelScope.launch {
remoteRepository.news.collect {
mMutableSharedFlow.emit(it)
}
}
}
}
// Activity:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val mMainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
// ShareFlow 开始发送数据
fun click(view: View) {
mMainViewModel.getNewsData()
}
// 收集 ShareFlow 发送的数据
fun click2(view: View) {
mMainViewModel.viewModelScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mMainViewModel.shareFlow.collect {
println("获取到的新闻,news=${
Gson().toJson(it)}")
}
}
}
}
}
In this example, a ShareFlow is defined in the ViewModel, and its replay parameter is set to 1, that is, when there is a new subscriber, a previously sent data will be sent to the subscriber first. In the Activity, there are two buttons. Button 1 triggers ShareFlow upstream to start sending data, and button 2 triggers downstream to collect data. After button 2 is pressed, the downstream will first collect a previously sent data.
Flow to SharedFlow
To convert any data stream to SharedFlow
, you can use the ShareIn
intermediate operator:
public fun <T> Flow<T>.shareIn(
scope: CoroutineScope,
started: SharingStarted,
replay: Int = 0
): SharedFlow<T>
ShareIn
The function has three parameters:
- scope: the scope of the coroutine scope where the shared stream starts
- started: the policy that controls the start and end of the share
- replay: When there is a new subscriber
Collect
, send several data that have already been sent to it, the default is 0
A simple example (similar to the StateIn example):
// ViewModel:
@HiltViewModel
class MainViewModel @Inject constructor(
private val remoteRepository: MyRemoteRepository,
) : ViewModel() {
// 将 news(Flow冷流) 转为 SharedFlow 热流
val mSharedFlow: SharedFlow<List<NewsData>> = remoteRepository.news
.shareIn(
scope = viewModelScope,
started = WhileSubscribed(5000),
replay = 1
)
}
// Activity:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val mMainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
fun click(view: View) {
// 点击开始收集上游数据
mMainViewModel.viewModelScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
mMainViewModel.mSharedFlow.collect {
println("收集到的新闻数据,news=${
Gson().toJson(it)}")
}
}
}
}
}
The ViewModel in the example defines a variable to convert the cold Flow flow to SharedFlow through the shareIn operator. The SharedFlow starts to start the upstream data flow after the first subscriber appears. If there is no downstream collection, it will be in 5 seconds. Cancel the upstream data flow. In addition, replay is set to 1. When there is a downstream collector, the latest value sent before will be sent to the downstream collector. In the code of MainActivity, clicking the button will trigger the upstream stream to start sending data, and the downstream stream will also start receiving data.