I know that there are a lot of posts "How to cancel Coroutines Scope" but I couldn't find the answer for my case.
I have an Array of objects that I want to send each of them to Server using Coroutines.
What I need is, if one of my requests returns error, canceling others.
Here is my code:
private fun sendDataToServer(function: () -> Unit) {
LiabilitiesWizardSessionManager.getLiabilityAddedDocuments().let { documents ->
if (documents.isEmpty().not()) {
CoroutineScope(Dispatchers.IO).launch {
documents.mapIndexed { index, docDetail ->
async {
val result = uploadFiles(docDetail)
}
}.map {
var result = it.await()
}
}
} else function.invoke()
}
}
Below is my uploadFiles() function:
private suspend fun uploadFiles(docDetail: DocDetail): ArchiveFileResponse? {
LiabilitiesWizardSessionManager.mCreateLiabilityModel.let { model ->
val file = File(docDetail.fullFilePath)
val crmCode = docDetail.docTypeCode
val desc = docDetail.docTypeDesc
val id = model.commitmentMember?.id
val idType = 1
val createArchiveFileModel = CreateArchiveFileModel(108, desc, id, idType).apply {
this.isLiability = true
this.adaSystem = 3
}
val result = mRepositoryControllerKotlin.uploadFile(file, createArchiveFileModel)
return when (result) {
is ResultWrapper.Success -> {
result.value
}
is ResultWrapper.GenericError -> {
null
}
is ResultWrapper.NetworkError -> {
null
}
}
}
}
I know, I'm missing something.
Related
I'm aware that there are a couple of topics on this but none of them solve my issue.
I'm trying to test an implementation of NetworkBoundResource.
inline fun <ResultType, RequestType, ErrorType> networkBoundResource(
crossinline query: () -> Flow<ResultType>,
crossinline fetch: suspend () -> Response<RequestType>,
crossinline saveFetchResult: suspend (RequestType) -> Unit,
crossinline onFetchFailed: (Response<*>?, Throwable?) -> ErrorType? = { _, _ -> null },
crossinline shouldFetch: (ResultType) -> Boolean = { true },
coroutineDispatcher: CoroutineDispatcher
) = flow<Resource<ResultType, ErrorType>> {
val data = query().first()
emit(Resource.Success(data))
if (shouldFetch(data)) {
val fetchResponse = safeApiCall { fetch() }
val fetchBody = fetchResponse.body()
if (fetchBody != null) {
saveFetchResult(fetchBody)
}
if (!fetchResponse.isSuccessful) {
emit(Resource.Error(onFetchFailed(fetchResponse, null)))
} else {
query().map { emit(Resource.Success(it)) }
}
}
}.catch { throwable ->
emit(Resource.Error(onFetchFailed(null, throwable)))
}.flowOn(coroutineDispatcher)
This works as expected in my use case in production code.
override suspend fun getCategories() = networkBoundResource(
query = {
categoryDao.getAllAsFlow().map { categoryMapper.categoryListFromDataObjectList(it) }
},
fetch = {
categoryServices.getCategories()
},
onFetchFailed = { errorResponse, _ ->
categoryMapper.toError(errorResponse)
},
saveFetchResult = { response ->
// Clear the old items and add the new ones
categoryDao.clearAll()
categoryDao.insertAll(categoryMapper.toDataObjectList(response.data))
},
coroutineDispatcher = dispatchProvider.IO
)
I have my test setup like this (using turbine for flow testing).
#OptIn(ExperimentalCoroutinesApi::class)
class NetworkBoundResourceTests {
data class ResultType(val data: String)
sealed class RequestType {
object Default : RequestType()
}
sealed class ErrorType {
object Default : RequestType()
}
private val dispatchProvider = TestDispatchProviderImpl()
#Test
fun `Test`() = runTest {
val resource = networkBoundResource(
query = { flowOf(ResultType(data = "")) },
fetch = { Response.success(RequestType.Default) },
saveFetchResult = { },
onFetchFailed = { _, _ -> ErrorType.Default },
coroutineDispatcher = dispatchProvider.IO
)
resource.test {
}
}
}
The coroutine dispatcher is set to unconfined through DI/Test dispatcher.
I want to test that;
Emitting first data from query, then query is updated and new data from saveFetchResult then query().map { emit(Resource.Success(it)) } emits the updated data from that save result.
I think I need to do something around a spyk on my flow with MockK but I can't seem to figure it out. query() will always return the same flow of data as it's mocked to do so if I awaitItem() again it returns the same data (as it should) as that's what the mock is setup for.
I've found a way to test this. Not exactly how I imagined it in my head.
#Test
fun `Given should fetch is true and fetch throws exception, When retrieving data, Then cached items emitted and error item after`() =
runTest {
val saveFetchResultAction = mockk<(() -> Unit)>("Save results action")
val fetchErrorAction = mockk<(() -> ErrorType)>("Fetch error action")
every { fetchErrorAction() } answers { ErrorType }
val fetchRequestAction = mockk<(() -> Response<RequestType>)>("Fetch request action")
coEvery { fetchRequestAction() } throws (Exception(""))
networkBoundResource(
query = { flowOf(ResultType) },
fetch = { fetchRequestAction() },
saveFetchResult = { saveFetchResultAction() },
onFetchFailed = { _, _ -> fetchErrorAction() },
shouldFetch = { true },
coroutineDispatcher = dispatchProvider.IO
).test {
// Assert that we've got the cached item
val cacheItem = awaitItem()
assertThat(cacheItem).isInstanceOf(Resource.Success::class.java)
val errorItem = awaitItem()
assertThat(errorItem).isInstanceOf(Resource.Error::class.java)
awaitComplete()
// Verify order & calls
verifyOrder {
fetchRequestAction()
fetchErrorAction()
}
verify(exactly = 1) { fetchErrorAction() }
verify(exactly = 1) { fetchRequestAction() }
verify(exactly = 0) { saveFetchResultAction() }
}
}
I'm looking for filter a list on user input (SearchView)
fun onQuery(query: String) {
viewModelScope.launch(Default) {
val personsFound = persons.filter { person ->
person.nom.contains(query) || person.prenom.contains(query)
}
withContext(Main) { _items.value = personsFound }
}
}
If the user tap quickly on the keyboard the function will be called many times and sometimes before the previous call is finished. So I'm looking to stop the coroutine if a new call is done and the coroutine is already running. How can I achieved this please ?
What I tried :
fun onQuery(query: String) {
val job = viewModelScope.launch(Default) {
val personsFound = persons.filter { person ->
person.nom.contains(query) || person.prenom.contains(query)
}
withContext(Main) { _items.value = personsFound }
}
if (job.isActive) job.cancel()
job.start()
}
If there is supposed to be only one instance of that job running, then I would try taking a reference to it outside of that code block and storing it as a private variable inside the class in which the method onQuery operates:
private var job: Job? = null
...
fun onQuery(query: String) {
job?.run { if (isActive) cancel() }
job = viewModelScope.launch(Default) {
val personsFound = persons.filter { person ->
person.nom.contains(query) || person.prenom.contains(query)
}
withContext(Main) { _items.value = personsFound }
}
}
I have 5 api call function in the viewModel that I want to called parallel how can I do this? I put each of the function in the WithContext(Dispachers.IO) but it's not working. I used coroutines flow for calling api.
Note: I used clean architecture pattern and I have single use-case
ViewModel codes:
class MyJobsViewModel constructor(
private val myJobsUseCases: MyJobsUseCases,
private val clientNavigator: ClientNavigator
) : ViewModel(), ClientNavigator by clientNavigator {
private val _state = mutableStateOf(MyJobsState())
val state: State<MyJobsState> get() = _state
private fun getAllJobs(
offset: Int = 0,
limit: Int = 10,
type: JobTypeEnum = JobTypeEnum.ALL
) {
myJobsUseCases.getJobsUseCase.invoke(offset = offset, limit = limit, type = type)
.onEach {
when (it) {
is Resource.Success -> _state.value =
state.value.copy(
isLoading = false,
allJobItems = it.data ?: JobItemsResponse()
)
is Resource.Error -> _state.value =
state.value.copy(
isLoading = false,
error = it.message ?: "An unexpected error occurred"
)
is Resource.Loading -> _state.value = state.value.copy(isLoading = true)
}
}.launchIn(viewModelScope)
}
private fun getActiveJobs(
offset: Int = 0,
limit: Int = 10,
type: JobTypeEnum = JobTypeEnum.ALL
) {
myJobsUseCases.getJobsUseCase.invoke(offset = offset, limit = limit, type = type)
.onEach {
when (it) {
is Resource.Success -> _state.value =
state.value.copy(
isLoading = false,
activeJobItems = it.data ?: JobItemsResponse()
)
is Resource.Error -> _state.value =
state.value.copy(
isLoading = false,
error = it.message ?: "An unexpected error occurred"
)
is Resource.Loading -> _state.value = state.value.copy(isLoading = true)
}
}.launchIn(viewModelScope)
}
}
The best way to parallel multiple calls is to follow structured concurrency principle.
For example, when you need to evaluate the result of 5 independent network requests:
suspend fun fetchA(): Int { /* ... */ }
suspend fun fetchB(): Int { /* ... */ }
suspend fun fetchC(): Int { /* ... */ }
suspend fun fetchD(): Int { /* ... */ }
suspend fun fetchE(): Int { /* ... */ }
suspend fun overallResult(): Int = coroutineScope {
val a = async { fetchA() }
val b = async { fetchB() }
val c = async { fetchC() }
val d = async { fetchD() }
val e = async { fetchE() }
a.await() + b.await() + c.await() + d.await() + e.await()
}
Or when you need to make 5 independent api calls without returning any value:
suspend fun callA() { /* ... */ }
suspend fun callB() { /* ... */ }
suspend fun callC() { /* ... */ }
suspend fun callD() { /* ... */ }
suspend fun callE() { /* ... */ }
suspend fun makeCalls(): Unit = coroutineScope {
launch { callA() }
launch { callB() }
launch { callC() }
launch { callD() }
launch { callE() }
}
Wrapping function in launch or async block produces a new coroutine and executes in parallel.
coroutineScope organizes the area where launch and async can be used. It completes only when every child coroutine is completed.
I have a problem for now in JetpackCompose.
The problem is, when I'm collecting the Data from a flow, the value is getting fetched from firebase like there is a listener and the data's changing everytime. But tthat's not that.
I don't know what is the real problem!
FirebaseSrcNav
suspend fun getName(uid: String): Flow<Resource.Success<Any?>> = flow {
val query = userCollection.document(uid)
val snapshot = query.get().await().get("username")
emit(Resource.success(snapshot))
}
NavRepository
suspend fun getName(uid: String) = firebase.getName(uid)
HomeViewModel
fun getName(uid: String): MutableStateFlow<Any?> {
val name = MutableStateFlow<Any?>(null)
viewModelScope.launch {
navRepository.getName(uid).collect { nameState ->
when (nameState) {
is Resource.Success -> {
name.value = nameState.data
//_posts.value = state.data
loading.value = false
}
is Resource.Failure<*> -> {
Log.e(nameState.throwable, nameState.throwable)
}
}
}
}
return name
}
The probleme is in HomeScreen I think, when I'm calling the collectasState().value.
HomeScreen
val state = rememberLazyListState()
LazyColumn(
state = state,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(post) { post ->
//val difference = homeViewModel.getDateTime(homeViewModel.getTimestamp())
val date = homeViewModel.getDateTime(post.timeStamp!!)
val name = homeViewModel.getName(post.postAuthor_id.toString()).collectAsState().value
QuestionCard(
name = name.toString(),
date = date!!,
image = "",
text = post.postText!!,
like = 0,
response = 0,
topic = post.topic!!
)
}
}
I can't post video but if you need an image, imagine a textField where the test is alternating between "null" and "MyName" every 0.005 second.
Check official documentation.
https://developer.android.com/kotlin/flow
Flow is asynchronous
On viewModel
private val _name = MutableStateFlow<String>("")
val name: StateFlow<String>
get() = _name
fun getName(uid: String) {
viewModelScope.launch {
//asyn call
navRepository.getName(uid).collect { nameState ->
when (nameState) {
is Resource.Success -> {
name.value = nameState.data
}
is Resource.Failure<*> -> {
//manager error
Log.e(nameState.throwable, nameState.throwable)
}
}
}
}
}
on your view
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
viewModel.name.collect { name -> handlename
}
}
}
I've got this function getting documents from Cloud Firestore:
fun getBasicItems(callback: (MutableList<FireStoreBasicItem>) -> Unit) {
fireStore.collection("BasicItems")
.get()
.addOnCompleteListener { task ->
if (task.isSuccessful) {
val basicItems = mutableListOf<FireStoreBasicItem>()
for (document in task.result!!) {
val fireStoreBasicItem = document.toObject(FireStoreBasicItem::class.java)
basicItems.add(fireStoreBasicItem)
callback(basicItems)
}
}
}
}
In my ViewModel I want to transform this to an Observable an then to a ViewState:
private fun loadDataTransformer(): ObservableTransformer<ItemEvent.LoadDataEvent, ItemsViewState> {
return ObservableTransformer { event ->
event.map {
itemRepository.getBasicItems(){myBasicItemList -> Observable.just(myBasicItemList)}
}
}
I tried it also with Observable.fromCallable. What am I doing wrong?
EDIT: My Solution
private fun loadDataTransformer(): ObservableTransformer<ItemEvent.LoadDataEvent, ItemsViewState> {
return ObservableTransformer { event ->
event.flatMap {
Single.create<MutableList<FireStoreBasicItem>> {
itemRepository.getBasicItems { myBasicItemList ->
it.onSuccess(myBasicItemList)
}
}.toObservable()
.map {
ItemsViewState.ItemDataState(it)
}
}
}
}
I would like to suggest you to use Single instead of Observable if you are expecting only one list of items. Then you can use Single.create:
private fun loadDataTransformer(): Single<ItemsViewState> =
Single.create { emitter ->
itemRepository.getBasicItems() { myBasicItemList ->
val viewState = // do some transformations
emitter.onSuccess(viewState)
}
}