Search code examples
androidkotlinkotlinx.coroutines

Coroutine context custom getter


I'm investigating kotlin coroutines related to Android after 1.0.0 release.

I found tons of examples of making scoped ViewModel (from arch components) with creating parent job and clearing it in onCleared or scoped Activity with job creation in onCreate and clearing in onDestroy (same with onResume and onPause). In some examples I meet this code structure (taken from official docs):

override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

Is this custom getter called all times, when we start a new coroutine from this scope? Isn't it bad? Maybe it would be better to keep single scope value instead of creating a new one every time?

[UPDATE]

I accept that solution, if we get rid of lateinit job and create it isntantly, but what if I want to do something like this (what should I do? Is this solution looks like correct or not?):

class LifecycleCrScope: CoroutineScope, LifecycleObserver {

  private var _job: Job? = null
  override val coroutineContext: CoroutineContext
    get() = job() + Dispatchers.Main


  fun job() : Job {
    return _job ?: createJob().also { _job = it }
  }

  fun createJob(): Job = Job() // or another implementation

  @OnLifecycleEvent(ON_PAUSE)
  fun pause() {
    _job?.cancel()
    _job = null
  }
}

Solution

  • I think you could run into issues with thread-safety when you supply a Job lazily like in your update because that code is evaluated from whatever thread happens to start a coroutine. The initial example on the other hand makes sure that the Job is setup from the Main thread and that it happens before other threads can be started in a typical android activity.

    You could achieve something similar to the initial example by creating the entire CoroutineContext at the start of the scope. That also removes the need to compute the final context for each started coroutine. For example:

    class LifecycleCrScope : CoroutineScope, LifecycleObserver {
    
        private var _ctx: CoroutineContext? = null
        override val coroutineContext: CoroutineContext
            get() = _ctx!! // throws when not within scope
    
        private fun startScope() {
            _ctx = Dispatchers.Main + Job()
        }
    
        private fun endScope() {
            _ctx!![Job]!!.cancel()  // throws only when misused, e.g. no startScope()
            _ctx = null
        }
    
        @OnLifecycleEvent(ON_RESUME) //  <-.
        fun resume() {               //    | Beware:
            startScope()             //    | symmetric but no scope
        }                            //    | during onCreate,
                                     //    | onStart, onStop, ...
        @OnLifecycleEvent(ON_PAUSE)  //  <-.
        fun pause() {
            endScope()
        }
    
    }
    

    Alternatively, if you don't like it to throw

    class LifecycleCrScope : CoroutineScope, LifecycleObserver {
        // initially cancelled. Jobs started outside scope will not execute silently
        private var _ctx: CoroutineContext = Dispatchers.Main + Job().apply { cancel() }
        ..
        private fun endScope() {
            _ctx[Job]!!.cancel()  // should never throw
        }
        ..