Skip to content
Permalink
Browse files

Refactor interactors and scoping

  • Loading branch information...
chrisbanes committed Aug 14, 2019
1 parent e00efa3 commit ddf0eef3c78709cc090e2560ed24b65a56f0bbed
Showing with 407 additions and 398 deletions.
  1. +3 −9 app/src/main/java/app/tivi/home/HomeActivityViewModel.kt
  2. +10 −8 app/src/main/java/app/tivi/home/search/SearchViewModel.kt
  3. +5 −5 common-entrygrid/src/main/java/app/tivi/util/EntryGridFragment.kt
  4. +46 −54 common-entrygrid/src/main/java/app/tivi/util/EntryViewModel.kt
  5. +13 −1 {base → common-ui}/src/main/java/app/tivi/util/ObservableLoadingCounter.kt
  6. +1 −1 data/src/main/java/app/tivi/api/UiResource.kt
  7. +2 −2 data/src/main/java/app/tivi/api/{Status.kt → UiStatus.kt}
  8. +23 −0 data/src/main/java/app/tivi/data/entities/Status.kt
  9. +2 −2 data/src/main/java/app/tivi/data/repositories/episodes/SeasonsEpisodesRepository.kt
  10. +29 −42 domain/src/main/java/app/tivi/domain/Interactor.kt
  11. +8 −5 domain/src/main/java/app/tivi/domain/interactors/AddEpisodeWatch.kt
  12. +8 −5 domain/src/main/java/app/tivi/domain/interactors/ChangeSeasonFollowStatus.kt
  13. +8 −5 domain/src/main/java/app/tivi/domain/interactors/ChangeSeasonWatchedStatus.kt
  14. +35 −8 domain/src/main/java/app/tivi/domain/interactors/ChangeShowFollowStatus.kt
  15. +8 −5 domain/src/main/java/app/tivi/domain/interactors/RemoveEpisodeWatch.kt
  16. +8 −5 domain/src/main/java/app/tivi/domain/interactors/RemoveEpisodeWatches.kt
  17. +8 −5 domain/src/main/java/app/tivi/domain/interactors/UpdateEpisodeDetails.kt
  18. +9 −6 domain/src/main/java/app/tivi/domain/interactors/UpdateFollowedShows.kt
  19. +8 −5 domain/src/main/java/app/tivi/domain/interactors/UpdatePopularShows.kt
  20. +9 −6 domain/src/main/java/app/tivi/domain/interactors/UpdateRelatedShows.kt
  21. +8 −5 domain/src/main/java/app/tivi/domain/interactors/UpdateShowDetails.kt
  22. +9 −6 domain/src/main/java/app/tivi/domain/interactors/UpdateShowSeasonData.kt
  23. +8 −5 domain/src/main/java/app/tivi/domain/interactors/UpdateShowSeasons.kt
  24. +8 −5 domain/src/main/java/app/tivi/domain/interactors/UpdateTrendingShows.kt
  25. +8 −5 domain/src/main/java/app/tivi/domain/interactors/UpdateUserDetails.kt
  26. +8 −5 domain/src/main/java/app/tivi/domain/interactors/UpdateWatchedShows.kt
  27. +2 −5 tasks/src/main/java/app/tivi/tasks/SyncAllFollowedShows.kt
  28. +3 −6 tasks/src/main/java/app/tivi/tasks/SyncShowWatchedProgress.kt
  29. +15 −17 ui-discover/src/main/java/app/tivi/home/discover/DiscoverViewModel.kt
  30. +8 −27 ui-episodedetails/src/main/java/app/tivi/episodedetails/EpisodeDetailsViewModel.kt
  31. +10 −15 ui-followed/src/main/java/app/tivi/home/followed/FollowedViewModel.kt
  32. +16 −27 ui-popular/src/main/java/app/tivi/home/popular/PopularShowsViewModel.kt
  33. +43 −52 ui-showdetails/src/main/java/app/tivi/showdetails/details/ShowDetailsFragmentViewModel.kt
  34. +10 −28 ui-trending/src/main/java/app/tivi/home/trending/TrendingShowsViewModel.kt
  35. +8 −11 ui-watched/src/main/java/app/tivi/home/watched/WatchedViewModel.kt
@@ -19,18 +19,15 @@ package app.tivi.home
import androidx.lifecycle.viewModelScope
import app.tivi.TiviMvRxViewModel
import app.tivi.domain.interactors.UpdateUserDetails
import app.tivi.domain.launchInteractor
import app.tivi.domain.launchObserve
import app.tivi.domain.observers.ObserveUserDetails
import app.tivi.home.main.HomeActivityViewState
import app.tivi.inject.ProcessLifetime
import app.tivi.trakt.TraktAuthState
import app.tivi.trakt.TraktManager
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.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@@ -42,24 +39,21 @@ class HomeActivityViewModel @AssistedInject constructor(
@Assisted initialState: HomeActivityViewState,
private val traktManager: TraktManager,
private val updateUserDetails: UpdateUserDetails,
observeUserDetails: ObserveUserDetails,
@ProcessLifetime private val dataOperationScope: CoroutineScope
observeUserDetails: ObserveUserDetails
) : TiviMvRxViewModel<HomeActivityViewState>(initialState) {
init {
viewModelScope.launchObserve(observeUserDetails) {
it.execute { copy(user = it()) }
}

viewModelScope.launchInteractor(observeUserDetails,
ObserveUserDetails.Params("me"))
observeUserDetails(ObserveUserDetails.Params("me"))

viewModelScope.launch {
traktManager.state
.distinctUntilChanged()
.onEach {
if (it == TraktAuthState.LOGGED_IN) {
dataOperationScope.launchInteractor(updateUserDetails,
UpdateUserDetails.Params("me", false))
updateUserDetails(UpdateUserDetails.Params("me", false))
}
}
.execute {
@@ -19,7 +19,6 @@ package app.tivi.home.search
import androidx.lifecycle.viewModelScope
import app.tivi.TiviMvRxViewModel
import app.tivi.domain.interactors.SearchShows
import app.tivi.domain.launchInteractor
import app.tivi.domain.launchObserve
import app.tivi.tmdb.TmdbManager
import app.tivi.util.ObservableLoadingCounter
@@ -33,6 +32,7 @@ import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class SearchViewModel @AssistedInject constructor(
@Assisted initialState: SearchViewState,
@@ -44,13 +44,15 @@ class SearchViewModel @AssistedInject constructor(

init {
viewModelScope.launch {
searchQuery.asFlow().debounce(300).collect {
viewModelScope.launchInteractor(
searchShows,
SearchShows.Params(it),
loadingState
)
}
searchQuery.asFlow()
.debounce(300)
.collect {
loadingState.addLoader()
withContext(searchShows.dispatcher) {
searchShows(SearchShows.Params(it))
}
loadingState.removeLoader()
}
}

viewModelScope.launch {
@@ -25,7 +25,7 @@ import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DefaultItemAnimator
import app.tivi.TiviFragment
import app.tivi.api.Status
import app.tivi.api.UiStatus
import app.tivi.common.entrygrid.databinding.FragmentEntryGridBinding
import app.tivi.common.epoxy.StickyHeaderScrollListener
import app.tivi.data.Entry
@@ -90,17 +90,17 @@ abstract class EntryGridFragment<LI : EntryWithShow<out Entry>, VM : EntryViewMo
controller.submitList(it.liveList)

when (it.uiResource.status) {
Status.SUCCESS -> {
UiStatus.SUCCESS -> {
swipeRefreshLatch.refreshing = false
controller.isLoading = false
}
Status.ERROR -> {
UiStatus.ERROR -> {
swipeRefreshLatch.refreshing = false
controller.isLoading = false
Snackbar.make(view, it.uiResource.message ?: "EMPTY", Snackbar.LENGTH_SHORT).show()
}
Status.REFRESHING -> swipeRefreshLatch.refreshing = true
Status.LOADING_MORE -> controller.isLoading = true
UiStatus.REFRESHING -> swipeRefreshLatch.refreshing = true
UiStatus.LOADING_MORE -> controller.isLoading = true
}

if (it.isLoaded) {
@@ -19,26 +19,31 @@ package app.tivi.util
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagedList
import app.tivi.api.Status
import app.tivi.api.UiStatus
import app.tivi.api.UiResource
import app.tivi.data.Entry
import app.tivi.data.resultentities.EntryWithShow
import app.tivi.domain.PagingInteractor
import app.tivi.data.entities.Status
import app.tivi.tmdb.TmdbManager
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch

abstract class EntryViewModel<LI : EntryWithShow<out Entry>, PI : PagingInteractor<*, LI>>(
private val dispatchers: AppCoroutineDispatchers,
pagingInteractor: PI,
tmdbManager: TmdbManager,
private val logger: Logger,
private val pageSize: Int = 21
) : ViewModel() {
protected abstract val dispatchers: AppCoroutineDispatchers
protected abstract val pagingInteractor: PI
protected abstract val tmdbManager: TmdbManager
protected abstract val logger: Logger

private val messages = ConflatedBroadcastChannel<UiResource>()
private val loaded = ConflatedBroadcastChannel(false)

@@ -49,75 +54,62 @@ abstract class EntryViewModel<LI : EntryWithShow<out Entry>, PI : PagingInteract
build()
}

val boundaryCallback = object : PagedList.BoundaryCallback<LI>() {
protected val boundaryCallback = object : PagedList.BoundaryCallback<LI>() {
override fun onItemAtEndLoaded(itemAtEnd: LI) = onListScrolledToEnd()

override fun onItemAtFrontLoaded(itemAtFront: LI) {
viewModelScope.launch {
loaded.offer(true)
}
loaded.offer(true)
}

override fun onZeroItemsLoaded() {
viewModelScope.launch {
loaded.offer(true)
}
loaded.offer(true)
}
}

val viewState = combine(
messages.asFlow(),
tmdbManager.imageProviderFlow,
pagingInteractor.observe().flowOn(pagingInteractor.dispatcher),
loaded.asFlow()
) { message, imageProvider, pagedList, loaded ->
EntryViewState(message, imageProvider, pagedList, loaded)
}

init {
refresh()
val viewState: Flow<EntryViewState<LI>> by lazy(LazyThreadSafetyMode.NONE) {
combine(
messages.asFlow(),
tmdbManager.imageProviderFlow,
pagingInteractor.observe().flowOn(pagingInteractor.dispatcher),
loaded.asFlow()
) { message, imageProvider, pagedList, loaded ->
EntryViewState(message, imageProvider, pagedList, loaded)
}
}

fun onListScrolledToEnd() {
viewModelScope.launch {
sendMessage(UiResource(Status.LOADING_MORE))
try {
callLoadMore().join()
onSuccess()
} catch (e: Exception) {
onError(e)
callLoadMore().also {
viewModelScope.launch {
it.catch { sendMessage(UiResource(UiStatus.ERROR, it.localizedMessage)) }
.map {
when (it) {
Status.FINISHED -> UiStatus.SUCCESS
else -> UiStatus.LOADING_MORE
}
}
.collect { sendMessage(UiResource(it)) }
}
}
}

fun refresh() {
viewModelScope.launch {
sendMessage(UiResource(Status.REFRESHING))
try {
callRefresh().join()
onSuccess()
} catch (e: Exception) {
onError(e)
callRefresh().also {
viewModelScope.launch {
it.catch { sendMessage(UiResource(UiStatus.ERROR, it.localizedMessage)) }
.map {
when (it) {
Status.FINISHED -> UiStatus.SUCCESS
else -> UiStatus.REFRESHING
}
}
.collect { sendMessage(UiResource(it)) }
}
}
}

protected abstract suspend fun callRefresh(): Job

protected abstract suspend fun callLoadMore(): Job

private fun onError(t: Throwable) {
logger.e(t)
viewModelScope.launch {
sendMessage(UiResource(Status.ERROR, t.localizedMessage))
}
}
protected abstract fun callRefresh(): Flow<Status>

private fun onSuccess() {
viewModelScope.launch {
sendMessage(UiResource(Status.SUCCESS))
}
}
protected abstract fun callLoadMore(): Flow<Status>

private suspend fun sendMessage(uiResource: UiResource) = messages.offer(uiResource)
private fun sendMessage(uiResource: UiResource) = messages.offer(uiResource)
}
@@ -1,5 +1,5 @@
/*
* Copyright 2018 Google LLC
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,11 +16,13 @@

package app.tivi.util

import app.tivi.data.entities.Status
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch

@@ -41,4 +43,14 @@ class ObservableLoadingCounter {
loadingState.send(loadingState.value - 1)
}
}
}

suspend fun ObservableLoadingCounter.collectFrom(statuses: Flow<Status>) {
statuses.collect {
if (it == Status.STARTED) {
addLoader()
} else if (it == Status.FINISHED) {
removeLoader()
}
}
}
@@ -20,4 +20,4 @@ package app.tivi.api
* A generic class that holds a value with its loading status.
* @param <T>
*/
data class UiResource(val status: Status, val message: String? = null)
data class UiResource(val status: UiStatus, val message: String? = null)
@@ -17,13 +17,13 @@
package app.tivi.api

/**
* Status of a resource that is provided to the UI.
* UiStatus of a resource that is provided to the UI.
*
*
* These are usually created by the Repository classes where they return
* `LiveData<Resource<T>>` to pass back the latest data to the UI with its fetch status.
*/
enum class Status {
enum class UiStatus {
SUCCESS,
ERROR,
REFRESHING,
@@ -0,0 +1,23 @@
/*
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package app.tivi.data.entities

enum class Status {
IDLE,
STARTED,
FINISHED
}
@@ -109,8 +109,8 @@ class SeasonsEpisodesRepository @Inject constructor(

suspend fun updateShowEpisodeWatchesIfNeeded(
showId: Long,
refreshType: RefreshType,
forceRefresh: Boolean,
refreshType: RefreshType = RefreshType.QUICK,
forceRefresh: Boolean = false,
lastUpdated: OffsetDateTime? = null
) {
if (refreshType == RefreshType.QUICK) {

0 comments on commit ddf0eef

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