Reputation: 2649
I needed to program a way that that would allow the user to interrupt my long processing, if desired by user, by touching a particular button.
Therefore I needed 2 threads, one running my long process and another waiting user interaction, if he wanted to interrupt
From all I read, I understood that the best solution for me would be coroutines
However, I has never used coroutines in Kotlin before. So I am layman in that feature.
To be honest, none of the tutorials in Internet was clear enough to me. With difficulty, I was able to do something.
This below code is inside button btRunOrStop
click handling and it is working OK, with some changes from standard:
if (! solved) // Stopping the long process
bStat.setText("") // clearing the progress
solving = false
runBlocking { jobGl.cancelAndJoin()} // Cancel and ends my long process.
} else { // Starting the long process
bStat.setText("Solving ...") // static progress
GlobalScope.launch {
try {
solving = true // Warn that the long process is running
jobGl = async {
runningProcess(param) // Finally my rocket was launched
}
retGl = jobGl.await() // return of my process, after running OK
solved = true // warn that the process has ended OK.
solving = false // No more pending process
// Generate an artificial button touch
mEv = MotionEvent.obtain(x,y,MotionEvent.ACTION_UP,0F,0F,0)
// False click to post processing after process OK.
btRunOrStop.dispatchTouchEvent(mEv)
}
finally{} // Exception handing. It does not work.
}
}
else { code after successful execution}
Finally my long process:
suspend fun solveEquac(param:Double):Double {
..... if (! solving) return ......
..... if (! GlobalScope.isActive) return ... // It does not work.
}
Unfortunately, I was required to use a variable (solving
) instead of using isActive
or exception block, (try finally
) which is recommended by official Kotlin documentation, because it just don't work. The process stops, but only after it's over.
My questions:
1) Is it possible a memory leakage by using GlobalScope
in same activity?
2) If yes, in what condition it can occurs?
3) If yes, how can I avoid it?
4) Why exception handling (try finally
) or isActive
does not work?
Upvotes: 1
Views: 57
Reputation: 93521
Yes, you should avoid GlobalScope as it will make it clumsy to cancel jobs and prevent leaks. It is a singleton so it will outlive your Activity. The leak occurs because your launch
lambda is capturing a reference to your Activity by using properties of your Activity.
As a side note, isActive
is always true on your GlobalScope because you never canceled your GlobalScope, only one of its child jobs. And you likely wouldn't want to cancel GlobalScope if you were using it for more than one job at a time.
One of the easiest ways to use CoroutineScope is to attach it to your Activity as a delegate, so your Activity is its own scope, allowing you to directly call launch
. You just need to remember to cancel it in onDestroy()
so jobs won't outlive your Activity. (At least not by long, as long as you make your jobs cancellable--see below.)
class MyActivity: AppCompatActivity(), CoroutineScope by MainScope() {
//...
override fun onDestroy() {
super.onDestroy()
cancel() // cancel any jobs in this Activity's scope
}
}
Best practice is to always make your suspend
functions internally select the correct dispatcher for their operation. To make your suspend function cancellable, it must be something with exit points in it, which are usually calls to cancellation-checking suspend functions (like yield()
and delay()
and functions of your own that call them). You can alternatively check isActive
and return early if its false, which makes your function cancellable but usable like yield()
to exit another suspend function. So if there is a big for-loop, you can call yield()
in the loop to give it an exit point. Or if there are a bunch of sequential steps, put yield()
calls in between. The alternative is to keep checking the state of isActive
, which is a property of the context so you can just directly reference it from within your withContext
block.
// Just an example. You'll have to come up with a way to break up your calculation.
suspend fun solveEquation(param:Double): Double = withContext(Dispatchers.Default) {
val x = /** some calculation */
yield()
val y = /** some calculation with x */
yield()
var z = /** some calculation with y */
for (i in 0..1000) {
yield()
z += /** some calculation */
}
z
}
I'm not sure what you were trying to do with try/finally
. You just need to use those as you normally would when working with streams. You can put a yield()
inside the try
or use
block and be confident the finally
block will execute regardless of cancellation.
To make your job cancellable, I suggest using a variable that's null when it's not runnable:
var calcJob: Job? = null // Accessed only from main thread.
Then your button listener can be something like this:
btRunOrStop.onClickListener = {
// Cancel job if it's running.
// Don't want to join it because that would block the main thread.
calcJob?.let {
bStat.setText("")
it.cancel()
calcJob = null
} ?: run { // Create job if one didn't exist to cancel
calcJob = launch { // Called on the Activity scope so we're in the main UI thread.
bStat.setText("Solving ...")
mEv = MotionEvent.obtain(x,y,MotionEvent.ACTION_UP,0F,0F,0)
btRunOrStop.dispatchTouchEvent(mEv)
// You might want to check this doesn't trigger the onClickListener and
// create endless loop. (I don't know if it does.)
val result = solveEquation(someParam) // background calculation
// Coroutine resumes on main thread so we can freely work with "result"
// and update UI here.
calcJob = null // Mark job as finished.
}
}
}
So by following the practice of using = withContext(Dispatchers.Default /*or IO*/){}
to define your suspend functions, you can safely do sequential UI stuff everywhere else and the code in your launch
block will be very clean.
Upvotes: 1