diff --git a/feature/announcement/src/main/java/io/github/droidkaigi/confsched2019/announcement/ui/actioncreator/AnnouncementActionCreator.kt b/feature/announcement/src/main/java/io/github/droidkaigi/confsched2019/announcement/ui/actioncreator/AnnouncementActionCreator.kt index 3f214bb51..fbf0dbeeb 100644 --- a/feature/announcement/src/main/java/io/github/droidkaigi/confsched2019/announcement/ui/actioncreator/AnnouncementActionCreator.kt +++ b/feature/announcement/src/main/java/io/github/droidkaigi/confsched2019/announcement/ui/actioncreator/AnnouncementActionCreator.kt @@ -3,11 +3,12 @@ package io.github.droidkaigi.confsched2019.announcement.ui.actioncreator import androidx.lifecycle.Lifecycle import io.github.droidkaigi.confsched2019.action.Action import io.github.droidkaigi.confsched2019.data.repository.AnnouncementRepository -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.util.ScreenLifecycle +import io.github.droidkaigi.confsched2019.util.coroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject @@ -15,8 +16,8 @@ import javax.inject.Inject class AnnouncementActionCreator @Inject constructor( override val dispatcher: Dispatcher, private val announcementRepository: AnnouncementRepository, - @PageScope private val lifecycle: Lifecycle -) : CoroutineScope by lifecycle.coroutineScope, + private val screenLifecycle: ScreenLifecycle +) : CoroutineScope by screenLifecycle.coroutineScope, ErrorHandler { fun load() = launch { diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/actioncreator/SearchActionCreator.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/actioncreator/SearchActionCreator.kt index d4af0e031..a1d2db0fa 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/actioncreator/SearchActionCreator.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/actioncreator/SearchActionCreator.kt @@ -1,21 +1,21 @@ package io.github.droidkaigi.confsched2019.session.ui.actioncreator -import androidx.lifecycle.Lifecycle 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.SearchResult import io.github.droidkaigi.confsched2019.model.SessionContents import io.github.droidkaigi.confsched2019.system.actioncreator.ErrorHandler +import io.github.droidkaigi.confsched2019.util.ScreenLifecycle +import io.github.droidkaigi.confsched2019.util.coroutineScope import kotlinx.coroutines.CoroutineScope import javax.inject.Inject @PageScope class SearchActionCreator @Inject constructor( override val dispatcher: Dispatcher, - @PageScope private val lifecycle: Lifecycle -) : CoroutineScope by lifecycle.coroutineScope, + private val screenLifecycle: ScreenLifecycle +) : CoroutineScope by screenLifecycle.coroutineScope, ErrorHandler { fun search(query: String?, sessionContents: SessionContents) { // if we do not have query, we should show speakers and sessions diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/actioncreator/SessionContentsActionCreator.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/actioncreator/SessionContentsActionCreator.kt index def2acf6f..0e5c082f7 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/actioncreator/SessionContentsActionCreator.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/actioncreator/SessionContentsActionCreator.kt @@ -11,7 +11,9 @@ import io.github.droidkaigi.confsched2019.model.LoadingState import io.github.droidkaigi.confsched2019.model.Session import io.github.droidkaigi.confsched2019.session.R import io.github.droidkaigi.confsched2019.system.actioncreator.ErrorHandler +import io.github.droidkaigi.confsched2019.util.ScreenLifecycle import io.github.droidkaigi.confsched2019.util.SessionAlarm +import io.github.droidkaigi.confsched2019.util.coroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber @@ -22,9 +24,9 @@ import javax.inject.Inject class SessionContentsActionCreator @Inject constructor( override val dispatcher: Dispatcher, private val sessionRepository: SessionRepository, - @PageScope private val lifecycle: Lifecycle, + private val screenLifecycle: ScreenLifecycle, private val sessionAlarm: SessionAlarm -) : CoroutineScope by lifecycle.coroutineScope, +) : CoroutineScope by screenLifecycle.coroutineScope, ErrorHandler { fun refresh() = launch { try { diff --git a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/actioncreator/SessionPagesActionCreator.kt b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/actioncreator/SessionPagesActionCreator.kt index 248f67df6..07bc622d1 100644 --- a/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/actioncreator/SessionPagesActionCreator.kt +++ b/feature/session/src/main/java/io/github/droidkaigi/confsched2019/session/ui/actioncreator/SessionPagesActionCreator.kt @@ -13,14 +13,16 @@ import io.github.droidkaigi.confsched2019.model.Room import io.github.droidkaigi.confsched2019.model.Session import io.github.droidkaigi.confsched2019.model.SessionPage import io.github.droidkaigi.confsched2019.system.actioncreator.ErrorHandler +import io.github.droidkaigi.confsched2019.util.ScreenLifecycle +import io.github.droidkaigi.confsched2019.util.coroutineScope import kotlinx.coroutines.CoroutineScope import javax.inject.Inject @PageScope class SessionPagesActionCreator @Inject constructor( override val dispatcher: Dispatcher, - @PageScope private val lifecycle: Lifecycle -) : CoroutineScope by lifecycle.coroutineScope, + private val screenLifecycle: ScreenLifecycle +) : CoroutineScope by screenLifecycle.coroutineScope, ErrorHandler { fun load(sessions: List) { dispatcher.launchAndDispatch(Action.SessionsLoaded(sessions)) diff --git a/feature/sponsor/src/main/java/io/github/droidkaigi/confsched2019/sponsor/ui/actioncreator/SponsorActionCreator.kt b/feature/sponsor/src/main/java/io/github/droidkaigi/confsched2019/sponsor/ui/actioncreator/SponsorActionCreator.kt index 0969983ae..0151cd321 100644 --- a/feature/sponsor/src/main/java/io/github/droidkaigi/confsched2019/sponsor/ui/actioncreator/SponsorActionCreator.kt +++ b/feature/sponsor/src/main/java/io/github/droidkaigi/confsched2019/sponsor/ui/actioncreator/SponsorActionCreator.kt @@ -2,12 +2,13 @@ package io.github.droidkaigi.confsched2019.sponsor.ui.actioncreator import androidx.lifecycle.Lifecycle 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 @@ -15,8 +16,8 @@ import javax.inject.Inject class SponsorActionCreator @Inject constructor( 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 { diff --git a/feature/staff/src/main/java/io/github/droidkaigi/confsched2019/staff/ui/actioncreator/StaffSearchActionCreator.kt b/feature/staff/src/main/java/io/github/droidkaigi/confsched2019/staff/ui/actioncreator/StaffSearchActionCreator.kt index 23d8c6f0c..23efa87e1 100644 --- a/feature/staff/src/main/java/io/github/droidkaigi/confsched2019/staff/ui/actioncreator/StaffSearchActionCreator.kt +++ b/feature/staff/src/main/java/io/github/droidkaigi/confsched2019/staff/ui/actioncreator/StaffSearchActionCreator.kt @@ -10,6 +10,8 @@ import io.github.droidkaigi.confsched2019.model.LoadingState import io.github.droidkaigi.confsched2019.model.StaffContents import io.github.droidkaigi.confsched2019.model.StaffSearchResult import io.github.droidkaigi.confsched2019.system.actioncreator.ErrorHandler +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 @@ -18,8 +20,8 @@ import javax.inject.Inject class StaffSearchActionCreator @Inject constructor( override val dispatcher: Dispatcher, private val staffRepository: StaffRepository, - @PageScope private val lifecycle: Lifecycle -) : CoroutineScope by lifecycle.coroutineScope, ErrorHandler { + private val screenLifecycle: ScreenLifecycle +) : CoroutineScope by screenLifecycle.coroutineScope, ErrorHandler { fun load() = launch { try { diff --git a/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/di/ScreenModule.kt b/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/di/ScreenModule.kt new file mode 100644 index 000000000..095d33ef1 --- /dev/null +++ b/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/di/ScreenModule.kt @@ -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 + } +} diff --git a/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/ui/MainActivity.kt b/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/ui/MainActivity.kt index b56063c04..99ee10215 100644 --- a/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/ui/MainActivity.kt +++ b/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/ui/MainActivity.kt @@ -32,6 +32,7 @@ import io.github.droidkaigi.confsched2019.announcement.ui.AnnouncementFragment import io.github.droidkaigi.confsched2019.announcement.ui.AnnouncementFragmentModule import io.github.droidkaigi.confsched2019.databinding.ActivityMainBinding import io.github.droidkaigi.confsched2019.di.PageScope +import io.github.droidkaigi.confsched2019.di.ScreenModule import io.github.droidkaigi.confsched2019.ext.android.changed import io.github.droidkaigi.confsched2019.floormap.ui.FloorMapFragment import io.github.droidkaigi.confsched2019.floormap.ui.FloorMapFragmentModule @@ -247,7 +248,7 @@ abstract class MainActivityModule { @Module abstract class MainActivityBuilder { - @ContributesAndroidInjector(modules = [MainActivityModule::class]) + @ContributesAndroidInjector(modules = [MainActivityModule::class, ScreenModule::class]) abstract fun contributeMainActivity(): MainActivity } } diff --git a/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/ui/ScreenViewModel.kt b/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/ui/ScreenViewModel.kt new file mode 100644 index 000000000..7fe1c0259 --- /dev/null +++ b/frontend/android/src/main/java/io/github/droidkaigi/confsched2019/ui/ScreenViewModel.kt @@ -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() + } +} diff --git a/frontendcomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/util/ScreenLifecycle.kt b/frontendcomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/util/ScreenLifecycle.kt new file mode 100644 index 000000000..2a2f98ec4 --- /dev/null +++ b/frontendcomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/util/ScreenLifecycle.kt @@ -0,0 +1,50 @@ +package io.github.droidkaigi.confsched2019.util + +import androidx.annotation.IntDef + +class ScreenLifecycle { + + 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() } + } +} diff --git a/frontendcomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/util/ScreenLifecycleExt.kt b/frontendcomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/util/ScreenLifecycleExt.kt new file mode 100644 index 000000000..46590e6c9 --- /dev/null +++ b/frontendcomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/util/ScreenLifecycleExt.kt @@ -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( + 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() + } + } + } + } +} diff --git a/frontendcomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/util/cachedLifecycleCoroutineScopes.kt b/frontendcomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/util/cachedLifecycleCoroutineScopes.kt new file mode 100644 index 000000000..60d79676e --- /dev/null +++ b/frontendcomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/util/cachedLifecycleCoroutineScopes.kt @@ -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() +private val cachedLifecycleJobs = ConcurrentHashMap() + +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 } + } + }