I have the following:
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))
}
}
}
}
}
When a user scans a barcode fetchProduct
is being called. Every time a new coroutine is being set up. And after a while, there are many running in the background and the combine
is triggered when the cart state is updated on all of them, which can cause errors.
I want to cancel all old coroutines and only have the latest call running and update on cart change.
I know I can do the following by saving the job and canceling it before starting a new one. But is this really the way to go? Seems like I'm missing something.
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)
)
}
}
I could also have a MutableSharedFlow in my ViewModel to make sure the UI only react to the last product the user has been fetching:
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)
}
}
What's considered best practice handling this scenario?