#目次
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):
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)
interface ProjectsView {
fun getProjects(projects: List<Project>)
}
interface ProjectsPresenter {
fun setView(projectsView: ProjectsView)
fun getData()
fun stop()
}
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.
@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()
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 :)
interface ProjectsView {
fun getProjects(projects: List<Project>)
}
interface ProjectsPresenter {
fun setView(projectsView: ProjectsView)
fun getData()
}
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() }
...
}
}
}
@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でもこちらで募集中