ViewModel中StateFlow和SharedFlow單元測試使用詳解
概念簡介
StateFlow和SharedFlow都是kotlin中的數(shù)據(jù)流,官方概念簡介如下:
StateFlow:一個(gè)狀態(tài)容器式可觀察數(shù)據(jù)流,可以向其收集器發(fā)出當(dāng)前狀態(tài)和新狀態(tài)。是熱數(shù)據(jù)流。
SharedFlow:StateFlow是StateFlow的可配置性極高的泛化數(shù)據(jù)流(StateFlow繼承于SharedFlow)
對于兩者的基本使用以及區(qū)別,此處不做詳解,可以參考官方文檔。本文會(huì)給出一些關(guān)于如何在業(yè)務(wù)中選擇選擇合適熱流(hot flow)的建議,以及單元測試代碼。
StateFlow的一般用法如下圖所示:
以讀取數(shù)據(jù)庫數(shù)據(jù)為例,Repository負(fù)責(zé)從數(shù)據(jù)庫讀取相應(yīng)數(shù)據(jù)并返回一個(gè)flow,在ViewModel收集這個(gè)flow中的數(shù)據(jù)并更新狀態(tài)(StateFlow),在MVVM模型中,ViewModel中暴露出來的StateFlow應(yīng)該是UI層中唯一的可信數(shù)據(jù)來源,注意是唯一,這點(diǎn)跟使用LiveData的時(shí)候不同。
我們應(yīng)該在ViewModel中暴露出熱流(StateFlow或者SharedFlow)而不是冷流(Flow)
如果我們?nèi)绻┞冻龅氖瞧胀ǖ睦淞?,?huì)導(dǎo)致每次有新的流收集者時(shí)就會(huì)觸發(fā)一次emit,造成資源浪費(fèi)。所以如果Repository提供的只有簡單的冷流怎么辦?很簡單,將之轉(zhuǎn)換成熱流就好了!通??梢圆捎靡韵聝煞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ù)雜的頁面,比如首頁,通常會(huì)有多個(gè)數(shù)據(jù)來源,也就有多個(gè)flow,為了保證單一可靠數(shù)據(jù)源原則,我們可以使用combine函數(shù)將多個(gè)flow組成一個(gè)flow,然后再使用stateIn函數(shù)轉(zhuǎn)換成StateFlow。
shareIn拓展函數(shù)使用方式也是類似的,只不過沒有初始值initialValue參數(shù),此處不做贅述。
這兩者如何選擇?
上文說到,我們應(yīng)該在ViewModel中暴露出熱流,現(xiàn)在我們有兩個(gè)熱流-StateFlow和SharedFlow,如何選擇?
沒什么特定的規(guī)則,選擇的時(shí)候只需要想一下一下問題:
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)有介紹,但是沒有補(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)用位于前臺的時(shí)候,避免造成資源浪費(fèi)。
單元測試
由于我們會(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單元測試代碼中:
class MyViewModelTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() ... }
1.測試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) } } } } } }
單元測試代碼如下:
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中,我們需要在測試代碼中執(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.測試SharedFlow
測試SharedFlow可以使用一個(gè)開源庫Turbine,Turbine是一個(gè)用于測試Flow的小型開源庫。
測試使用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) } }
以上就是ViewModel中StateFlow和SharedFlow單元測試使用詳解的詳細(xì)內(nèi)容,更多關(guān)于ViewModel StateFlow SharedFlow測試的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解Android 基于TCP和UDP協(xié)議的Socket通信
這篇文章主要介紹了詳解Android 基于TCP和UDP協(xié)議的Socket通信,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-11-11Android自定義頂部導(dǎo)航欄控件實(shí)例代碼
這篇文章主要介紹了Android自定義頂部導(dǎo)航欄控件實(shí)例代碼,需要的朋友可以參考下2017-12-12Android補(bǔ)間動(dòng)畫基本使用(位移、縮放、旋轉(zhuǎn)、透明)
這篇文章主要介紹了Android補(bǔ)間動(dòng)畫基本使用(位移、縮放、旋轉(zhuǎn)、透明),補(bǔ)間動(dòng)畫就是原形態(tài)變成新形態(tài)時(shí)為了過渡變形過程,生成的動(dòng)畫2018-05-05Android字符串和十六進(jìn)制相互轉(zhuǎn)化出現(xiàn)的中文亂碼問題
這篇文章主要介紹了Android字符串和十六進(jìn)制相互轉(zhuǎn)化出現(xiàn)的中文亂碼問題的相關(guān)資料,需要的朋友可以參考下2016-02-02利用Kotlin如何實(shí)現(xiàn)Android開發(fā)中的Parcelable詳解
這篇文章主要給大家介紹了關(guān)于利用Kotlin如何實(shí)現(xiàn)Android開發(fā)中的Parcelable的相關(guān)資料,并且給大家介紹了關(guān)于Kotlin使用parcelable出現(xiàn):BadParcelableException: Parcelable protocol requires a Parcelable.Creator...問題的解決方法,需要的朋友可以參考下。2017-12-12Android原生項(xiàng)目集成Flutter解決方案
這篇文章主要介紹了Android原生項(xiàng)目集成Flutter解決方案,想了解Flutter的同學(xué)可以參考下2021-04-04android實(shí)現(xiàn)自動(dòng)關(guān)機(jī)的具體方法
android實(shí)現(xiàn)自動(dòng)關(guān)機(jī)的具體方法,需要的朋友可以參考一下2013-06-06基于Android實(shí)現(xiàn)仿QQ5.0側(cè)滑
本課程將帶領(lǐng)大家通過自定義控件實(shí)現(xiàn)QQ5.0側(cè)滑菜單,課程將循序漸進(jìn),首先實(shí)現(xiàn)最普通的側(cè)滑菜單,然后引入屬性動(dòng)畫與拖動(dòng)菜單效果相結(jié)合,最終實(shí)現(xiàn)QQ5.0側(cè)滑菜單效果。通過本課程大家會(huì)對側(cè)滑菜單有更深層次的了解,通過自定義控件和屬性動(dòng)畫打造千變?nèi)f化的側(cè)滑菜單效果2015-12-12