[Android] the use and analysis of Flow

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

flowThe builder function creates a new stream, and we can then use emitthe 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 collectcalling coroutine will continue to execute.

catch exception

To handle exceptions, you can use catchthe 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, flowthe upstream data provider will CoroutineContextexecute , that is, by default, the downstream and upstream will run in the same coroutine scope .

Also, it cannot perform emitoperations .

If you need to change the coroutine scope of the data flow, you can use the intermediate operator flowOnoperator .

flowOnChanges the scope of upstream dataflows CoroutineContext, but does not affect downstream dataflows .

If there are multiple flowOnoperators , 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 valueproperty .

StateFlowGreat for classes that need to keep mutable state observable .

FlowIt is a cold data flow, but StateFlowa hot data flow. The hot flow has the following characteristics:

  • StateFlow.collectCalling 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 collectfeeding 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

StateFlowand LiveDatahave similarities, both are observable data container classes .

But there are also differences:

  • StateFlowNeed to pass the initial state to the constructor instead LiveDataof
  • When the View enters STOPPEDthe state , LiveData.observe()it will automatically unregister the consumer, but the operation of collecting data from StateFlowor any other data flow will not stop automatically. To achieve the same behavior as LiveData, Lifecycle.repeatOnLifecyclethe data flow needs to be collected from the block

StateFlowA 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 MutableStateFlowupdating is 数据提供方(上游), and the class StateFlow.collectfrom is 数据使用方(下游).

In addition, repeatOnLifecyclethe 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 stateInintermediate operator.

stateInThere 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 startedthere are three possible values:

  • SharingStarted.Eagerly: Immediately start the upstream data flow, and terminate the upstream flow when the scopespecified scope is ended
  • SharingStarted.Lazily: Start the upstream data flow after the first subscriber appears, and terminate the upstream flow when the scopespecified scope is ended
  • SharingStarted.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)

stateInUse 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.

WhileSubscribed5000 is passed in to implement 5the 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

SharedFlowThe configuration is more flexible, supports configuration replay, buffer size, etc., StateFlowis SharedFlowa specialized version, replayfixed to 1, and the default buffer size is 0

We can use shareInthe function which will return a hot data stream SharedFlowthat 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 subscriber Collect, send some data that has already been sent to it, the default is 0
  • extraBufferCapacity: In addition replay, SharedFlowhow much data is cached, the default is 0
  • onBufferOverflow: Indicates the cache policy, that is, ShareFlowhow to , and the default is to suspend

The difference between StateFolw and SharedFlow

StateFolw SharedFlow and are heat flow.

StateFlowEssentially, it replayis 1 and has no buffer SharedFlow, so the default value will be obtained first when subscribing for the first time.

StateFlowIt 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 Collectmethod 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 ShareInintermediate operator:

public fun <T> Flow<T>.shareIn(
    scope: CoroutineScope,
    started: SharingStarted,
    replay: Int = 0
): SharedFlow<T>

ShareInThe 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.

Guess you like

Origin blog.csdn.net/yang553566463/article/details/125082239