-
Notifications
You must be signed in to change notification settings - Fork 264
(提案)ActivityのViewModelのライフサイクルを用いてCoroutineのJobを管理する #578
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -2,21 +2,22 @@ package io.github.droidkaigi.confsched2019.sponsor.ui.actioncreator | |||
|
|
||||
| import androidx.lifecycle.Lifecycle | ||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||
| import io.github.droidkaigi.confsched2019.action.Action | ||||
| import io.github.droidkaigi.confsched2019.di.PageScope | ||||
| import io.github.droidkaigi.confsched2019.dispatcher.Dispatcher | ||||
| import io.github.droidkaigi.confsched2019.ext.android.coroutineScope | ||||
| import io.github.droidkaigi.confsched2019.model.LoadingState | ||||
| import io.github.droidkaigi.confsched2019.system.actioncreator.ErrorHandler | ||||
| import io.github.droidkaigi.confsched2019.data.repository.SponsorRepository | ||||
| import io.github.droidkaigi.confsched2019.util.ScreenLifecycle | ||||
| import io.github.droidkaigi.confsched2019.util.coroutineScope | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.launch | ||||
| import javax.inject.Inject | ||||
|
|
||||
| class SponsorActionCreator @Inject constructor( | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you do not make a scope here, another SponsorActionCreator will be created when the screen rotates, is it difficult to make the scope?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. これは難しそうなので、とりあえずなくてもいいかもですね。。 |
||||
| override val dispatcher: Dispatcher, | ||||
| private val sponsorRepository: SponsorRepository, | ||||
| @PageScope private val lifecycle: Lifecycle | ||||
| ) : CoroutineScope by lifecycle.coroutineScope, | ||||
| private val screenLifecycle: ScreenLifecycle | ||||
| ) : CoroutineScope by screenLifecycle.coroutineScope, | ||||
| ErrorHandler { | ||||
| fun load() = launch { | ||||
| try { | ||||
|
|
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package io.github.droidkaigi.confsched2019.di | ||
|
|
||
| import androidx.fragment.app.FragmentActivity | ||
| import androidx.lifecycle.ViewModelProviders | ||
| import dagger.Module | ||
| import dagger.Provides | ||
| import io.github.droidkaigi.confsched2019.ui.ScreenViewModel | ||
| import io.github.droidkaigi.confsched2019.util.ScreenLifecycle | ||
|
|
||
| @Module | ||
| object ScreenModule { | ||
|
|
||
| @JvmStatic @Provides fun provideScreenViewModel( | ||
| activity: FragmentActivity | ||
| ): ScreenViewModel { | ||
| return ViewModelProviders.of(activity).get(ScreenViewModel::class.java) | ||
| } | ||
|
|
||
| @JvmStatic @Provides fun provideScreenLifecycle( | ||
| screenViewModel: ScreenViewModel | ||
| ): ScreenLifecycle { | ||
| return screenViewModel.lifecycle | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package io.github.droidkaigi.confsched2019.ui | ||
|
|
||
| import androidx.lifecycle.ViewModel | ||
| import io.github.droidkaigi.confsched2019.util.ScreenLifecycle | ||
|
|
||
| class ScreenViewModel : ViewModel() { | ||
|
|
||
| val lifecycle: ScreenLifecycle = ScreenLifecycle() | ||
|
|
||
| init { | ||
| lifecycle.dispatchOnInit() | ||
| } | ||
|
|
||
| override fun onCleared() { | ||
| lifecycle.dispatchOnCleared() | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package io.github.droidkaigi.confsched2019.util | ||
|
|
||
| import androidx.annotation.IntDef | ||
|
|
||
| class ScreenLifecycle { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to make ScreenLifecycle a Lifecycle of AAC? |
||
|
|
||
| companion object { | ||
| const val NONE = 0 | ||
| const val INIT = 1 | ||
| const val CLEARED = 2 | ||
|
|
||
| @Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY) | ||
| @IntDef( | ||
| NONE, | ||
| INIT, | ||
| CLEARED | ||
| ) | ||
| annotation class State | ||
| } | ||
|
|
||
| @State | ||
| internal var state: Int = NONE | ||
|
|
||
| private val onInitHooks = HashSet<(() -> Unit)>() | ||
| private val onDestroyHooks = HashSet<(() -> Unit)>() | ||
|
|
||
| fun onInit(r: (() -> Unit)) { | ||
| onInitHooks.add(r) | ||
| if (state == INIT) { | ||
| r() | ||
| } | ||
| } | ||
|
|
||
| fun onCleared(r: (() -> Unit)) { | ||
| onDestroyHooks.add(r) | ||
| if (state == CLEARED) { | ||
| r() | ||
| } | ||
| } | ||
|
|
||
| fun dispatchOnInit() { | ||
| state = INIT | ||
| onInitHooks.forEach { it() } | ||
| } | ||
|
|
||
| fun dispatchOnCleared() { | ||
| state = CLEARED | ||
| onDestroyHooks.forEach { it() } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package io.github.droidkaigi.confsched2019.util | ||
|
|
||
| import io.github.droidkaigi.confsched2019.ext.android.Dispatchers | ||
| import kotlinx.coroutines.GlobalScope | ||
| import kotlinx.coroutines.Job | ||
| import kotlinx.coroutines.SupervisorJob | ||
| import kotlinx.coroutines.launch | ||
|
|
||
| /** | ||
| * This implementation refers to https://github.com/Kotlin/kotlinx.coroutines/pull/760 | ||
| */ | ||
| fun ScreenLifecycle.createJob( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. この実装の思想は「画面回転を跨いだActivity」が最長ライフサイクルである前提に立ってると思うんですが、実際には Application が最大であり、それに対応できていないこと、また Supervisor を使ってのViewModelのインスタンススコープから外れた Global な実装は取扱を間違えると memory leak しそうです。正確には、この実装が暗黙的なライフサイクルとコールバックチェーンに支えられていて、コードだけ見ても memory leak しないという自信がないです。 Twitter でちらっと見た感じの印象としては ViewModel が直接 CoroutineScope (と必要であれば Dagger2 のComponent) を管理するイメージでした。(最新の ktx alpha の方式)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
僕個人として、この考え自体には凄い賛成です。特にこのアプリはこの考えの恩恵をかなり受けられそうです。
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. これの方式との比較が気になるという感じですかね?確かにAACのライフサイクル化よりそちらを先に考えたほうがいいかもですねー |
||
| state: @ScreenLifecycle.Companion.State Int = ScreenLifecycle.NONE | ||
| ): Job { | ||
| require(state != ScreenLifecycle.CLEARED) { | ||
| "CLEARED is a terminal state that is forbidden for createJob(…), to avoid leaks." | ||
| } | ||
| return SupervisorJob().also { job -> | ||
| when (state) { | ||
| ScreenLifecycle.CLEARED -> job.cancel() | ||
| else -> GlobalScope.launch(Dispatchers.Main) { | ||
| onCleared { | ||
| job.cancel() | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package io.github.droidkaigi.confsched2019.util | ||
|
|
||
| import io.github.droidkaigi.confsched2019.ext.android.Dispatchers | ||
| import kotlinx.coroutines.CoroutineScope | ||
| import kotlinx.coroutines.Job | ||
| import java.util.concurrent.ConcurrentHashMap | ||
|
|
||
| private val cachedLifecycleCoroutineScopes = ConcurrentHashMap<ScreenLifecycle, CoroutineScope>() | ||
| private val cachedLifecycleJobs = ConcurrentHashMap<ScreenLifecycle, Job>() | ||
|
|
||
| val ScreenLifecycle.coroutineScope: CoroutineScope | ||
| get() = cachedLifecycleCoroutineScopes[this] ?: job.let { job -> | ||
| val newScope = CoroutineScope(job + Dispatchers.Main) | ||
| if (job.isActive) { | ||
| cachedLifecycleCoroutineScopes[this] = newScope | ||
| job.invokeOnCompletion { cachedLifecycleCoroutineScopes -= this } | ||
| } | ||
| newScope | ||
| } | ||
|
|
||
| val ScreenLifecycle.job: Job | ||
| get() = cachedLifecycleJobs[this] ?: createJob().also { | ||
| if (it.isActive) { | ||
| cachedLifecycleJobs[this] = it | ||
| it.invokeOnCompletion { cachedLifecycleJobs -= this } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.