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.
Related
I don't know how to define a Job as new. I want to define a job to execute it whenever I want, using start() method.
The result I get is that the job is executed when I define it.
override fun onStart() {
super.onStart();
var idenNuevo:String?=null
val job1 = GlobalScope.launch(Dispatchers.Default) {
idenNuevo = insertarUsuario();
}
with(thus.bind){
registroBtnRegistro.setOnClickListener(){
job1.start()
}
runBlocking{
job1.join()
runOnUiThread {
registroTvDebug.text = "$idenNuevo"
}
}
}
}
You need to explicitly set a different start parameter, the default puts the Job in the active state:
val job1 = GlobalScope.launch(Dispatchers.Default, start=CoroutineStart.LAZY) {
By default, the coroutine is immediately scheduled for execution. Other start options can be specified via start parameter. See CoroutineStart for details. An optional start parameter can be set to CoroutineStart.LAZY to start coroutine lazily. In this case, the coroutine Job is created in new state. It can be explicitly started with start function and will be started implicitly on the first invocation of join.
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].
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
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()
I want to use coroutines instead of handlers. I have call which will refresh data every few seconds. It is called in Job object. When I leave app to background, I want to cancel this Job. When I come back to app I want to relaunch this job to do its work.
But once I cancel job, it will not relaunch ever again. Is there any way how to relaunch cancelled job? Because isActive == false even when I reinitialize variable
var job: Job? = null
job = launch {
while (isActive){
delay(5000)
doStuff()
}
}
When I pause my Activity I call job?.cancel() and job = null
When I enter Activity, I will call code above again. I thought I can reinitialize this job and launch refresh code again.
There is no way to restart a cancel()ed job. But this is easy enough to work around. Just start a new job instead. You could do it like this:
fun launchNewJob() = launch {
while (isActive) {
delay(5000)
doStuff()
}
}
job = launchNewJob()
Jobs/coroutines are not heavy objects like Threads, so you don't need to worry about creating new ones.
You can't "re-launch" the Job but you can create a new one, when you wrap the code inside:
var job = launch { jobFunction() }
fun jobFunction() {
while (isActive){
delay(5000)
doStuff()
}
}
if(job.isActive.not()) {
job = launch( jobFunction() )
}