網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
概念簡(jiǎn)介
StateFlow和SharedFlow都是kotlin中的數(shù)據(jù)流,官方概念簡(jiǎn)介如下:
StateFlow:一個(gè)狀態(tài)容器式可觀察數(shù)據(jù)流,可以向其收集器發(fā)出當(dāng)前狀態(tài)和新狀態(tài)。是熱數(shù)據(jù)流。
SharedFlow:StateFlow是StateFlow的可配置性極高的泛化數(shù)據(jù)流(StateFlow繼承于SharedFlow)
對(duì)于兩者的基本使用以及區(qū)別,此處不做詳解,可以參考官方文檔。本文會(huì)給出一些關(guān)于如何在業(yè)務(wù)中選擇選擇合適熱流(hot flow)的建議,以及單元測(cè)試代碼。
StateFlow的一般用法如下圖所示:
以讀取數(shù)據(jù)庫(kù)數(shù)據(jù)為例,Repository負(fù)責(zé)從數(shù)據(jù)庫(kù)讀取相應(yīng)數(shù)據(jù)并返回一個(gè)flow,在ViewModel收集這個(gè)flow中的數(shù)據(jù)并更新狀態(tài)(StateFlow),在MVVM模型中,ViewModel中暴露出來(lái)的StateFlow應(yīng)該是UI層中唯一的可信數(shù)據(jù)來(lái)源,注意是唯一,這點(diǎn)跟使用LiveData的時(shí)候不同。
我們應(yīng)該在ViewModel中暴露出熱流(StateFlow或者SharedFlow)而不是冷流(Flow)
如果我們?nèi)绻┞冻龅氖瞧胀ǖ睦淞鳎瑫?huì)導(dǎo)致每次有新的流收集者時(shí)就會(huì)觸發(fā)一次emit,造成資源浪費(fèi)。所以如果Repository提供的只有簡(jiǎn)單的冷流怎么辦?很簡(jiǎn)單,將之轉(zhuǎn)換成熱流就好了!通常可以采用以下兩種方式:
1、還是正常收集冷流,收集到一個(gè)數(shù)據(jù)就往另外構(gòu)建的StateFlow或SharedFlow發(fā)送
2、使用stateIn或shareIn拓展函數(shù)轉(zhuǎn)換成熱流
既然官方給我們提供了拓展函數(shù),那肯定是直接使用這個(gè)方案最好,使用方式如下:
private const val DEFAULT_TIMEOUT = 500L @HiltViewModel class MyViewModel @Inject constructor( userRepository: UserRepository ): ViewModel() { val userFlow: StateFlow<UiState> = userRepository .getUsers() .asResult() // 此處返回Flow<Result<User>> .map { result -> when(result) { is Result.Loading -> UiState.Loading is Result.Success -> UiState.Success(result.data) is Result.Error -> UiState.Error(result.exception) } } .stateIn( scope = viewModelScope, initialValue = UiState.Loading, started = SharingStarted.WhileSubscribed(DEFAULT_TIMEOUT) ) // started參數(shù)保證了當(dāng)配置改變時(shí)不會(huì)重新觸發(fā)訂閱 }
在一些業(yè)務(wù)復(fù)雜的頁(yè)面,比如首頁(yè),通常會(huì)有多個(gè)數(shù)據(jù)來(lái)源,也就有多個(gè)flow,為了保證單一可靠數(shù)據(jù)源原則,我們可以使用combine函數(shù)將多個(gè)flow組成一個(gè)flow,然后再使用stateIn函數(shù)轉(zhuǎn)換成StateFlow。
shareIn拓展函數(shù)使用方式也是類似的,只不過(guò)沒(méi)有初始值initialValue參數(shù),此處不做贅述。
這兩者如何選擇?
上文說(shuō)到,我們應(yīng)該在ViewModel中暴露出熱流,現(xiàn)在我們有兩個(gè)熱流-StateFlow和SharedFlow,如何選擇?
沒(méi)什么特定的規(guī)則,選擇的時(shí)候只需要想一下一下問(wèn)題:
1.我真的需要在特定的時(shí)間、位置獲取Flow的最新狀態(tài)嗎?
如果不需要,那考慮SharedFlow,比如常用的事件通知功能。
2.我需要重復(fù)發(fā)射和收集同樣的值嗎?
如果需要,那考慮SharedFlow,因?yàn)镾tateFlow會(huì)忽略連續(xù)兩次重復(fù)的值。
3.當(dāng)有新的訂閱者訂閱的時(shí)候,我需要發(fā)射最近的多個(gè)值嗎?
如果需要,那考慮SharedFlow,可以配置replay參數(shù)。
compose中收集流的方式
關(guān)于在UI層收集ViewModel層的熱流方式,官方文檔已經(jīng)有介紹,但是沒(méi)有補(bǔ)充在JetPack Compose中的收集流方式,下面補(bǔ)充一下。
先添加依賴implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha03'
// 收集StateFlow val uiState by viewModel.userFlow.collectAsStateWithLifecycle() // 收集SharedFlow,區(qū)別在于需要賦初始值 val uiState by viewModel.userFlow.collectAsStateWithLifecycle( initialValue = UiState.Loading ) when(uiState) { is UiState.Loading -> TODO() is UiState.Success -> TODO() is UiState.Error -> TODO() }
使用collectAsStateWithLifecycle()也是可以保證流的收集操作之發(fā)生在應(yīng)用位于前臺(tái)的時(shí)候,避免造成資源浪費(fèi)。
單元測(cè)試
由于我們會(huì)在ViewModel中使用到viewModelScope,首先可以定義一個(gè)MainDispatcherRule,用于設(shè)置MainDispatcher。
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.TestDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.junit.rules.TestRule import org.junit.rules.TestWatcher import org.junit.runner.Description /** * A JUnit [TestRule] that sets the Main dispatcher to [testDispatcher] * for the duration of the test. */ class MainDispatcherRule( val testDispatcher: TestDispatcher = UnconfinedTestDispatcher() ) : TestWatcher() { override fun starting(description: Description) { super.starting(description) Dispatchers.setMain(testDispatcher) } override fun finished(description: Description) { super.finished(description) Dispatchers.resetMain() } }
將MainDispatcherRule用于ViewModel單元測(cè)試代碼中:
class MyViewModelTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() ... }
1.測(cè)試StateFlow
現(xiàn)在我們有一個(gè)業(yè)務(wù)ViewModel如下:
@HiltViewModel class MyViewModel @Inject constructor( private val userRepository: UserRepository ) : ViewModel() { private val _userFlow = MutableStateFlow<UiState>(UiState.Loading) val userFlow: StateFlow<UiState> = _userFlow.asStateFlow() fun onRefresh() { viewModelScope.launch { userRepository .getUsers().asResult() .collect { result -> _userFlow.update { when (result) { is Result.Loading -> UiState.Loading is Result.Success -> UiState.Success(result.data) is Result.Error -> UiState.Error(result.exception) } } } } } }
單元測(cè)試代碼如下:
class MyViewModelTest{ @get:Rule val mainDispatcherRule = MainDispatcherRule() // arrange private val repository = TestUserRepository() @OptIn(ExperimentalCoroutinesApi::class) @Test fun `when initialized, repository emits loading and data`() = runTest { // arrange val viewModel = MyViewModel(repository) val users = listOf(...) // 初始值應(yīng)該是UiState.Loading,因?yàn)閟tateFlow可以直接獲取最新值,此處直接做斷言 assertEquals(UiState.Loading, viewModel.userFlow.value) // action repository.sendUsers(users) viewModel.onRefresh() //check assertEquals(UiState.Success(users), viewModel.userFlow.value) } } // Mock UserRepository class TestUserRepository : UserRepository { /** * The backing hot flow for the list of users for testing. */ private val usersFlow = MutableSharedFlow<List<User>>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) override fun getUsers(): Flow<List<User>> { return usersFlow } /** * A test-only API to allow controlling the list of users from tests. */ suspend fun sendUsers(users: List<User>) { usersFlow.emit(users) } }
如果ViewModel中使用的是stateIn拓展函數(shù):
@OptIn(ExperimentalCoroutinesApi::class) @Test fun `when initialized, repository emits loading and data`() = runTest { //arrange val viewModel = MainWithStateinViewModel(repository) val users = listOf(...) //action // 因?yàn)榇藭r(shí)collect操作并不是在ViewModel中,我們需要在測(cè)試代碼中執(zhí)行collect val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) { viewModel.userFlow.collect() } //check assertEquals(UiState.Loading, viewModel.userFlow.value) //action repository.sendUsers(users) //check assertEquals(UiState.Success(users), viewModel.userFlow.value) collectJob.cancel() }
2.測(cè)試SharedFlow
測(cè)試SharedFlow可以使用一個(gè)開源庫(kù)Turbine,Turbine是一個(gè)用于測(cè)試Flow的小型開源庫(kù)。
測(cè)試使用sharedIn拓展函數(shù)的SharedFlow:
@OptIn(ExperimentalCoroutinesApi::class) @Test fun `when initialized, repository emits loading and data`() = runTest { val viewModel = MainWithShareInViewModel(repository) val users = listOf(...) repository.sendUsers(users) viewModel.userFlow.test { val firstItem = awaitItem() assertEquals(UiState.Loading, firstItem) val secondItem = awaitItem() assertEquals(UiState.Success(users), secondItem) } }
原文鏈接:https://juejin.cn/post/7189176023362043965
相關(guān)推薦
- 2022-05-13 初識(shí)C++ 引用&內(nèi)聯(lián)函數(shù)
- 2022-05-06 SQL查詢服務(wù)器下所有數(shù)據(jù)庫(kù),數(shù)據(jù)庫(kù)的全部表
- 2022-06-29 Python中bytes和str的區(qū)別與聯(lián)系詳解_python
- 2022-08-25 C語(yǔ)言超詳細(xì)講解線性表_C 語(yǔ)言
- 2022-10-28 一文帶你了解Python列表生成式應(yīng)用的八重境界_python
- 2022-08-21 深入了解C語(yǔ)言中常見的文件操作方法_C 語(yǔ)言
- 2024-04-07 springmvc接受對(duì)象參數(shù)和MultipartFile參數(shù)的問(wèn)題
- 2023-07-04 Guava 之 EventBus
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支