Kotlin Flow 可以用于替換 Rxjava,也可以用于替換 LiveData,功能十分強大,它是 Kotlin 協程庫提供的一部分功能,因此,如果我們項目中已經引用了 Kotlin 協程,則不需要額外引入 Flow 相關的依賴。
在協程中,掛起函數最多僅能返回一個值,而數據流 Flow 可按順序發出多個值,例如,我們可以通過數據流從數據庫中實時接收更新。數據流使用掛起函數通過異步方式生成和使用值,也就是說,數據流可安全地發出網絡請求以生成下一個值,而不會阻塞主線程。
數據流 Flow 包含三個重要角色:
數據提供方:生成數據,并添加到數據流中
中介(可選):可修改發送到數據流的值,或修正數據流本身
數據使用方:使用數據流中的值
創建數據流
flow 構建器函數會創建一個新數據流,然后我們可使用 emit 函數將新值發送到數據流中。
val latestNews: Flow<List<NewsData>> = flow {
while (true) {
val latestNews = newsApi.fetchLatestNews()
emit(latestNews)
delay(5000)
}
}
修改數據流
中介可以利用中間運算符在不使用值的情況下修改數據流,例如:
- filter : 對待操作的值進行過濾
- map :對值進行加工后繼續向后傳遞
- flatMapLatest:轉換成一個新的流,需要返回一個轉換后的新的流。(如果下個值來了,上一個值變換還沒結束,上一個值的轉換會被取消)
- onEach:接收到的每一個值
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 獲取到篩選后 新聞列表(結果) 數據
}
收集數據流
只有在 收集數據流時 才會觸發 數據提供方 刷新最新數據。除非使用其他中間運算符指定流,否則數據流始終為冷流并延遲執行。
fun getNewsData() {
viewModelScope.launch(Dispatchers.Main) {
remoteRepository.news
.catch {
// todo 收集異常
}
.collect {
// 收到到的數據
}
}
}
數據流收集可能會由于以下原因而停止:
收集數據的協程被取消,此操作也會讓 數據提供方 停止活動。
數據提供方完成發出數據項。在這種情況下,數據流將關閉,調用 collect 的協程則繼續執行。
捕獲異常
如需處理異常,可以使用 catch 運算符,如:
fun getNewsData() {
viewModelScope.launch {
remoteRepository.news
.catch {
// todo 在這里收集異常
}
.collect {
newsData.value = it
}
}
}
另外,catch 還可執行 emit 操作,向數據流發出新的數據項,例如,如果我們在 上游 發現了異常,我們可以繼續調用 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())
}
}
協程作用域切換
默認情況下,flow 上游數據提供方 會基于 下游數據收集方 的協程 CoroutineContext 執行,也就是說,默認情況,下游和上游會運行在同一個協程作用域中。
并且,它無法從不同協程作用域對值執行 emit 操作。
如果需要更改數據流的的協程作用域,可以使用中間運算符 flowOn 運算符。
flowOn 會更改上游數據流的 CoroutineContext,但不會影響到下游數據流的作用域。
如果有多個 flowOn 運算符,每個運算符都會更改當前位置的上游數據流。
// 上游數據流代碼,上游數據流將會在 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 演示
下面的代碼將演示通過 Flow 不斷獲取最新的新聞列表。
步驟一:創建 News 類,定義新聞格式
data class NewsData(var id: Int, var content: String)
1
步驟二:創建 NewsApi 接口,用于請求最新的新聞列表
interface NewsApi {
/**
* 請求最新的新聞列表
*/
suspend fun fetchLatestNews(): List<NewsData>
companion object {
fun create(): NewsApi {
return NewsApiImpl()
}
}
}
步驟三:創建 DataSource 類,內部有一個 latestNews 變量,作為 Flow 的上游數據提供者,每隔 5 秒,通過 newsApi 請求新聞數據,并調用 emit 方法將新聞數據發出
class MyRemoteDataSource @Inject constructor(
private val newsApi: NewsApi,
) {
val latestNews: Flow<List<NewsData>> = flow {
while (true) {
val latestNews = newsApi.fetchLatestNews()
emit(latestNews)
delay(5000)
}
}
}
步驟四:創建 RemoteRepository 類,內部有一個 news 變量,作為 Flow 的數據中間處理者,篩選數據,切換上游作用域,收集上游異常等都可以在這里處理。
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())
}
}
步驟五:創建 ViewModel 類,定義了一個成員方法 getNewsData() ,作為 Flow 的下游數據接收者,另外,還定義了一個 LiveData 變量,監聽最新的新聞數據。
@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
}
}
}
}
步驟六:編寫 MainActivity 代碼,接受最新的新聞數據并打印出來
@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()
}
}
步驟七:創建 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
}
}
步驟八:編寫 依賴注入類(di)
@InstallIn(SingletonComponent::class)
@Module
class MainModule {
@Singleton
@Provides
fun provideAppDatabase(@ApplicationContext context: Context): NewsApi {
return NewsApi.create()
}
}
// 另外,別忘了在 Application 中加上 @HiltAndroidApp 注解
@HiltAndroidApp
class MainApplication : Application()
步驟九:由于 demo 使用到了 Hilt ,因此我們需要加上如下依賴:
// 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"
}
步驟十:為了能在 activity 或 fragment 中使用 by viewModels() api,我們還需額外引入以下 依賴:
// app-build.gradle
dependencies {
implementation "androidx.activity:activity-ktx:1.4.0"
implementation "androidx.fragment:fragment-ktx:1.4.1"
}
StateFlw
StateFlow 是一個狀態容器式的可觀察數據流,可以向其收集器發出當前狀態更新和新狀態更新。還可通過其 value 屬性讀取當前狀態值。
StateFlow 非常適合需要讓可變狀態保持可觀察的類。
Flow 是冷數據流,而 StateFlow 是熱數據流,熱流有如下特性:
- 調用 StateFlow.collect 收集數據不會觸發任何 數據提供方(上游) 的代碼
- 上游數據流 如果已經處于活躍(發送)狀態,即使沒有任何地方調用 StateFlow.collect ,上游流仍會持續活躍(沒有 Gc Root 引用自然也會被回收)
- 它允許被多個觀察者共用 (因此是共享的數據流)
當一個 新的數據接收方 開始從數據流中 collect 數據時,它將接收到信息流中的最近一個狀態及任何后續狀態。
注意:如果 StateFlow.value 接收的新數據和前一個舊數據一樣時,下游并不會接收到數據的更新通知。
StateFlow 和 LiveData
StateFlow 和 LiveData 具有相似之處,兩者都是可觀察的數據容器類。
但也存在不同之處:
- StateFlow 需要將初始狀態傳遞給構造函數,而 LiveData 不需要
- 當 View 進入 STOPPED 狀態時,LiveData.observe() 會自動取消注冊使用方,但是從 StateFlow 或任何其他數據流收集數據的操作并不會自動停止,如需實現與 LiveData 相同的行為,則需從 Lifecycle.repeatOnLifecycle 塊中收集數據流
StateFlow 的簡單用法如下:
// 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()
}
}
如下代碼中,負責更新 MutableStateFlow 的類是 數據提供方(上游) ,從 StateFlow.collect 的類是 數據使用方(下游)。
另外,repeatOnLifecycle 能使界面處于活躍狀態下才會更新界面,要使用該 api,還需引入以下依賴:
// Project-build.gradle
buildscript {
ext {
lifecycleVersion = '2.4.1'
}
}
// app-build.gradle
dependencies {
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$rootProject.lifecycleVersion"
}
Flow 轉為 StateFlow
如需將任何數據流轉換為 StateFlow ,可以使用 stateIn 中間運算符。
stateIn 有兩個重載函數,一般我們用第二個:
// 函數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>
其中函數1是一個掛起函數,且僅需要傳一個 scope 參數既可,函數2是非掛起函數,需要傳遞三個參數,三個參數的含義如下:
1、scope :共享流開始時所在的協程作用域范圍
2、started :控制共享的開始和結束的策略
3、initialValue: 流的初始值
而 started 有三種取值可選:
- SharingStarted.Eagerly :立即啟動上游數據流,且在 scope 指定的作用域被結束時終止上游流
- SharingStarted.Lazily :在第一個訂閱者出現后開始啟動上游數據流,且在 scope 指定的作用域被結束時終止上游流
- SharingStarted.WhileSubscribed(stopTimeoutMillis) :在第一個訂閱者出現后開始啟動上游數據流,沒有下游收集的情況下,指定時間后(默認是0)會取消上游數據流
stateIn 使用如下:
// 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)}")
}
}
}
}
}
例子中的 ViewModel,定義了一個變量,將 Flow 冷流通過 stateIn 操作符轉為 StateFlow ,該 StateFlow 在第一個訂閱者出現后開始啟動上游數據流,沒有下游收集的情況下,會在 5秒 后取消上游數據流,另外,initialValue 初始值設置為 一個空列表 。在 MainActivity 的代碼中,點擊按鈕將會觸發 上游流開始發送數據,同時下游流也開始接收數據。
WhileSubscribed 傳入了 5000 ,是為了實現等待5 秒后仍然沒有訂閱者存在就終止協程的功能,這個方法有以下功能:
應用轉至后臺運行后,5 秒鐘后所有來自其他層的數據更新會停止,這樣可以節省電量
在屏幕旋轉時,因為重新訂閱的時間在5s內,因此上游流不會中止
SharedFlow
SharedFlow 配置更為靈活,支持配置replay,緩沖區大小等,StateFlow是SharedFlow的特化版本,replay固定為1,緩沖區大小默認為0
我們可使用 shareIn 函數會返回一個熱數據流 SharedFlow, SharedFlow 會 向從其所有的 數據接收方(下游) 發出數據。
我們先看下 ShareFlow 的構造函數:
public fun <T> MutableSharedFlow(
replay: Int = 0,
extraBufferCapacity: Int = 0,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T>
其主要有3個參數:
- replay:有新的訂閱者Collect時,發送幾個已經發送過的數據給它,默認為0
- extraBufferCapacity:除了 replay,SharedFlow 還緩存多少數據,默認為0
- onBufferOverflow:表示緩存策略,即緩沖區滿了之后 ShareFlow 如何處理,默認為掛起
StateFolw 和 SharedFlow 的區別
StateFolw 和 SharedFlow 都屬于熱流。
StateFlow 本質上是一個 replay 為 1,且沒有緩沖區的 SharedFlow,因此第一次訂閱時會先獲得默認值。
StateFlow 僅在值已更新,并且值發生了變化時才會返回,也就是說如果更新后的值沒有變化,Collect 方法不會回調,但是 ShareFlow 是會回調的。
下面舉個簡單的使用 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)}")
}
}
}
}
}
該例子中,ViewModel 中定義了一個 ShareFlow,并將其 replay 參數設置為 1 ,即當有新的訂閱者時,會先發送 1 個之前發送過的數據給訂閱者。在Activity 中,有兩個按鈕,按鈕 1 觸發 ShareFlow 上游開始發送數據, 按鈕 2 觸發 下游收集數據,按鈕 2 按下后,下游會先收集到 1 個之前發送過的數據。
Flow 轉為 SharedFlow
如需將任何數據流轉換為 SharedFlow ,可以使用 ShareIn 中間運算符:
public fun <T> Flow<T>.shareIn(
scope: CoroutineScope,
started: SharingStarted,
replay: Int = 0
): SharedFlow<T>
ShareIn 函數有三個參數:
- scope:共享流開始時所在的協程作用域范圍
- started:控制共享的開始和結束的策略
- replay:有新的訂閱者Collect時,發送幾個已經發送過的數據給它,默認為0
舉個簡單的例子(跟 StateIn 的例子很像):
// 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)}")
}
}
}
}
}
例子中的 ViewModel,定義了一個變量,將 Flow 冷流通過 shareIn 操作符轉為 SharedFlow ,該 SharedFlow 在第一個訂閱者出現后開始啟動上游數據流,沒有下游收集的情況下,會在 5秒 后取消上游數據流,另外,replay設置為 1 ,當有下游收集者時,會將之前發過的最近一個值發給下游收集者。在 MainActivity 的代碼中,點擊按鈕將會觸發 上游流開始發送數據,同時下游流也開始接收數據。