Would it seem reasonable to add `object InactiveSc...
# coroutines
m
Would it seem reasonable to add
object InactiveScope
the standard library, which would be a
CoroutineScope
instance with an job always in a cancelled state? I think this would be useful in cases when we have a scope var which isn’t initialized at object creation, so we could use this object as a placeholder rather than using a null value. I’m thinking about scopes tied to Android activity states, i.e.
activityCreatedScope
,
activityStartedScope
,
activityResumedScope
. Currently the solutions are to use
lateinit
or nullable
var
.
s
I think your Activity/Fragment, or even better your ViewModel should just implement CoroutineScope. Then you can assign a real or an ‘initially-cancelled’ Job to its
coroutineContext
.
m
There’s more than one scope in this case, because different tasks are linked to different activity states
s
I see. Even in that case, I suggest not to use an already implemented scope, like InactiveScope. Instead, use ViewModel like object that get recreated each onCreate, onStart, onResume and cleared/destroyed/canceled each onPause, onStop, onDestroy and that object implements
CoroutineScope
. But, in a similar way, an
object CancelledJob : Job { ... }
could help here, that can be initially assigned to these objects’ `coroutineContext`s.
m
The Android use-case is just an example. I’m suggesting a singleton scope which can be used as a placeholder in places where there’s a mutable scope instance which isn’t initialized immediately. I think that’s what you’re saying in your second paragraph, but I’m not sure. I also don’t see why you want to bring in
ViewModel
-like objects for tasks which are linked to the activity lifecycle, given that the
ViewModel
itself is precisely designed for taks which outlive the activity lifecycle. Maybe it’s clearer if I show a current implementation:
Copy code
open class CoroutineActivity : AppCompatActivity() {

    private var _activityCreatedScope: CoroutineScope? = null
    val activityCreatedScope: CoroutineScope
        get() = _activityCreatedScope!!
    private var _activityStartedScope: CoroutineScope? = null
    val activityStartedScope: CoroutineScope
        get() = _activityStartedScope!!
    private var _activityResumedScope: CoroutineScope? = null
    val activityResumedScope: CoroutineScope
        get() = _activityResumedScope!!

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _activityCreatedScope = MainScope()
    }

    override fun onStart() {
        super.onStart()
        _activityStartedScope = MainScope()
    }

    override fun onResume() {
        super.onResume()
        _activityResumedScope = MainScope()
    }

    override fun onPause() {
        super.onPause()
        activityResumedScope.cancel()
        _activityResumedScope = null
    }

    override fun onStop() {
        super.onStop()
        activityStartedScope.cancel()
        _activityStartedScope = null
    }

    override fun onDestroy() {
        super.onDestroy()
        activityCreatedScope.cancel()
        _activityCreatedScope = null
    }
}
This would be simpler if the `var`s for the scope could simply be initially assigned with a
InactiveScope
e
I fear that that would work to shuffle various bugs under the rug. You see, when you try to
launch
a coroutine in a scope that is not active it does not start silently. It is hard to notice. But when you try to read a
lateinit var
that was not initialized yet, then you get exception. Much better way to catch bugs.
2
s
My idea of bringing in ViewModel like object is to have those objects be responsible for launch tasks (
launch
,
async
) and never the Activity. The Activity then no longer has to worry about structured concurrency.
Copy code
open class CoroutineActivity : AppCompatActivity() {

    private lateinit var _vmCreatedScope: MyViewModelWithScope
    private lateinit var _vmStartedScope: MyViewModelWithScope
    private lateinit var _vmResumedScope: MyViewModelWithScope

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _vmCreatedScope = MyViewModelWithScope()
    }

    override fun onStart() {
        super.onStart()
        _vmStartedScope = MyViewModelWithScope()
    }

    override fun onResume() {
        super.onResume()
        _vmResumedScope = MyViewModelWithScope()
    }

    override fun onPause() {
        super.onPause()
        vmResumedScope.cancel()
    }

    override fun onStop() {
        super.onStop()
        vmStartedScope.cancel()
    }

    override fun onDestroy() {
        super.onDestroy()
        vmCreatedScope.cancel()
    }
}
And your logic is in your
MyViewModelWithScope
(they could be different classes for each state, if need be; in this example, they are the same instance type) and your
MyViewModelWithScope
implements
CoroutineScope
m
@elizarov Ok so maybe a
CancelledScope
would be better, rather than inactive. This way the behavior is the same before the lifecycle reaches an “active” state and after it exits this “active” state.
e
Same problem, though. CancelledScope is just as tricky.
g
Probably something like this would help in your case and avoid subtle bugs when you try to start coroutine in wrong state:
Copy code
object UninitializedScope : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = error("Scope is not initialized")
}
👍 1
I never tried this approach, just an idea
But this is better than lateinit only for components that may be destroyed and recreated again, like Android Fragment’s view, I prefer to use own scope for view because it has own lifecycle, different from parent Fragment one