Cancel viewModelScope and re-use it later on - android

I'm using Coroutines for dealing with async jobs:
viewModelScope.launch {
val userResponse = getUsers() //suspendable function
}
What I want to do is stop all existing/ongoing coroutine jobs. Idea is that I click on different tabs. If getUsers() takes up to 5 seconds and user clicks from User tab to Job tab, I want that existing API call is stopped and response is not observed.
I tried to do viewModelScope.cancel(), but that seems not to be working.
Question is - how to cancel existing jobs on button click?

Define a reusable Job like following in the ViewModel class:
private var job = Job()
get() {
if (field.isCancelled) field = Job()
return field
}
Pass it to all of launch coroutine builders as the parent Job:
viewModelScope.launch(job) {
val userResponse = getUsers()
}
viewModelScope.launch(job) {
// some other work
}
...
On button click, just cancel the parent job:
fun cancelAll() {
job.cancel()
}

You can get its Job through its CouroutineContext like this:
viewModelScope.coroutineContext[Job]
To stop all existing/ongoing coroutine jobs you can call its cancel method:
viewModelScope.coroutineContext[Job]?.cancel()
If you need to start other coroutines eventually then call its cancelChildren method instead:
viewModelScope.coroutineContext[Job]?.cancelChildren()

Related

Android. Kotlin Flow: How to cancel countdown timer?

I have a kotlin flow timer. Here is my code:
class CountDownTimer {
suspend fun startTimer(value: Int, onTick: OnTickCallback, onFinish: OnFinishCallback) {
onTick.invoke(0)
(1..timerValue)
.asSequence()
.asFlow()
.onEach {
delay(DELAY)
onTick.invoke(it)
}
.onCompletion {
onFinish.invoke()
}
.cancellable()
.collect()
}
}
Everything working well, but there are situations when I start a new timer, but the current one has not yet completed. So I want to cancel the timer if I start a new one. I know that for this I need to get a Job and call a cancel() on it.
But I can't create job, because I haven't CoroutineScope.
Yes, I could inject the scope in the constructor of my class CountDownTimer, but I need the timer to be attached to the viewModelScope.
Therefore, I start the timer in the view model in viewModelScope.
viewModelScope.launch {
countDownTimer.startTimer(
60,
onTick= { // some logic },
onFinish= { // some logic }
)
}
Now I can get Job inside view model and before start timer cancel() job.
But it turns out I have to store the job object in the view model, will it be correct? Perhaps there is some more automated way to cancel a job. Or perhaps I'd better inject some other CoroutineScope into my CountDownTimer, then the question is what should the CoroutineScope be?
Please, help me.
viewModelScope.launch {} is a job. You can save it into a variable and cancel it anytime. It is ok to store it in a viewmodel.
val job = viewModelScope.launch { ... }
job.cancel()

Job delay is not started again after canceling

So when I press a button I need to wait 3 seconds before executing another method, I worked that out with the followin
val job = CoroutineScope(Dispatchers.Main).launch(Dispatchers.Default, CoroutineStart.DEFAULT) {
delay(THREE_SECONDS)
if (this.isActive)
product?.let { listener?.removeProduct(it) }
}
override fun onRemoveProduct(product: Product) {
job.start()
}
now, if I press a cancel button right after I start the job I stop the job from happening and that is working fine
override fun onClick(v: View?) {
when(v?.id) {
R.id.dismissBtn -> {
job.cancel()
}
}
}
The problem is that when I execute again the onRemoveProduct that executes the job.start() it will not start again, seems like that job.isActive never yields to true, why is this happening ?
A Job once cancelled cannot be started again. You need to do that in a different way. One way is to create a new job everytime onRemoveProduct is called.
private var job: Job? = null
fun onRemoveProduct(product: Product) {
job = scope.launch {
delay(THREE_SECONDS)
listener?.removeProduct(product) // Assuming the two products are same. If they aren't you can modify this statement accordingly.
}
}
fun cancelRemoval() { // You can call this function from the click listener
job?.cancel()
}
Also, in this line of your code CoroutineScope(Dispatchers.Main).launch(Dispatchers.Default, CoroutineStart.DEFAULT),
You shouldn't/needn't create a new coroutine scope by yourself. You can/should use the already provided viewModelScope or lifecycleScope. They are better choices as they are lifecycle aware and get cancelled at the right time.
Dispatchers.Main is useless because it gets replaced by Dispatchers.Default anyways. Dispatchers.Default is also not required here because you aren't doing any heavy calculations (or calling some blocking code) here.
CoroutineStart.DEFAULT is the default parameter so you could have skipped that one.
And you also need not check if (this.isActive) because
If the [Job] of the current coroutine is cancelled or completed while delay is waiting, it immediately resumes with [CancellationException].

Kotlin Coroutines how to achieve to call api in right way

Hey I want to call api from object class. I am new in Coroutines. I tried some code, but i am not sure is it correct way of doing it or not.
Inside LoginHelper there is function called logout have more that one function. I want to excute api call first. then i want to excute other function inside logout.
In Mainactivity I am calling LoginHelper.logout it will finish then i need to excute other line. But i don't want to make suspend function because it's using other place as well.
Also i got a errorProcess:
com.dimen.app, PID: 12496
android.os.NetworkOnMainThreadException
at android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork(StrictMode.java:1605)
Session.kt
interface Session{
#DELETE("/session/delete")
fun deleteSession(): Call<Void>
}
SessionRepository.kt
suspend fun deleteSession(): RequestResult<Void> {
return apiCall(api.deleteSession())
}
RequestResult is a Sealed Class
sealed class RequestResult<out T : Any> {
data class Success<out T : Any>(): RequestResult<T>
data class Error(): RequestResult<Nothing>()
fun result(success: (data: T?) -> Unit),error: (error: Error) -> Unit)
}
MainActivity.kt
private fun setupLogout() {
logoutButton.setOnClickListener {
LoginHelper.logout() // need to wait untill this finish
// more logic here....
}
}
LoginHelper.kt
object LoginHelper {
fun logout() {
logD("logout")
deleteSession() // need to wait untill this finish and then excute more function....
}
private fun deleteSession() {
runBlocking{
apiCall.deleteSession().execute()
}
}
}
Never use runBlocking in an Android app unless you know exactly what you're doing. It's the wrong choice 99% of the time because it defeats the purpose of using coroutines. Blocking means the current thread waits for the coroutine to run its asynchronous code. But you cannot block the main thread because that freezes the UI.
Since your LoginHelper is an object or singleton, it needs its own CoroutineScope if it's going to launch coroutines.
You can make deleteSession() a suspend function so it can call the api.deleteSession() suspend function.
You can make logout() launch a coroutine to sequentially delete the session and subsequently perform other tasks. And you can make it return the launched Job so other classes can choose whether or not to simply start the logout, or to start and wait for the logout in a coroutine.
object LoginHelper {
private val scope = CoroutineScope(SupervisorJob() + CoroutineName("LoginHelper"))
fun logout(): Job = scope.launch {
logD("logout")
deleteSession()
// .... more functions that happen after deleteSession() is complete
}
private suspend fun deleteSession() {
Tokenclass.getToken()?.let {
logE("token ::-> $it")
apiCall.deleteSession(it).execute()
}
}
}
If you want the outside class to be able to wait for the logout to complete, it can call join() on the returned Job in its own coroutine, for example:
logoutButton.setOnClickListener {
lifecycleScope.launch {
LoginHelper.logout().join()
// more logic here....
}
}
If you don't need to wait for it in the activity, you don't need to start a coroutine, and you don't need to call join().

How to cancel collect coroutine StateFlow?

I have collect flow from shared viewmodel in fragment :
private val viewModel: MyViewModel by sharedViewModel()
private fun observeViewModelStateFlowData() {
job = lifecycleScope.launch {
viewModel.stateFlowData.collect {
when (it) {
is ViewStates.Success -> handleSuccess(it.data)
}
}
}
}
in ViewModel :
private val _stateFlowData = MutableStateFlow<ViewStates<Model>>(ViewStates.Idle)
val stateFlowData: StateFlow<ViewStates<Model>> get() = _stateFlowData
but when I go to next fragment and back to this fragment again, flow collect again.
I cancel the job in onStop() lifecycle method of fragment :
override fun onStop() {
job?.cancel()
super.onStop()
}
but not cancel and collect again!!!
This happens even when I leave the activity (when the viewmodel is cleared) and come back to activity again!!!
How can I do this so that I can prevent the collecting of flow ?
Well you have to know something about coroutine. If we just call cancel, it doesn’t mean that the coroutine work will just stop. If you’re performing some relatively heavy computation, like reading from multiple files, there’s nothing that automatically stops your code from running.
You need to make sure that all the coroutine work you’re implementing is cooperative with cancellation, therefore you need to check for cancellation periodically or before beginning any long running work. Try to add check before handling a result.
job = lifecycleScope.launch {
viewModel.stateFlowData.collect {
ensureActive()
when (it) {
is ViewStates.Success -> handleSuccess(it.data)
}
}
}
}
For more info take a look on this article https://medium.com/androiddevelopers/cancellation-in-coroutines-aa6b90163629

How do I stop a Kotlin coroutine on Android?

I am trying to stop a coroutine if user presses a button. However, when I do:
GlobalScope.launch(Dispatchers.Main) {
//code
}
button.setonclicklistener(){
GlobalScope.cancel()
}
The app crashes. How can I fix this?
Change it like this
var job: Job? = null
job = GlobalScope.launch(Dispatchers.Main) {
//code
}
button.setonclicklistener(){
job?.cancel()
}
Here is the sample you can use and modify as per your code
val job =GlobalScope.launch(Dispatchers.Main) {
try {
//code
} finally {
println("job: I'm running finally")
}
}
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
First, I would like to point out that you should not use the GlobalScope. Instead, you should make your local scope bound to your component fragment/activity/presenter, etc. lifecycle. here's why https://elizarov.medium.com/the-reason-to-avoid-globalscope-835337445abc
Now, once that is taken care of, you can create a local scope like this (assuming you need MainScope)
class MyFragment: Fragment(), CoroutineScope by MainScope() {
var job: Job? = null
....
Next, like #Francesc suggested, you'll need to grab a Job reference. Since every coroutine launch returns a Job, you can keep the reference and cancel it whenever you need. Or in this case, if the fragment dies, the coroutine will be canceled automatically as it's bound to the fragment now (which is most of the time the desired behavior).
job = launch {
// your code
}
button.setOnClickListener() {
job?.cancel()
}
Also please note that now you don't have to mention the scope and context before launching.

Categories

Resources