diff --git a/buildSrc/src/main/java/dependencies/Dep.kt b/buildSrc/src/main/java/dependencies/Dep.kt index d121828f8..35e3d04b3 100644 --- a/buildSrc/src/main/java/dependencies/Dep.kt +++ b/buildSrc/src/main/java/dependencies/Dep.kt @@ -49,6 +49,7 @@ object Dep { val fragment = "androidx.fragment:fragment:1.1.0-alpha03" val lifecycleExtensions = "androidx.lifecycle:lifecycle-extensions:2.0.0" + val viewModelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:2.0.0" val lifecycleLiveData = "androidx.lifecycle:lifecycle-livedata:2.0.0" object Room { diff --git a/corecomponent/androidcomponent/build.gradle b/corecomponent/androidcomponent/build.gradle index e3f0e0148..b6a877d40 100644 --- a/corecomponent/androidcomponent/build.gradle +++ b/corecomponent/androidcomponent/build.gradle @@ -26,6 +26,8 @@ dependencies { api Dep.AndroidX.Navigation.runtimeKtx api Dep.AndroidX.Navigation.fragmentKtx api Dep.AndroidX.Navigation.uiKtx + implementation Dep.AndroidX.viewModelKtx + implementation Dep.AndroidX.lifecycleExtensions api Dep.liveDataKtx diff --git a/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/di/PageComponent.kt b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/di/PageComponent.kt new file mode 100644 index 000000000..835c8508b --- /dev/null +++ b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/di/PageComponent.kt @@ -0,0 +1,18 @@ +package io.github.droidkaigi.confsched2019.di + +import androidx.fragment.app.Fragment +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModelProviders +import io.github.droidkaigi.confsched2019.widget.PageViewModel + +interface PageComponent + +inline fun getPageComponent( + fragment: Fragment, + noinline pageComponentCreator: (pageLifecycleOwner: LifecycleOwner) -> T +): T { + val viewModel = ViewModelProviders + .of(fragment, PageViewModel.Factory(pageComponentCreator)) + .get(PageViewModel::class.java) + return viewModel.pageComponent as T +} diff --git a/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/ext/ReceiveChannelExt.kt b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/ext/ReceiveChannelExt.kt index 1c515fc55..14dba8b60 100644 --- a/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/ext/ReceiveChannelExt.kt +++ b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/ext/ReceiveChannelExt.kt @@ -47,6 +47,24 @@ import kotlinx.coroutines.launch } } +@MainThread fun ReceiveChannel.toLiveData( + coroutineScope: CoroutineScope, + defaultValue: T? = null +): LiveData { + return object : LiveData(), CoroutineScope by GlobalScope { + init { + if (defaultValue != null) { + value = defaultValue + } + coroutineScope.launch { + for (element in this@toLiveData) { + postValue(element) + } + } + } + } +} + @MainThread fun ReceiveChannel.toLiveData( store: Store, defaultValue: T? = null diff --git a/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/widget/PageViewModel.kt b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/widget/PageViewModel.kt new file mode 100644 index 000000000..4fccd8151 --- /dev/null +++ b/corecomponent/androidcomponent/src/main/java/io/github/droidkaigi/confsched2019/widget/PageViewModel.kt @@ -0,0 +1,33 @@ +package io.github.droidkaigi.confsched2019.widget + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import io.github.droidkaigi.confsched2019.di.PageComponent + +class PageViewModel : ViewModel(), LifecycleOwner { + lateinit var pageComponent: PageComponent + + private val lifecycle = LifecycleRegistry(this).apply { + handleLifecycleEvent(Lifecycle.Event.ON_RESUME) + } + + override fun getLifecycle(): Lifecycle = lifecycle + + override fun onCleared() { + super.onCleared() + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + } + + class Factory(val pageComponentCreator: (LifecycleOwner) -> PageComponent) : + ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + val pageViewModel = PageViewModel() + pageViewModel.pageComponent = pageComponentCreator(pageViewModel) + @Suppress("UNCHECKED_CAST") + return pageViewModel as T + } + } +} diff --git a/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/StaffSearchFragment.kt b/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/StaffSearchFragment.kt index e227639f8..016232c7b 100644 --- a/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/StaffSearchFragment.kt +++ b/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/StaffSearchFragment.kt @@ -17,29 +17,28 @@ import androidx.fragment.app.Fragment import com.xwray.groupie.GroupAdapter import com.xwray.groupie.Item import com.xwray.groupie.ViewHolder +import dagger.Component import io.github.droidkaigi.confsched2019.App +import io.github.droidkaigi.confsched2019.di.AppComponent +import io.github.droidkaigi.confsched2019.di.PageComponent +import io.github.droidkaigi.confsched2019.di.PageScope +import io.github.droidkaigi.confsched2019.di.getPageComponent import io.github.droidkaigi.confsched2019.ext.changed import io.github.droidkaigi.confsched2019.ext.requireValue import io.github.droidkaigi.confsched2019.staff_dfm.R import io.github.droidkaigi.confsched2019.staff_dfm.databinding.FragmentStaffSearchBinding import io.github.droidkaigi.confsched2019.staff_dfm.ui.actioncreator.StaffSearchActionCreator -import io.github.droidkaigi.confsched2019.staff_dfm.ui.di.DaggerStaffComponent -import io.github.droidkaigi.confsched2019.staff_dfm.ui.di.StaffModule +import io.github.droidkaigi.confsched2019.staff_dfm.ui.di.PageModule import io.github.droidkaigi.confsched2019.staff_dfm.ui.item.StaffItem import io.github.droidkaigi.confsched2019.staff_dfm.ui.store.StaffSearchStore -import me.tatarka.injectedvmprovider.InjectedViewModelProviders import javax.inject.Inject -import javax.inject.Provider class StaffSearchFragment : Fragment() { private lateinit var binding: FragmentStaffSearchBinding @Inject lateinit var searchActionCreator: StaffSearchActionCreator + @Inject lateinit var searchStore: StaffSearchStore private var searchView: SearchView? = null - @Inject lateinit var searchStoreProvider: Provider - private val searchStore: StaffSearchStore by lazy { - InjectedViewModelProviders.of(requireActivity()).get(searchStoreProvider) - } private val groupAdapter = GroupAdapter() @@ -60,13 +59,13 @@ class StaffSearchFragment : Fragment() { override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) - val appComponent = (requireContext().applicationContext as App).appCmponent - val component = DaggerStaffComponent.builder() - .appComponent(appComponent) - .staffModule(StaffModule(this)) - .build() - component.inject(this) + getPageComponent(this) { pageLifecycleOwner -> + DaggerStaffPageComponent.builder() + .appComponent(appComponent) + .pageModule(PageModule(pageLifecycleOwner)) + .build() + }.inject(this) binding.searchRecycler.adapter = groupAdapter @@ -132,3 +131,19 @@ class StaffSearchFragment : Fragment() { imm?.hideSoftInputFromWindow(view?.windowToken, 0) } } + +@PageScope +@Component( + modules = [PageModule::class], + dependencies = [AppComponent::class] +) +interface StaffPageComponent : PageComponent { + @Component.Builder + interface Builder { + fun pageModule(pageModule: PageModule): Builder + fun appComponent(appComponent: AppComponent): Builder + fun build(): StaffPageComponent + } + + fun inject(fragment: StaffSearchFragment) +} diff --git a/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/actioncreator/StaffSearchActionCreator.kt b/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/actioncreator/StaffSearchActionCreator.kt index 1d1856084..b8e6e605e 100644 --- a/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/actioncreator/StaffSearchActionCreator.kt +++ b/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/actioncreator/StaffSearchActionCreator.kt @@ -1,11 +1,9 @@ package io.github.droidkaigi.confsched2019.staff_dfm.ui.actioncreator -import androidx.lifecycle.Lifecycle import io.github.droidkaigi.confsched2019.action.Action import io.github.droidkaigi.confsched2019.data.repository.StaffRepository import io.github.droidkaigi.confsched2019.di.PageScope import io.github.droidkaigi.confsched2019.dispatcher.Dispatcher -import io.github.droidkaigi.confsched2019.ext.coroutineScope import io.github.droidkaigi.confsched2019.model.LoadingState import io.github.droidkaigi.confsched2019.model.StaffContents import io.github.droidkaigi.confsched2019.model.StaffSearchResult @@ -18,8 +16,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 { + @PageScope private val coroutineScope: CoroutineScope +) : CoroutineScope by coroutineScope, ErrorHandler { fun load() = launch { try { diff --git a/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/di/PageModule.kt b/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/di/PageModule.kt new file mode 100644 index 000000000..91c79c64c --- /dev/null +++ b/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/di/PageModule.kt @@ -0,0 +1,25 @@ +package io.github.droidkaigi.confsched2019.staff_dfm.ui.di + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import dagger.Module +import dagger.Provides +import io.github.droidkaigi.confsched2019.di.PageScope +import io.github.droidkaigi.confsched2019.ext.coroutineScope +import kotlinx.coroutines.CoroutineScope + +@Module +class PageModule(val lifecycleOwner: LifecycleOwner) { + + @Provides + @PageScope + fun providesLifecycle(): Lifecycle { + return lifecycleOwner.lifecycle + } + + @Provides + @PageScope + fun provideCoroutineScope(): CoroutineScope { + return lifecycleOwner.lifecycle.coroutineScope + } +} diff --git a/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/di/StaffComponent.kt b/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/di/StaffComponent.kt deleted file mode 100644 index 7157f8527..000000000 --- a/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/di/StaffComponent.kt +++ /dev/null @@ -1,24 +0,0 @@ -package io.github.droidkaigi.confsched2019.staff_dfm.ui.di - -import dagger.Component -import io.github.droidkaigi.confsched2019.di.AppComponent -import io.github.droidkaigi.confsched2019.di.PageScope -import io.github.droidkaigi.confsched2019.staff_dfm.ui.StaffSearchFragment - -@PageScope -@Component( - modules = [ - StaffModule::class - ], - dependencies = [AppComponent::class] -) -interface StaffComponent { - @Component.Builder - interface Builder { - fun build(): StaffComponent - fun appComponent(appComponent: AppComponent): Builder - fun staffModule(staffModule: StaffModule): Builder - } - - fun inject(fragment: StaffSearchFragment) -} diff --git a/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/di/StaffModule.kt b/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/di/StaffModule.kt deleted file mode 100644 index 0af6a3863..000000000 --- a/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/di/StaffModule.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.droidkaigi.confsched2019.staff_dfm.ui.di - -import androidx.lifecycle.Lifecycle -import dagger.Module -import dagger.Provides -import io.github.droidkaigi.confsched2019.di.PageScope -import io.github.droidkaigi.confsched2019.staff_dfm.ui.StaffSearchFragment - -@Module -class StaffModule(private val fragment: StaffSearchFragment) { - - @Provides - @PageScope - fun providesLifecycle(): Lifecycle { - return fragment.viewLifecycleOwner.lifecycle - } -} diff --git a/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/store/StaffSearchStore.kt b/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/store/StaffSearchStore.kt index 6acebe07d..206b24b96 100644 --- a/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/store/StaffSearchStore.kt +++ b/feature/staff_dfm/src/main/java/io/github/droidkaigi/confsched2019/staff_dfm/ui/store/StaffSearchStore.kt @@ -1,32 +1,34 @@ package io.github.droidkaigi.confsched2019.staff_dfm.ui.store import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModel 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.toLiveData 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.store.Store +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.map import javax.inject.Inject +@PageScope class StaffSearchStore @Inject constructor( - dispatcher: Dispatcher -) : Store() { + dispatcher: Dispatcher, + @PageScope coroutineScope: CoroutineScope +) { val query get() = searchResult.value?.query val loadingState: LiveData = dispatcher .subscribe() .map { it.loadingState } - .toLiveData(this, LoadingState.LOADING) + .toLiveData(coroutineScope, LoadingState.LOADING) val searchResult = dispatcher .subscribe() .map { it.searchResult } - .toLiveData(this, StaffSearchResult.EMPTY) + .toLiveData(coroutineScope, StaffSearchResult.EMPTY) val staffContents = dispatcher .subscribe() .map { it.staffContents } - .toLiveData(this, StaffContents.EMPTY) + .toLiveData(coroutineScope, StaffContents.EMPTY) }