在视图模型中替换当前协程调用的最佳实践

Best practise for replacing current coroutine call in viewmodels

我有以下内容:

interface CartRepository {
  fun getCart(): Flow<CartState>
}

interface ProductRepository {
  fun getProductByEan(ean: String): Flow<Either<ServerError, Product?>>
}

class ScanViewModel(
  private val productRepository: ProductRepository,
  private val cartRepository: CartRepository
) :
  BaseViewModel<ScanUiState>(Initial) {
  fun fetchProduct(ean: String) = viewModelScope.launch {
    setState(Loading)

    productRepository
      .getProductByEan(ean)
      .combine(cartRepository.getCart(), combineToGridItem())
      .collect { result ->
        when (result) {
          is Either.Left -> {
            sendEvent(Error(R.string.error_barcode_product_not_found, null))
            setState(Initial)
          }
          is Either.Right -> {
            setState(ProductUpdated(result.right))
          }
        }
      }
    }
}

当用户扫描条形码时 fetchProduct 被调用。每次建立一个新的协程。一段时间后,后台有很多 运行,当购物车状态全部更新时触发 combine,这可能会导致错误。

我想取消所有旧协程,只有最新调用 运行 和购物车更改更新。

我知道我可以通过保存作业并在开始新作业之前取消它来执行以下操作。但这真的是要走的路吗?好像我错过了什么。

var searchJob: Job? = null

private fun processImage(frame: Frame) {
  barcodeScanner.process(frame.toInputImage(this))
    .addOnSuccessListener { barcodes ->
      barcodes.firstOrNull()?.rawValue?.let { ean ->
        searchJob?.cancel()
        searchJob = viewModel.fetchProduct(ean)
      }
    }
    .addOnFailureListener {
      Timber.e(it)
      messageMaker.showError(
        binding.root,
        getString(R.string.unknown_error)
      )
    }
}

我还可以在我的 ViewModel 中使用 MutableSharedFlow,以确保 UI 仅对用户获取的最后一个产品做出反应:

  private val productFlow = MutableSharedFlow<Either<ServerError, Product?>>(replay = 1)

  init {
    viewModelScope.launch {
      productFlow.combine(
        mycroftRepository.getCart(),
        combineToGridItem()
      ).collect { result ->
        when (result) {
          is Either.Right -> {
            setState(ProductUpdated(result.right))
          }
          else -> {
            sendEvent(Error(R.string.error_barcode_product_not_found, null))
            setState(Initial)
          }
        }
      }
    }
  }

  fun fetchProduct(ean: String) = viewModelScope.launch {
    setState(Loading)

    repository.getProductByEan(ean).collect { result ->
      productFlow.emit(result)
    }
  }

处理这种情况的最佳做法是什么?

我想不出更简单的模式来在开始新作业时取消之前的任何作业。

如果您担心在屏幕旋转时丢失存储的作业引用(您可能不会,因为 Fragment 实例通常在旋转时重复使用),您可以将作业存储和取消移动到 ViewModel 中:

private var fetchProductJob: Job? = null

fun fetchProduct(ean: String) {
    fetchProductJob?.cancel()
    fetchProductJob = viewModelScope.launch {
        //...
    }
}

如果您重复使用此模式,您可以像这样创建一个助手 class。不知道有没有更好的方法。

class SingleJobPipe(val scope: CoroutineScope) {
    private var job: Job? = null

    fun launch(
        context: CoroutineContext = EmptyCoroutineContext,
        start: CoroutineStart = CoroutineStart.DEFAULT,
        block: suspend CoroutineScope.() -> Unit
    ): Job = synchronized(this) {
        job?.cancel()
        scope.launch(context, start, block).also { job = it }
    }
}

// ...

private val fetchProductPipe = SingleJobPipe(viewModelScope)

fun fetchProduct(ean: String) = fetchProductPipe.launch {
        //...
    }