我的编程空间,编程开发者的网络收藏夹
学习永远不晚

ViewModel中StateFlow和SharedFlow单元测试使用详解

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

ViewModel中StateFlow和SharedFlow单元测试使用详解

概念简介

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

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 {
  
  private val usersFlow =
    MutableSharedFlow<List<User>>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
  override fun getUsers(): Flow<List<User>> {
    return usersFlow
  }
  
  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)
    }
}

以上就是ViewModel中StateFlow和SharedFlow单元测试使用详解的详细内容,更多关于ViewModel StateFlow SharedFlow测试的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

ViewModel中StateFlow和SharedFlow单元测试使用详解

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

ViewModel中StateFlow和SharedFlow单元测试使用详解

这篇文章主要为大家介绍了ViewModel中StateFlow和SharedFlow单元测试使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-01-17

GOLang单元测试用法详解

Go语言中自带有一个轻量级的测试框架testing和自带的gotest命令来实现单元测试和性能测试。本文将通过示例详细聊聊Go语言单元测试的原理与使用,需要的可以参考一下
2022-12-15

前端自动化测试Vue中TDD和单元测试示例详解

这篇文章主要为大家介绍了前端自动化测试Vue中TDD和单元测试示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-02-14

Go单元测试利器testify使用示例详解

这篇文章主要为大家介绍了Go单元测试利器testify使用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2022-12-15

Android Studio中使用junit做单元测试

单元测试(unit testing),是指对软件中的小可测试单元进行检查和验证。比如一个函数,一个方法等。关于单元测试要不要做,由谁来做这些问题暂时抛到一边。本文只是单纯的介绍如何用Android Studio做单元测试。1. 确保你的工程
2022-06-06

Android中使用Junit进行单元测试

不管我们在学习还是在开发的时候,都会用到测试,在Android中进行的Junit单元工具测试需要创建一个类去继承于AndroidTestCase类,同时还需要在主配置文件AndroidManifest.xml中配置相关的信息创建Test类继
2022-06-06

使用 PHP 函数的最佳实践:测试和单元测试?

针对 php 函数进行测试的最佳实践包括:单元测试:隔离测试单个函数或类,验证预期行为;集成测试:测试多个函数和类的交互,验证应用程序整体运行情况。PHP 函数的最佳实践:测试和单元测试引言在 PHP 中编写健壮可靠的代码至关重要。单元
使用 PHP 函数的最佳实践:测试和单元测试?
2024-05-03

利用Python中unittest实现简单的单元测试实例详解

前言 单元测试的重要性就不多说了,可恶的是Python中有太多的单元测试框架和工具,什么unittest, testtools, subunit, coverage, testrepository, nose, mox, mock, fix
2022-06-04

怎么在SpringBoot中使用Mockito单元测试

这期内容当中小编将会给大家带来有关怎么在SpringBoot中使用Mockito单元测试,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。Mock 测试Mock 测试就是在测试过程中,创建一个假的对象,避免你
2023-06-15

详解如何用JavaScript编写一个单元测试

测试代码是确保代码稳定的第一步。能做到这一点的最佳方法之一就是使用单元测试。这篇文章主要介绍了如何用JavaScript编写你的第一个单元测试,感兴趣的可以了解一下
2022-11-13

C++中怎么使用CppUnit进行单元测试

这篇文章主要讲解了“C++中怎么使用CppUnit进行单元测试”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“C++中怎么使用CppUnit进行单元测试”吧!如果使用VC6,那么直接用VC6打
2023-06-17

Android中如何使用JUnit进行单元测试

在我们日常开发android app的时候,需要不断地进行测试,所以使用JUnit测试框架显得格外重要,学会JUnit可以加快应用的开发周期。Android中建立JUnit测试环境有以下两种方法。一、直接在需要被测试的工程中新建测试类集成步
2022-06-06

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录