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() )
}
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 have a job inside my AndroidViewModel class. Job is triggered by viewModelScope.launch. Job is a long running process which return result by lambda functions. According to requirement If user want to cancel the job while remaining in the scope on button click it should cancel the job. The problem is when I cancel the job, process is still running in the background and it is computing the background task. Below is my ViewModelClass with its job and cancel function.
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.*
class SelectionViewModel(val app: Application) : AndroidViewModel(app) {
private var mainJob: Job? = null
private var context: Context? = null
fun performAction(
fileOptions: FileOptions,
onSuccess: (ArrayList<String>?) -> Unit,
onFailure: (String?) -> Unit,
onProgress: (Pair<Int, Int>) -> Unit
) {
mainJob = viewModelScope.launch {
withContext(Dispatchers.IO) {
kotlin.runCatching {
while (isActive) {
val mOutputFilePaths: ArrayList<String> = ArrayList()
// Long running Background task
.. progress
OnProgress.invoke(pair)
// resul success
onSuccess.invoke(mOutputFilePaths)
}
}.onFailure {
withContext(Dispatchers.Main) {
onFailure.invoke(it.localizedMessage)
}
}
}
}
}
fun cancelJob() {
mainJob?.cancel()
}
}
Here it is I am initiating my ViewModel
val viewModel: SelectionViewModelby lazy {
ViewModelProviders.of(this).get(SelectionViewModel::class.java)
}
and when I start the job I call the following method
viewModel.performAction(fileOptions,{success->},{failure->},{progress->})
When I want to cancel the task. I call the following method.
viewModel.cancelJob()
Problem is even after canceling the job I am still receiving the progress as it is being invoked. This means job has not been canceled.
I want to implement the correct way to start and cancel the job while remaining in the viewmodel scope.
So what is the proper way to implement the viewmodel to start and cancel the job?
In order to cancel the job you have to have a suspending function call.
This means that if your job has code like
while (canRead) {
read()
addResults()
}
return result
this can never be cancelled the way you wish it to be cancelled.
there are two ways you can cancel this code
a) add a delay function (this will check for cancelling and cancel your job)
b) (which in the above case is the correct way) periodically add a yield() function
so the above code should look like this:
while(canRead) {
yield()
read()
addResults()
}
return result
edit: some further explanations are probably necessary to make this clear
just because you run something withContext, does not mean that coroutines can stop or break it at any time
what coroutines do is basically change the old way of doing things with callbacks and replace it with suspending functions
what we used to do for complex calculations was start a thread ,which would execute the calculations and then get a callback with the results.
at any point you could cancel the thread and the work would stop.
cancelling coroutines is not the same
if you cancel a coroutine what you basically do is tell it that the job is cancelled , and at the next opportune moment it should stop
but if you don't use yield() delay() or any suspending function such an opportune moment will never arrive
it is the equivalent of running something like this with threads
while(canRead && !cancelled) {
doStuff
}
where you would manually set the cancelled flag, if you set it but didn't check it in your code , it would never stop
as a side note, be careful because right now you have a big block of calculations running code, this will run on one thread because you never called a suspending function. When you add the yield() call , it could change threads or context (within what you defined ofc) so make sure it is thread safe
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.