-
Notifications
You must be signed in to change notification settings - Fork 1.9k
[Proposal] New "reusable" scope #874
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I wish to expose my considerations:
In my opinion this is not a workaround, if the
We should does not assume that the
A |
Thanks @fvasco. Very correct reasoning!
Well, yes, but it might be. My thought is every object defines some kind of a scope, isn't it? So, if this object can run coroutines, then it must define a coroutine scope for them.
You're right that all closeables must be handled.
Please let me disagree here. It IS cancellable in the real sense. As soon as it's canceled, all this job's children are cancelled in this case, too.
|
Yes, I agree, but we have (unfortunately) apply the same rule to
We should take care to reopen a valid scope (ie: without reusing some authentication credential), creating a
It is a trick
debugger/logger sometimes invoke |
I'm a bit confused here. Whose this
Do you mean it inherits all the extensions to
Do you mean that class MyPresenter<MyView>: CoroutineScope by ReusableCoroutineScope() {
//...
fun takeView(v: V) { /* do some IO stuff */ } // enter scope
fun dropView(v: V) {
cancelCoroutineScope()
/* release other resources */
} // exit scope
}
Yes, in this case the job will be recreated, but how bad is this if it's empty, very cheap and does nothing? |
Hi, @fvasco I had some time to reconsider the initial idea and came up to the following changes.
So, my original vision evolved into the following: public interface ReusableCoroutineScope : CoroutineScope { // (1)
public val isRestarted: Boolean // (2)
public fun restartCoroutineContext() // (3)
} public val CoroutineScope.isReusable: Boolean get() = this is ReusableCoroutineScope // (4) internal class ReusableContextScope(
context: CoroutineContext,
private val newJob: () -> Job
) : ReusableCoroutineScope {
private var _isRestarted = false
private var _reusableContext: CoroutineContext = context
override val isRestarted: Boolean get() = _isRestarted
override val coroutineContext: CoroutineContext
get() {
if (_reusableContext[Job]!!.isCancelled) {
restartCoroutineContext()
}
return _reusableContext
}
override fun restartCoroutineContext() {
_reusableContext[Job]!!.cancel()
_reusableContext += newJob()
_isRestarted = true
}
} |
Hi @DmitriyZaitsev
It is
I consider this a secondary problem.
This can be tricky to understand, especially if you are looking for a bug.
Why this is useful?
Is the Finally I consider preferable the "workarounds" than your "solution". |
Hi @fvasco ,
If getter does not recreate the job, internal class RestartableContextScope(
context: CoroutineContext,
private val newJob: () -> Job
) : RestartableCoroutineScope {
private var _isRestarted = false
private var _coroutineContext: CoroutineContext = context
override val isRestarted: Boolean get() = _isRestarted
override val coroutineContext: CoroutineContext get() = _coroutineContext
override fun restart() {
_coroutineContext[Job]!!.cancel()
_coroutineContext += newJob()
_isRestarted = true
}
}
With the fix above, we don't have a bug.
This is useful to know whether the scope already been canceled and restarted.
"solution" is the "workaround" encapsulated in the library. Finally, we can have class MyObject: RestartableCoroutineScope by RestartableMainScope() {
fun onEnter() {
if (!isActive) restart()
// do some stuff, launch coroutines, etc
}
fun onExit() { cancelCoroutineScope() }
// ...
} or class MyObject {
private val scope = RestartableMainScope()
fun onEnter() {
if (!scope.isActive) scope.restart()
// do some stuff, launch coroutines, etc
}
fun onExit() { scope.cancelCoroutineScope() }
// ...
} |
I think we should consolidate the effort around #760. Proposed there syntax: class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.coroutineScope.launch {
someSuspendFunction()
someOtherSuspendFunction()
someCancellableSuspendFunction()
}
}
}
|
I don't think that reusable scope is a good fit for
This change is not worth it and carries more risks than value. |
@qwwdfsad fair enough. thanks for your comment! |
Use case (the problem)
Unlike Activities that after
onDestroy()
become really dead and garbage collected, fragments have a different lifecycle and can stay physically alive afteronDestroy()
/onDetach()
, for example cached for future use.So, when we get an instance of such fragment that survived after destruction, we can no longer launch any coroutine in because the parent job is already canceled.
Naturally, presenters can be considered as coroutine scopes too.
But in practice, a presenter can be used with different views, and its scope is likely determined by the lifecycle of the view attached to it - a lifetime in the interval between the presenter's
takeView()
anddropView()
.Presenter drops view when a user rotates device or starts another activity etc. At the same time, the presenter should stop all running tasks. If it cancels coroutine scope once, we become unable to launch coroutines again.
ViewModel
, and I'm sure you can come up with your own examples.Workarounds
CoroutineScope
itself.One can create the coroutine scope inside a fragment/presenter and manage it explicitly by hands.
This might be error prone and requires writing some boilerplate code. But we all know that less code == less bugs.
The use of
coroutineScope[Job]?.cancelChildren()
instead ofcoroutineScope[Job]?.cancel()
partially solves the issue: we get all jobs canceled except the parent.Solution
Introduce new
ReusableCoroutineScope
.One defines a
newJob()
function that's going to be used as a generator of new jobs in cases when the coroutineContext's job became canceled.Also, inspired by #829, we can also add an alternative
ReusableMainScope()
factory function that will create a scope withDispatchers.Main
and re-inject newSupervisorJob
to the context.Benefits
@elizarov, @qwwdfsad, what do you think?
The text was updated successfully, but these errors were encountered: