網站首頁 編程語言 正文
概念簡介
StateFlow和SharedFlow都是kotlin中的數據流,官方概念簡介如下:
StateFlow:一個狀態容器式可觀察數據流,可以向其收集器發出當前狀態和新狀態。是熱數據流。
SharedFlow:StateFlow是StateFlow的可配置性極高的泛化數據流(StateFlow繼承于SharedFlow)
對于兩者的基本使用以及區別,此處不做詳解,可以參考官方文檔。本文會給出一些關于如何在業務中選擇選擇合適熱流(hot flow)的建議,以及單元測試代碼。
StateFlow的一般用法如下圖所示:
以讀取數據庫數據為例,Repository負責從數據庫讀取相應數據并返回一個flow,在ViewModel收集這個flow中的數據并更新狀態(StateFlow),在MVVM模型中,ViewModel中暴露出來的StateFlow應該是UI層中唯一的可信數據來源,注意是唯一,這點跟使用LiveData的時候不同。
我們應該在ViewModel中暴露出熱流(StateFlow或者SharedFlow)而不是冷流(Flow)
如果我們如果暴露出的是普通的冷流,會導致每次有新的流收集者時就會觸發一次emit,造成資源浪費。所以如果Repository提供的只有簡單的冷流怎么辦?很簡單,將之轉換成熱流就好了!通常可以采用以下兩種方式:
1、還是正常收集冷流,收集到一個數據就往另外構建的StateFlow或SharedFlow發送
2、使用stateIn或shareIn拓展函數轉換成熱流
既然官方給我們提供了拓展函數,那肯定是直接使用這個方案最好,使用方式如下:
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參數保證了當配置改變時不會重新觸發訂閱 }
在一些業務復雜的頁面,比如首頁,通常會有多個數據來源,也就有多個flow,為了保證單一可靠數據源原則,我們可以使用combine函數將多個flow組成一個flow,然后再使用stateIn函數轉換成StateFlow。
shareIn拓展函數使用方式也是類似的,只不過沒有初始值initialValue參數,此處不做贅述。
這兩者如何選擇?
上文說到,我們應該在ViewModel中暴露出熱流,現在我們有兩個熱流-StateFlow和SharedFlow,如何選擇?
沒什么特定的規則,選擇的時候只需要想一下一下問題:
1.我真的需要在特定的時間、位置獲取Flow的最新狀態嗎?
如果不需要,那考慮SharedFlow,比如常用的事件通知功能。
2.我需要重復發射和收集同樣的值嗎?
如果需要,那考慮SharedFlow,因為StateFlow會忽略連續兩次重復的值。
3.當有新的訂閱者訂閱的時候,我需要發射最近的多個值嗎?
如果需要,那考慮SharedFlow,可以配置replay參數。
compose中收集流的方式
關于在UI層收集ViewModel層的熱流方式,官方文檔已經有介紹,但是沒有補充在JetPack Compose中的收集流方式,下面補充一下。
先添加依賴implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha03'
// 收集StateFlow val uiState by viewModel.userFlow.collectAsStateWithLifecycle() // 收集SharedFlow,區別在于需要賦初始值 val uiState by viewModel.userFlow.collectAsStateWithLifecycle( initialValue = UiState.Loading ) when(uiState) { is UiState.Loading -> TODO() is UiState.Success -> TODO() is UiState.Error -> TODO() }
使用collectAsStateWithLifecycle()也是可以保證流的收集操作之發生在應用位于前臺的時候,避免造成資源浪費。
單元測試
由于我們會在ViewModel中使用到viewModelScope,首先可以定義一個MainDispatcherRule,用于設置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單元測試代碼中:
class MyViewModelTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() ... }
1.測試StateFlow
現在我們有一個業務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) } } } } } }
單元測試代碼如下:
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(...) // 初始值應該是UiState.Loading,因為stateFlow可以直接獲取最新值,此處直接做斷言 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拓展函數:
@OptIn(ExperimentalCoroutinesApi::class) @Test fun `when initialized, repository emits loading and data`() = runTest { //arrange val viewModel = MainWithStateinViewModel(repository) val users = listOf(...) //action // 因為此時collect操作并不是在ViewModel中,我們需要在測試代碼中執行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.測試SharedFlow
測試SharedFlow可以使用一個開源庫Turbine,Turbine是一個用于測試Flow的小型開源庫。
測試使用sharedIn拓展函數的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
相關推薦
- 2022-05-23 Android實現手機聯系人分欄效果_Android
- 2022-10-01 react使用useState修改對象或者數組的值無法改變視圖的問題_React
- 2022-08-25 .net加載失敗的程序集實現重新加載_實用技巧
- 2022-01-13 使用postcss插件配置rem和手寫rem的方法
- 2022-04-22 阿里云ECS服務器Linux下載安裝JDK8
- 2022-05-20 Spring Boot 整合流程引擎 Flowable,so easy
- 2022-07-10 linux賬號管理權限
- 2022-09-10 python中isoweekday和weekday的區別及說明_python
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支