Coroutine scope cancel - android

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

Unit testing network bound resource

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() }
}
}

How to cancel a coroutine if already running?

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 }
}
}

Call parallel multiple the api request in the clean architecture

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.

Problem with state in jetpackCompose and Flow

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
}
}
}

Callback to Observable

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)
}
}

Categories

Resources