0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Binding Coroutine Job, SupervisorJob and viewModelScope to Android Lifecycle

Last updated at Posted at 2022-02-01

#目次
0.はじめに;
1.TLDR
[2.Coroutine Job](#2-Coroutine Job)
3.SupervisorJob
4.viewModelScope
5.おわりに

#0-はじめに
株式会社オプティマインドのGoです!

When developing android apps, it is often common for beginners to be confused about how the kotlin coroutine integrated with android lifecycle. In this article I will explain the relationship between Coroutine Job, SupervisorJob and viewModelScope using a simple example (I will use Hilt as DI tool as an example. You can of course fit it to any favorate DI tools).

#1-TLDR;
viewModelScope は SupervisorJob() と Dispatchers.Main.immediateの結合したものです。

According to the following code from AOSP(Android Open Source Project):

lifecycle/lifecycle-viewmodel-ktx/src/main/java/androidx/lifecycle/ViewModel.kt
package androidx.lifecycle

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import java.io.Closeable
import kotlin.coroutines.CoroutineContext

private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"

/**
 * [CoroutineScope] tied to this [ViewModel].
 * This scope will be canceled when ViewModel will be cleared, i.e [ViewModel.onCleared] is called
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
 */
public val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }

internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context

    override fun close() {
        coroutineContext.cancel()
    }
}

#2-Coroutine Job
To explain how Coroutine Job could be binding with android lifecycle, I will demonstrate a demo MVP(Model View Presenter) example. (The reason for choosing MVP is only for simplicity and you can see how the lifecycle is binding with coroutine easily)

ProjectsView.kt
interface ProjectsView {

  fun getProjects(projects: List<Project>)
}
ProjectsPresenter.kt
interface ProjectsPresenter {

  fun setView(projectsView: ProjectsView)

  fun getData()

  fun stop()
}
ProjectsPresenterImpl.kt
class ProjectsPresenterImpl(private val repo: ProjectsRepo) : ProjectsPresenter
    ,CoroutineScope 
{
  private val parentJob = Job()
  private lateinit var projectsView: ProjectsView

  override fun setView(projectsView: ProjectsView) {
    this.projectsView = projectsView
  }

  override fun getData() {
    launch {
      val result = runCatching { repo.getProjects() }   
      ...
    }
  }

  override fun start() {
    if(!parentJob.isActive){
       parentJob = Job()
    }
  }
  override fun stop() {
    parentJob.cancel()
  }

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


}

Note in the above example, the parent Job and all its child jobs are cancelled whenever stop() is called and that is why we need to check and start a new Job when start() is called.

ProjectsActivity.kt
@AndroidEntryPoint 
class ProjectsActivity : AppCompatActivity(), ProjectsView {

  @Inject lateinit var presenter: ProjectsPresenter

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    presenter.setView(this)
  }

  override fun getProjects(projects: List<Project>) {
    TODO("Do some thing with the received data")
  }

  override fun onStop() {
    presenter.stop()
    super.onStop()
  }

  override fun onStart() {
    super.onStart()
    presenter.getData()
  }
}

#3-SupervisorJob

With a special Job which is called SupervisorJob will not get cancelled even if its child Job cancel. Therefore with the following simple modification, we can remove the code in onStart() and only cancelChildren is enough when onStop()

ProjectsPresenterImpl.kt
class ProjectsPresenterImpl(private val repo: ProjectsRepo) : ProjectsPresenter,
    ProjectsPresenter 
{
  private val parentJob = SupervisorJob()
  private lateinit var projectsView: ProjectsView

  override fun setView(projectsView: ProjectsView) {
    this.projectsView = projectsView
  }

  override fun getData() {
    launch {
      val result = runCatching { repo.getProjects() }   
      ...
    }
  }

  override fun start() {
  }

  override fun stop() {
    parentJob.cancelChildren()
  }

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


}

#4-viewModelScope

According to definitions of viewModelScope, we only need to use viewModelScope.launch and that's it :)

ProjectsView.kt
interface ProjectsView {

  fun getProjects(projects: List<Project>)
}
ProjectsPresenter.kt
interface ProjectsPresenter {

  fun setView(projectsView: ProjectsView)

  fun getData()
}
ProjectsPresenterImpl.kt
class ProjectsPresenterImpl(private val repo: ProjectsRepo) : ProjectsPresenter,
    ViewModel()
{

  private lateinit var projectsView: ProjectsView

  override fun setView(projectsView: ProjectsView) {
    this.projectsView = projectsView
  }

  override fun getData() {
    viewModelScope.launch {
      val result = runCatching { repo.getProjects() }   
      ...
    }
  }
}
ProjectsActivity.kt

@AndroidEntryPoint
class ProjectsActivity : AppCompatActivity(), ProjectsView {

  private val presenter by viewModels<ProjectsPresenterImpl>()

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    presenter.setView(this)
  }

  override fun getProjects(projects: List<Project>) {
    TODO("Do some thing with the received data")
  }


  override fun onStart() {
    super.onStart()
    presenter.getData()
  }
}

When ever you come to a situation to bind lifecycle with coroutine scope, viewModelScope is also the first choice.

#5-おわりに

ここまで読んでいただきありがとうございました。

株式会社オプティマインドでは、一緒に働く仲間を大募集中です。
カジュアル面談も大歓迎ですので、気軽にお声がけください。

【エンジニア領域の募集職種】
●ソフトウェアエンジニア
●QAエンジニア
●Androidアプリエンジニア
●組合せ最適化アルゴリズムエンジニア
●経路探索アルゴリズムエンジニア
●バックエンドエンジニア
●インフラエンジニア
●UXUIデザイナー

【ビジネス領域の募集職種】
●セールスコンサルタント
●採用・人事

『Loogiaってどんなサービス?』については、こちら

『オプティマインドってどんな会社?』については、こちら

Wantedlyでもこちらで募集中

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?