Skip to content
Permalink
Browse files

Move Entry Grids to MvRx

  • Loading branch information...
chrisbanes committed Nov 7, 2019
1 parent 33b4e25 commit 95906f4db586e7c53ceaa01aadf5ebae0579603c
@@ -20,13 +20,12 @@ import android.annotation.SuppressLint
import android.os.Bundle
import android.view.ActionMode
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.recyclerview.widget.DefaultItemAnimator
import app.tivi.TiviFragmentWithBinding
import app.tivi.api.UiError
import app.tivi.api.UiLoading
import app.tivi.common.entrygrid.R
@@ -39,21 +38,17 @@ import app.tivi.extensions.scheduleStartPostponedTransitions
import app.tivi.ui.ProgressTimeLatch
import app.tivi.ui.SpacingItemDecorator
import app.tivi.ui.transitions.GridToGridTransitioner
import com.airbnb.mvrx.withState
import com.google.android.material.snackbar.Snackbar
import dagger.android.support.DaggerFragment
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import javax.inject.Inject

@SuppressLint("ValidFragment")
abstract class EntryGridFragment<LI, VM> : DaggerFragment()
abstract class EntryGridFragment<LI, VM> : TiviFragmentWithBinding<FragmentEntryGridBinding>()
where LI : EntryWithShow<out Entry>, VM : EntryViewModel<LI, *> {
protected abstract val viewModel: VM

private lateinit var swipeRefreshLatch: ProgressTimeLatch

private lateinit var controller: EntryGridEpoxyController<LI>
protected lateinit var binding: FragmentEntryGridBinding

@Inject lateinit var appBarConfiguration: AppBarConfiguration

@@ -65,21 +60,19 @@ abstract class EntryGridFragment<LI, VM> : DaggerFragment()
controller = createController()

GridToGridTransitioner.setupSecondFragment(this, R.id.grid_appbar) {
binding.gridRecyclerview.itemAnimator = DefaultItemAnimator()
requireBinding().gridRecyclerview.itemAnimator = DefaultItemAnimator()
}
}

override fun onCreateView(
override fun createBinding(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentEntryGridBinding.inflate(inflater, container, false)
return binding.root
): FragmentEntryGridBinding {
return FragmentEntryGridBinding.inflate(inflater, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
override fun onViewCreated(binding: FragmentEntryGridBinding, savedInstanceState: Bundle?) {
postponeEnterTransitionWithTimeout()

swipeRefreshLatch = ProgressTimeLatch(minShowTime = 1350) {
@@ -107,13 +100,9 @@ abstract class EntryGridFragment<LI, VM> : DaggerFragment()
}

binding.gridSwipeRefresh.setOnRefreshListener(viewModel::refresh)

viewLifecycleOwner.lifecycleScope.launch {
viewModel.viewState.collect { invalidate(it) }
}
}

private fun invalidate(state: EntryViewState<LI>) {
override fun invalidate(binding: FragmentEntryGridBinding) = withState(viewModel) { state ->
controller.state = state
controller.submitList(state.liveList)

@@ -150,7 +139,9 @@ abstract class EntryGridFragment<LI, VM> : DaggerFragment()

override fun onDestroyView() {
super.onDestroyView()

currentActionMode?.finish()
currentActionMode = null
}

abstract fun startSelectionActionMode(): ActionMode?
@@ -16,9 +16,9 @@

package app.tivi.util

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagedList
import app.tivi.TiviMvRxViewModel
import app.tivi.api.UiError
import app.tivi.api.UiIdle
import app.tivi.api.UiLoading
@@ -34,19 +34,19 @@ import app.tivi.data.entities.TiviShow
import app.tivi.data.resultentities.EntryWithShow
import app.tivi.domain.PagingInteractor
import app.tivi.domain.interactors.ChangeShowFollowStatus
import app.tivi.domain.launchObserve
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.broadcastIn
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch

abstract class EntryViewModel<LI : EntryWithShow<out Entry>, PI : PagingInteractor<*, LI>>(
initialState: EntryViewState<LI>,
private val pageSize: Int = 21
) : ViewModel() {
) : TiviMvRxViewModel<EntryViewState<LI>>(initialState) {
protected abstract val dispatchers: AppCoroutineDispatchers
protected abstract val pagingInteractor: PI
protected abstract val logger: Logger
@@ -76,16 +76,34 @@ abstract class EntryViewModel<LI : EntryWithShow<out Entry>, PI : PagingInteract
}
}

val viewState: Flow<EntryViewState<LI>> by lazy(LazyThreadSafetyMode.NONE) {
combine(
messages.asFlow(),
pagingInteractor.observe(),
loaded.asFlow(),
showSelection.observeIsSelectionOpen(),
showSelection.observeSelectedShowIds()
) { message, pagedList, loaded, selectionOpen, selectedIds ->
EntryViewState(message, pagedList, loaded, selectionOpen, selectedIds)
}.broadcastIn(viewModelScope).asFlow()
protected fun launchObserves() {
viewModelScope.launch {
messages.asFlow().execute {
copy(status = it() ?: UiSuccess)
}
}

viewModelScope.launchObserve(pagingInteractor) {
it.execute { copy(liveList = it()) }
}

viewModelScope.launch {
loaded.asFlow().execute {
copy(isLoaded = it() ?: false)
}
}

viewModelScope.launch {
showSelection.observeIsSelectionOpen().execute {
copy(selectionOpen = it() ?: false)
}
}

viewModelScope.launch {
showSelection.observeSelectedShowIds().execute {
copy(selectedShowIds = it() ?: emptySet())
}
}
}

fun onListScrolledToEnd() {
@@ -19,11 +19,12 @@ package app.tivi.util
import androidx.paging.PagedList
import app.tivi.api.UiIdle
import app.tivi.api.UiStatus
import com.airbnb.mvrx.MvRxState

data class EntryViewState<LI>(
val status: UiStatus = UiIdle,
val liveList: PagedList<LI>? = null,
val isLoaded: Boolean = false,
val selectionOpen: Boolean = false,
val selectedShowIds: Set<Long> = emptySet()
)
) : MvRxState
@@ -28,7 +28,8 @@ abstract class TiviFragmentWithBinding<V : ViewDataBinding> : TiviFragment() {
// Fake injected variable to force Dagger to create the factory
@Inject lateinit var logger: Logger

private var binding: V? = null
var binding: V? = null
private set

final override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return createBinding(inflater, container, savedInstanceState)
@@ -84,6 +84,9 @@ dependencies {
kapt Libs.Dagger.compiler
kapt Libs.Dagger.androidProcessor

compileOnly Libs.AssistedInject.annotationDagger2
kapt Libs.AssistedInject.processorDagger2

implementation Libs.mvRx

implementation Libs.Epoxy.epoxy
@@ -16,20 +16,18 @@

package app.tivi.home.popular

import androidx.lifecycle.ViewModel
import app.tivi.inject.ViewModelKey
import dagger.Binds
import com.squareup.inject.assisted.dagger2.AssistedModule
import dagger.Module
import dagger.android.ContributesAndroidInjector
import dagger.multibindings.IntoMap

@Module
abstract class PopularBuilder {
@ContributesAndroidInjector
@ContributesAndroidInjector(modules = [
PopularAssistedModule::class
])
internal abstract fun popularShowsFragment(): PopularShowsFragment
}

@Binds
@IntoMap
@ViewModelKey(PopularShowsViewModel::class)
abstract fun bindPopularShowsViewModel(viewModel: PopularShowsViewModel): ViewModel
}
@Module(includes = [AssistedInject_PopularAssistedModule::class])
@AssistedModule
interface PopularAssistedModule
@@ -22,25 +22,25 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import app.tivi.SharedElementHelper
import app.tivi.common.entrygrid.databinding.FragmentEntryGridBinding
import app.tivi.common.layouts.PosterGridItemBindingModel_
import app.tivi.data.entities.findHighestRatedPoster
import app.tivi.data.resultentities.PopularEntryWithShow
import app.tivi.extensions.toActivityNavigatorExtras
import app.tivi.util.EntryGridEpoxyController
import app.tivi.util.EntryGridFragment
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.mvrx.fragmentViewModel
import javax.inject.Inject

class PopularShowsFragment : EntryGridFragment<PopularEntryWithShow, PopularShowsViewModel>() {
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
override val viewModel: PopularShowsViewModel by viewModels(factoryProducer = { viewModelFactory })
override val viewModel: PopularShowsViewModel by fragmentViewModel()
@Inject lateinit var popularShowsViewModelFactory: PopularShowsViewModel.Factory

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
override fun onViewCreated(binding: FragmentEntryGridBinding, savedInstanceState: Bundle?) {
super.onViewCreated(binding, savedInstanceState)

binding.gridToolbar.apply {
setTitle(R.string.discover_popular_title)
@@ -49,7 +49,7 @@ class PopularShowsFragment : EntryGridFragment<PopularEntryWithShow, PopularShow

internal fun onItemClicked(item: PopularEntryWithShow) {
val sharedElements = SharedElementHelper()
binding.gridRecyclerview.findViewHolderForItemId(item.generateStableId()).let {
requireBinding().gridRecyclerview.findViewHolderForItemId(item.generateStableId()).let {
sharedElements.addSharedElement(it.itemView, "poster")
}

@@ -23,23 +23,27 @@ import app.tivi.domain.interactors.UpdatePopularShows
import app.tivi.domain.observers.ObservePagedPopularShows
import app.tivi.util.AppCoroutineDispatchers
import app.tivi.util.EntryViewModel
import app.tivi.util.EntryViewState
import app.tivi.util.Logger
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

class PopularShowsViewModel @Inject constructor(
class PopularShowsViewModel @AssistedInject constructor(
@Assisted initialState: EntryViewState<PopularEntryWithShow>,
override val dispatchers: AppCoroutineDispatchers,
override val pagingInteractor: ObservePagedPopularShows,
private val interactor: UpdatePopularShows,
override val logger: Logger,
override val changeShowFollowStatus: ChangeShowFollowStatus
) : EntryViewModel<PopularEntryWithShow, ObservePagedPopularShows>() {
) : EntryViewModel<PopularEntryWithShow, ObservePagedPopularShows>(initialState) {
init {
pagingInteractor(ObservePagedPopularShows.Params(pageListConfig, boundaryCallback))

// Kick start the viewState to happen now, rather than when the Fragment
// starts observing
viewState
launchObserves()

refresh(false)
}
@@ -51,4 +55,19 @@ class PopularShowsViewModel @Inject constructor(
override fun callRefresh(fromUser: Boolean): Flow<InvokeStatus> {
return interactor(UpdatePopularShows.Params(UpdatePopularShows.Page.REFRESH, fromUser))
}

@AssistedInject.Factory
interface Factory {
fun create(initialState: EntryViewState<PopularEntryWithShow>): PopularShowsViewModel
}

companion object : MvRxViewModelFactory<PopularShowsViewModel, EntryViewState<PopularEntryWithShow>> {
override fun create(
viewModelContext: ViewModelContext,
state: EntryViewState<PopularEntryWithShow>
): PopularShowsViewModel? {
val fragment: PopularShowsFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.popularShowsViewModelFactory.create(state)
}
}
}
@@ -84,6 +84,9 @@ dependencies {
kapt Libs.Dagger.compiler
kapt Libs.Dagger.androidProcessor

compileOnly Libs.AssistedInject.annotationDagger2
kapt Libs.AssistedInject.processorDagger2

implementation Libs.mvRx

implementation Libs.Epoxy.epoxy
@@ -16,20 +16,18 @@

package app.tivi.home.recommended

import androidx.lifecycle.ViewModel
import app.tivi.inject.ViewModelKey
import dagger.Binds
import com.squareup.inject.assisted.dagger2.AssistedModule
import dagger.Module
import dagger.android.ContributesAndroidInjector
import dagger.multibindings.IntoMap

@Module
abstract class RecommendedBuilder {
@ContributesAndroidInjector
@ContributesAndroidInjector(modules = [
RecommendedAssistedModule::class
])
internal abstract fun RecommendedShowsFragment(): RecommendedShowsFragment
}

@Binds
@IntoMap
@ViewModelKey(RecommendedShowsViewModel::class)
abstract fun bindRecommendedShowsViewModel(viewModel: RecommendedShowsViewModel): ViewModel
}
@Module(includes = [AssistedInject_RecommendedAssistedModule::class])
@AssistedModule
interface RecommendedAssistedModule

0 comments on commit 95906f4

Please sign in to comment.
You can’t perform that action at this time.