Skip to content

[Proposal] New "reusable" scope #874

Closed
@DmitriyZaitsev

Description

@DmitriyZaitsev

Use case (the problem)

  1. Some components, e.g. Android Fragments can be reused after it's "destruction".
    Unlike Activities that after onDestroy() become really dead and garbage collected, fragments have a different lifecycle and can stay physically alive after onDestroy()/onDetach(), for example cached for future use.
class MyFragment : Fragment(), CoroutineScope by MainScope() {
    // ...
    override fun onDestroy() {
        super.onDestroy()
        cancelCoroutineScope()
        // ...
    }
}

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.


  1. Those who use MVP pattern in their apps, often inject Presenters as singletons that live as long as the entire app lives.
class MyPresenter<V> {
    // ...
    fun takeView(v: V) { /*...*/ }
    fun dropView(v: V) { /*...*/ }
}

Naturally, presenters can be considered as coroutine scopes too.

class MyPresenter<MyView>: CoroutineScope by MainScope() {
    //...
    fun takeView(v: V) { /*...*/ } // enter scope
    fun dropView(v: V) { cancelCoroutineScope() } // exit scope
}

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() and dropView().
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.


  1. The same goes for ViewModel, and I'm sure you can come up with your own examples.

Workarounds

  1. Do not implement CoroutineScope itself.
    One can create the coroutine scope inside a fragment/presenter and manage it explicitly by hands.
class MyPresenter<MyView> {
    private var myScope: CoroutineScope? = null
    fun takeView(v: V) { buildCoroutineScope() } // enter scope
    fun dropView(v: V) { cancelCoroutineScope() } // exit scope
}

This might be error prone and requires writing some boilerplate code. But we all know that less code == less bugs.


  1. Do not cancel the parent job, but it's children.
class MyPresenter<MyView>: CoroutineScope by MainScope() {
    //...
    fun takeView(v: V) { /*...*/ } // enter scope
    fun dropView(v: V) { cancelChildrenJobs() } // exit scope
}

The use of coroutineScope[Job]?.cancelChildren() instead of coroutineScope[Job]?.cancel() partially solves the issue: we get all jobs canceled except the parent.

Solution

Introduce new ReusableCoroutineScope.

internal class ReusableContextScope(
    context: CoroutineContext,
    private val newJob: () -> Job
) : CoroutineScope {
    private var reusableContext: CoroutineContext = context

    override val coroutineContext: CoroutineContext
        get() {
            if (reusableContext[Job]?.isCancelled == true) {
                reusableContext += newJob()
            }
            return reusableContext
        }
}

@Suppress("FunctionName")
fun ReusableCoroutineScope(
    context: CoroutineContext,
    newJob: () -> Job = { SupervisorJob() }
): CoroutineScope =
    ReusableContextScope(context, newJob)

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 with Dispatchers.Main and re-inject new SupervisorJob to the context.

@Suppress("FunctionName")
fun ReusableMainScope(): CoroutineScope =
    ReusableContextScope(Dispatchers.Main) { SupervisorJob() }

Benefits

  1. With this reusable scope, we can have a shiny nice short record for sensitive components.
class MyFragment : Fragment(), CoroutineScope by ReusableMainScope() {
    // ...
    override fun onDestroy() {
        super.onDestroy()
        cancelCoroutineScope()
    }
}
  1. We don't care about managing jobs
  2. We have safe code
  3. Our coroutines still don't leak
  4. If we need to reuse some canceled/destroyed component, we just take and use it like if it were a fresh new object.

@elizarov, @qwwdfsad, what do you think?

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions