https://kotlinlang.org logo
Title
m

Marc Knaup

04/20/2020, 2:00 PM
Oh, looks like that we must not pass a
Job
to
launch
as it breaks parent/child-relationship 😅 We’ve got to take the
Job
returned by
launch
instead.
s

streetsofboston

04/20/2020, 2:07 PM
It could even be argued not to have a
cancel
method as well, for a common MyTask. Cancelling the CourotineScope should suffice. Instead, a special MyCancellableTask should offer this extra
cancel
functionality, to indicate that this (calling
cancel
) should be used with some care.
m

Marc Knaup

04/20/2020, 2:07 PM
Thanks! Even after using coroutines for months now I still cannot fully wrap my head around how to implement some basic things properly. Like for example there’s no easy way to create a child coroutine scope without directly invoking code in it. The worst thing is that most approaches “somehow work”, so the resulting errors are tricky to spot.
@streetsofboston true, not every task is cancellable. A separate sub-interface makes sense. Cancelling the coroutine scope doesn’t make sense. The task is just one operation in the scope and I don’t want to cancel others.
Also, afaik all that
cancel()
on a coroutine scope does is to call
cancel()
on the
Job
of the coroutine scope’s context.
s

streetsofboston

04/20/2020, 2:11 PM
If the cancellation of your task is related to an object ending its lifecycle (eg the object, that is the target of the data of your task, goes away), you should create a CoroutineScope for that object.
You want to avoid managing the cancellation of all MyTasks that you launch….
m

Marc Knaup

04/20/2020, 2:12 PM
But that parent-child relationship is already established when I launch my task?
cancel()
is just for canceling the task if the parent is still alive.
s

streetsofboston

04/20/2020, 2:13 PM
If the parent is still alive, what are the use cases for canceling a MyTask?
m

Marc Knaup

04/20/2020, 2:14 PM
Whatever the task is doing is no longer relevant?
s

streetsofboston

04/20/2020, 2:14 PM
When is it no longer relevant?
m

Marc Knaup

04/20/2020, 2:15 PM
Well that depends on the use case. A current example is that I can stop a validation process if whatever is being validated has been removed.
s

streetsofboston

04/20/2020, 2:15 PM
Sorry to ask these questions, but in most use-cases you don’t need to cancel a launch’s Job. The CoroutineScope should handle it.
Ah… that ‘whatever’ should have its own CoroutineScope. When that is being removed, it should call ‘cancel’ on its CoroutineScope.
m

Marc Knaup

04/20/2020, 2:16 PM
So I should potentially create a crazy amount of coroutine scopes for all kinds of objects that may be used in any long-running coroutine?!
s

streetsofboston

04/20/2020, 2:17 PM
Yup. A CoroutineScope is just an interface around a CoroutineContext that has a Job.
It is very lightweight.
m

Marc Knaup

04/20/2020, 2:18 PM
So every
User
,
Message
,
Post
- whatever business model I have - should have a
CoroutineScope
? 😮
s

streetsofboston

04/20/2020, 2:18 PM
No. But the target of that User, Message, Post should have its own CoroutineScope.
m

Marc Knaup

04/20/2020, 2:18 PM
What “target”?
s

streetsofboston

04/20/2020, 2:19 PM
E.g. a screen/UI, or a cache tied to a User, a database,
m

Marc Knaup

04/20/2020, 2:19 PM
Yeah that’s clear. But what if there is no natural target? The data I’m validating belongs to an app’s
AuthticationManager
which has app-lifetime.
I guess in that case I could subdivide the lifecycle further in signed-out and signed-in.
But even then the lifetime of what needs to be validated (some sign-in credentials) may only need validation for a fraction of the lifetime of the signed-out state.
s

streetsofboston

04/20/2020, 2:22 PM
Yes, but then you don’t need to cancel it, i think. Just let the sign-in run its course. The AuthenticationManager will always be there to receive the sign-in result.
m

Marc Knaup

04/20/2020, 2:22 PM
I need to cancel it if the sign-in is complete (that’s easy as sign-out lifecycle ends) but also if the sign-in fails due to invalid credentials (in some cases), in which case a validation is no longer needed.
s

streetsofboston

04/20/2020, 2:24 PM
On a failure of a Coroutine, if a child-coroutine fails or gets cancelled, all its sibling-coroutines fail/get cancelled. No need to cancel it manually
m

Marc Knaup

04/20/2020, 2:25 PM
There is no coroutine failure.
s

streetsofboston

04/20/2020, 2:25 PM
You said that when the ‘sign-in’ fails….
m

Marc Knaup

04/20/2020, 2:25 PM
Yes, that doesn’t mean that any coroutine will fail.
Also, only a subset of potential sign-in error causes must cause the validation task to be stopped.
s

streetsofboston

04/20/2020, 2:26 PM
But the Coroutine in which the failed sign-in runs will fail….
m

Marc Knaup

04/20/2020, 2:26 PM
Why, if I catch it and report the sign-in error to the UI?
Also, the validation is bound to the signed-out lifecycle, not the sign-in process.
s

streetsofboston

04/20/2020, 2:28 PM
Ah, the UI is the target of all your asynchronous code. Then your UI (screen) is the CoroutineScope (having a SupervisorJob).
m

Marc Knaup

04/20/2020, 2:28 PM
No, while signed out there is not necessarily a UI.
sign-in/sign-out lifecycle and UI lifecycle have some overlaps. That’s about it. None is child of the other and cannot be either 🤔
They describe totally different things and lifecycles
s

streetsofboston

04/20/2020, 2:31 PM
Ah… they have overlapping lifecycles. I wrote a blog post a while ago about lifecycles and how to tie them to CoroutineScopes. The overlapping of CoroutineScope (switching between them) is also mentioned toward the end of it. https://medium.com/swlh/how-can-we-use-coroutinescopes-in-kotlin-2210695f0e89
m

Marc Knaup

04/20/2020, 2:32 PM
Thanks, I’ll check it out
s

streetsofboston

04/20/2020, 2:33 PM
In the beginning it is tricky to get our heads around Structured Concurrency. But when it clicks, it is a very nice system 🙂 Often, when your code starts managing Jobs and Cancellation explicitly, it is often (but not always!) a code-smell that may be able to get solved by Structured Concurrency.
m

Marc Knaup

04/20/2020, 2:34 PM
In an ideal world I agree 🙂 But I guess we’re still a long way to get there.
Flow
is a good step forward though!
With UI it’s quite easy as there is a direct relationship between how long you see the UI component and the related coroutine scope. It’s already a little tricky on Android though because depending on the use-case you may need create…destroy lifecycle, start…stop lifecycle or resume…pause lifecycle.
s

streetsofboston

04/20/2020, 2:35 PM
Flow uses Structure Concurrency too. It is like a
suspend
function, with respect to Structure Concurrency, but it can produce multiple values instead of just one 🙂
Don’t tie the onPause/onResume to a lifecycle (CoroutineScope). That is just paused and resumed. Instead, tie a lifecycle/CoroutineScope to the lifecycle of a ViewModel; starts on the first creation of an Activity, dies on the onDestroy with ‘isFinished() == true’ (that is when the ViewModel’s onCleared is called)
m

Marc Knaup

04/20/2020, 2:39 PM
That’s dangerous though. I may keep network and other background events running even when the entire app is in background. The activity isn’t destroyed then, only stopped. I had that problem just recently 😅
s

streetsofboston

04/20/2020, 2:41 PM
Yup, that is true. But what happened, what went wrong when the app was in the background?
m

Marc Knaup

04/20/2020, 2:41 PM
It kept polling the server every 10 seconds for data that the user isn’t even able to see as the Activity is stopped.
And for a chat I have tied the “automatically marking messages as read” logic (in a chat) to the resume…pause lifecycle of an Activity. Messages shouldn’t be marked as read if the Activity is merely started but not resumed. Or worse - not visible at all.
s

streetsofboston

04/20/2020, 2:43 PM
Ah… that is different than a one-shot ‘get-me-some-data-from-the-network-and-then-show-it’ case. Having a Job for this use-case would indeed make sense.
m

Marc Knaup

04/20/2020, 2:43 PM
Even in a one-shot I expect network requests to be canceled when the user stops seeing the Activity.
s

streetsofboston

04/20/2020, 2:44 PM
For a one-shot, canceling the ViewModel’s CoroutineScope is sufficient for when the Activity goes away.
But if the Activity is just in the background, why cancel the request? The user may come back 2 seconds later…
m

Marc Knaup

04/20/2020, 2:45 PM
Would Android create a new scope in
onStart
? Also, my code would have to know that whatever has been set up in
onCreate
that’s bound to the scope has to be set up again in
onStart
, which doesn’t seem right.
I’m messing with a well-defined lifecycle then
For just one specific task within that scope
s

streetsofboston

04/20/2020, 2:46 PM
Why cancel in the onStop? Your Activity’s views are still there and when the user comes back, they will see the data from the request. Tie the scope to a ViewModel; starts at the initial creation of the Activity, stops on the final destruction of it (onDestroy with isFinished==true)
m

Marc Knaup

04/20/2020, 2:46 PM
Child scopes make more sense resume…pause is a child of start…stop is a child of create…destroy
s

streetsofboston

04/20/2020, 2:47 PM
That could work, but for one-shot requests, why cancel it when the Activity goes into the background? The user could come back. Also, when the phone rotates, onStop is called and you don’t want to cancel the request.
m

Marc Knaup

04/20/2020, 2:47 PM
It doesn’t make sense to waste resources when the app is in background. Depending on the network layer and use case the app may be busy for minutes in the background, including retries and related heavy duty.
Good thing is that I don’t support rotation for now 😄 Isn’t AndroidX’s lifecycle observation supposed to prevent such temporary lifecycle hick-ups due to configuration changes?
Otherwise some logic may be necessary to prevent split-second lifecycle deaths and just keep it alive.
s

streetsofboston

04/20/2020, 2:49 PM
When the app is in the background for a while, the OS will pause the app (starve it of CPU cycles). Often, there is no need to manage that yourself. The AndroidX’s ViewModel is used to prevent such hick-ups. The ViewModel’s lifecycle survives config-changes. But it remains active when the app/Activity is just in the background.
m

Marc Knaup

04/20/2020, 2:50 PM
I don’t rely on the OS to reduce my apps unnecessary activity. Also, the more resources the app uses in the background the worse it may be downgraded by the OS for that.
s

streetsofboston

04/20/2020, 2:50 PM
Don’t over-complicate it 🙂 Create your ViewModel for your Activity. Use the ViewModel’s CoroutineScope (it has one of its own), to launch network requests and such.
m

Marc Knaup

04/20/2020, 2:51 PM
Nope, no background network requests for us. It also slows down the app. If you leave a Activity with a heavy API request on a bad connection and go the next Activity, the user will have to wait longer because previous API request(s) are still consuming network resources.
s

streetsofboston

04/20/2020, 2:53 PM
Yup, if you requests are very heavy, then yes, canceling it sooner can be a good thing to do.
m

Marc Knaup

04/20/2020, 2:53 PM
No matter how I turn it, network requests to deliver data for anything but what the user is currently interested in is a bad idea. The only exception is if the entire synchronization logic is centralized and independent of UI, like in WhatsApp for example.
Even non-heavy requests can be bad on a mobile connection. We’re talking long delays + multiple retries here :)
s

streetsofboston

04/20/2020, 2:57 PM
Another way of doing this without managing Jobs of individual requests is to ‘finish’ the activity when moving to another activity. That would kill the ViewModel, cancel its CoroutineScope, cancelling all outstanding requests. Rotation/config-changes would not cause requests to be cancelled.
m

Marc Knaup

04/20/2020, 2:57 PM
afaik that would mess with the back stack
And would be a workaround for a lifecycle which isn’t the right one in the first place 🙂
If it’s clearly defined like “network requests live as long as the Activity is visible” then start…stop is that exact lifecycle
💯 1
Or my (weird?) approach of having a
val reader: Flow<Nothing>
which causes new messages in a chat conversation to be marked as read automatically while the Flow has any consumers. I subscribe to it in the related Activity in the resume…pause lifecycle.
I wasn’t sure what the best approach is to implement that, but
Flow<Nothing>
works 😅
It’s that weird 🙂
s

streetsofboston

04/20/2020, 3:01 PM
Is that (the Flow<Nothing>) to keep the message flow alive?
Haha… cool trick, but it seems to work 🙂
m

Marc Knaup

04/20/2020, 3:03 PM
No, it doesn’t affect message delivery. It only causes the Flow-generating side to mark messages as read while the Flow is being collected.
Collection is here 🙂
It’s a project I’ve taken over which is based on MVP approach. No MVVM yet 😕
s

streetsofboston

04/20/2020, 3:04 PM
I mean, you keep the Flow alive, ie. it remains active in what it is doing (mark incoming messages as read) while the collector is not interested in the Flow’s emissions.
m

Marc Knaup

04/20/2020, 3:04 PM
But I was happy to kick out that stupid RxJava for Flow!
💯 1
I mean, you keep the Flow alive, ie. it remains active in what it is doing (mark incoming messages as read) while the collector is not interested in the Flow’s emissions.
Exactly
But it doesn’t feel right as there’s no data that actually “flows”.
I guess I need something like
conversationCoordinator.markAsReadWhileScopeIsAlive(this)
s

streetsofboston

04/20/2020, 3:06 PM
Sorry, need to go to a meeting 🙂 Great chatting with you!
m

Marc Knaup

04/20/2020, 3:06 PM
u2, have fun
Regarding task cancelation - I just have another use case in front of me: When clicking on a message attachment, a task starts that downloads the attachment and then opens the UI for it. That one needs to be canceled when the Activity stops. But it also needs to be canceled when the user has clicked on another attachment in the meantime. That is not related to any lifecycle and thus requires a manual cancel.