Skip to content
Permalink
Browse files

Migrate most of app over to use Flow

Room DAOs are still Flowables for now, until Room natively
supports Flow
  • Loading branch information...
chrisbanes committed Aug 1, 2019
1 parent cd9419f commit 71d668c2f6a4a98e323a137afd3f7c8ed2a41ead
Showing with 335 additions and 245 deletions.
  1. +0 −8 app/build.gradle
  2. +8 −3 app/src/main/java/app/tivi/home/HomeActivityViewModel.kt
  3. +5 −2 app/src/main/java/app/tivi/home/search/SearchViewModel.kt
  4. +1 −0 base/build.gradle
  5. +8 −2 build.gradle
  6. +2 −0 buildSrc/src/main/java/app/tivi/buildsrc/dependencies.kt
  7. +24 −21 common-entrygrid/src/main/java/app/tivi/util/EntryGridFragment.kt
  8. +29 −17 common-entrygrid/src/main/java/app/tivi/util/EntryViewModel.kt
  9. +8 −8 common-ui/src/main/java/app/tivi/TiviMvRxViewModel.kt
  10. +2 −2 data/src/main/java/app/tivi/data/daos/EpisodeWatchEntryDao.kt
  11. +2 −2 data/src/main/java/app/tivi/data/daos/EpisodesDao.kt
  12. +2 −2 data/src/main/java/app/tivi/data/daos/FollowedShowsDao.kt
  13. +2 −2 data/src/main/java/app/tivi/data/daos/PairEntryDao.kt
  14. +2 −2 data/src/main/java/app/tivi/data/daos/RelatedShowsDao.kt
  15. +2 −2 data/src/main/java/app/tivi/data/daos/SeasonsDao.kt
  16. +3 −3 data/src/main/java/app/tivi/data/daos/TiviShowDao.kt
  17. +3 −3 data/src/main/java/app/tivi/data/daos/UserDao.kt
  18. +4 −3 data/src/main/java/app/tivi/data/repositories/episodes/EpisodeWatchStore.kt
  19. +6 −5 data/src/main/java/app/tivi/data/repositories/episodes/SeasonsEpisodesStore.kt
  20. +5 −2 data/src/main/java/app/tivi/data/repositories/followedshows/FollowedShowsStore.kt
  21. +6 −1 data/src/main/java/app/tivi/data/repositories/relatedshows/RelatedShowsStore.kt
  22. +2 −2 data/src/main/java/app/tivi/data/repositories/shows/ShowRepository.kt
  23. +2 −3 data/src/main/java/app/tivi/data/repositories/shows/ShowStore.kt
  24. +2 −1 data/src/main/java/app/tivi/data/repositories/traktusers/TraktUsersStore.kt
  25. +0 −11 interactors/src/main/java/app/tivi/interactors/Interactor.kt
  26. +6 −5 interactors/src/main/java/app/tivi/interactors/ObserveEpisodeDetails.kt
  27. +6 −7 interactors/src/main/java/app/tivi/interactors/ObserveEpisodeWatches.kt
  28. +6 −7 interactors/src/main/java/app/tivi/interactors/ObserveFollowedShowSeasonData.kt
  29. +6 −3 interactors/src/main/java/app/tivi/interactors/ObserveFollowedShows.kt
  30. +6 −3 interactors/src/main/java/app/tivi/interactors/ObservePagedPopularShows.kt
  31. +6 −3 interactors/src/main/java/app/tivi/interactors/ObservePagedTrendingShows.kt
  32. +1 −1 interactors/src/main/java/app/tivi/interactors/ObservePopularShows.kt
  33. +6 −5 interactors/src/main/java/app/tivi/interactors/ObserveRelatedShows.kt
  34. +6 −5 interactors/src/main/java/app/tivi/interactors/ObserveShowDetails.kt
  35. +6 −5 interactors/src/main/java/app/tivi/interactors/ObserveShowFollowStatus.kt
  36. +6 −5 interactors/src/main/java/app/tivi/interactors/ObserveShowSeasons.kt
  37. +1 −1 interactors/src/main/java/app/tivi/interactors/ObserveTrendingShows.kt
  38. +6 −5 interactors/src/main/java/app/tivi/interactors/ObserveUserDetails.kt
  39. +6 −3 interactors/src/main/java/app/tivi/interactors/ObserveWatchedShows.kt
  40. +12 −10 tmdb/src/main/java/app/tivi/tmdb/TmdbManager.kt
  41. +2 −0 trakt/build.gradle
  42. +4 −2 ui-discover/src/main/java/app/tivi/home/discover/DiscoverViewModel.kt
  43. +22 −11 ui-episodedetails/src/main/java/app/tivi/episodedetails/EpisodeDetailsViewModel.kt
  44. +19 −13 ui-followed/src/main/java/app/tivi/home/followed/FollowedViewModel.kt
  45. +5 −1 ui-popular/src/main/java/app/tivi/home/popular/PopularShowsViewModel.kt
  46. +42 −29 ui-showdetails/src/main/java/app/tivi/showdetails/details/ShowDetailsFragmentViewModel.kt
  47. +6 −1 ui-trending/src/main/java/app/tivi/home/trending/TrendingShowsViewModel.kt
  48. +19 −13 ui-watched/src/main/java/app/tivi/home/watched/WatchedViewModel.kt
@@ -260,14 +260,6 @@ android.applicationVariants.all { variant ->
}
}

// Enable experimental coroutines APIs, including Flow
project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
freeCompilerArgs += "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi"
freeCompilerArgs += "-Xuse-experimental=kotlinx.coroutines.FlowPreview"
}
}

def getGitHash() {
def stdout = new ByteArrayOutputStream()
exec {
@@ -28,6 +28,7 @@ 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.launch
import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationResponse
import net.openid.appauth.AuthorizationService
@@ -39,9 +40,13 @@ class HomeActivityViewModel @AssistedInject constructor(
observeUserDetails: ObserveUserDetails
) : TiviMvRxViewModel<HomeActivityViewState>(initialState) {
init {
observeUserDetails.observe()
.execute { copy(user = it()) }
observeUserDetails(ObserveUserDetails.Params("me"))
viewModelScope.launch {
observeUserDetails.observe()
.execute { copy(user = it()) }
}
viewModelScope.launch {
observeUserDetails(ObserveUserDetails.Params("me"))
}

traktManager.state
.distinctUntilChanged()
@@ -27,6 +27,7 @@ import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import io.reactivex.subjects.BehaviorSubject
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit

class SearchViewModel @AssistedInject constructor(
@@ -44,8 +45,10 @@ class SearchViewModel @AssistedInject constructor(
// TODO: onError
}).disposeOnClear()

tmdbManager.imageProviderObservable
.execute { copy(tmdbImageUrlProvider = it() ?: tmdbImageUrlProvider) }
viewModelScope.launch {
tmdbManager.imageProviderFlow
.execute { copy(tmdbImageUrlProvider = it() ?: tmdbImageUrlProvider) }
}

searchShows.observe().execute {
copy(searchResults = it())
@@ -24,6 +24,7 @@ dependencies {

api Libs.Coroutines.core
api Libs.Coroutines.rx2
api Libs.kotlinFlowExtensions

api Libs.Dagger.dagger

@@ -74,9 +74,15 @@ subprojects {
}
}

// Treat all Kotlin warnings as errors
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions.allWarningsAsErrors = true
kotlinOptions {
// Treat all Kotlin warnings as errors
allWarningsAsErrors = true

// Enable experimental coroutines APIs, including Flow
freeCompilerArgs += "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi"
freeCompilerArgs += "-Xuse-experimental=kotlinx.coroutines.FlowPreview"
}
}

afterEvaluate {
@@ -69,6 +69,8 @@ object Libs {
const val android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$version"
}

const val kotlinFlowExtensions = "com.github.akarnokd:kotlin-flow-extensions:0.0.2"

object AndroidX {
const val appcompat = "androidx.appcompat:appcompat:1.1.0-beta01"
const val browser = "androidx.browser:browser:1.0.0"
@@ -23,18 +23,19 @@ import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DefaultItemAnimator
import app.tivi.TiviFragment
import app.tivi.api.Status
import app.tivi.common.entrygrid.databinding.FragmentEntryGridBinding
import app.tivi.common.epoxy.StickyHeaderScrollListener
import app.tivi.data.Entry
import app.tivi.data.resultentities.EntryWithShow
import app.tivi.extensions.observeNotNull
import app.tivi.ui.ProgressTimeLatch
import app.tivi.ui.SpacingItemDecorator
import app.tivi.common.epoxy.StickyHeaderScrollListener
import app.tivi.ui.transitions.GridToGridTransitioner
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.flow.collect
import javax.inject.Inject

@SuppressLint("ValidFragment")
@@ -86,27 +87,29 @@ abstract class EntryGridFragment<LI : EntryWithShow<out Entry>, VM : EntryViewMo

binding.gridSwipeRefresh.setOnRefreshListener(viewModel::refresh)

viewModel.viewState.observeNotNull(this) {
controller.tmdbImageUrlProvider = it.tmdbImageUrlProvider
controller.submitList(it.liveList)

when (it.uiResource.status) {
Status.SUCCESS -> {
swipeRefreshLatch.refreshing = false
controller.isLoading = false
lifecycleScope.launchWhenStarted {
viewModel.viewState.collect {
controller.tmdbImageUrlProvider = it.tmdbImageUrlProvider
controller.submitList(it.liveList)

when (it.uiResource.status) {
Status.SUCCESS -> {
swipeRefreshLatch.refreshing = false
controller.isLoading = false
}
Status.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
}
Status.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
}

if (it.isLoaded) {
// First time we've had state, start any postponed transitions
scheduleStartPostponedTransitions()
if (it.isLoaded) {
// First time we've had state, start any postponed transitions
scheduleStartPostponedTransitions()
}
}
}
}
@@ -25,8 +25,8 @@ import app.tivi.data.Entry
import app.tivi.data.resultentities.EntryWithShow
import app.tivi.interactors.PagingInteractor
import app.tivi.tmdb.TmdbManager
import io.reactivex.rxkotlin.Observables
import io.reactivex.subjects.BehaviorSubject
import hu.akarnokd.kotlin.flow.BehaviorSubject
import kotlinx.coroutines.flow.combineLatest
import kotlinx.coroutines.launch

abstract class EntryViewModel<LI : EntryWithShow<out Entry>, PI : PagingInteractor<*, LI>>(
@@ -36,8 +36,8 @@ abstract class EntryViewModel<LI : EntryWithShow<out Entry>, PI : PagingInteract
private val logger: Logger,
private val pageSize: Int = 21
) : ViewModel() {
private val messages = BehaviorSubject.create<UiResource>()
private val loaded = BehaviorSubject.createDefault(false)
private val messages = BehaviorSubject<UiResource>()
private val loaded = BehaviorSubject(false)

protected val pageListConfig = PagedList.Config.Builder().run {
setPageSize(pageSize * 3)
@@ -48,17 +48,27 @@ abstract class EntryViewModel<LI : EntryWithShow<out Entry>, PI : PagingInteract

val boundaryCallback = object : PagedList.BoundaryCallback<LI>() {
override fun onItemAtEndLoaded(itemAtEnd: LI) = onListScrolledToEnd()
override fun onItemAtFrontLoaded(itemAtFront: LI) = loaded.onNext(true)
override fun onZeroItemsLoaded() = loaded.onNext(true)

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

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

val viewState = Observables.combineLatest(
messages,
tmdbManager.imageProviderObservable,
val viewState = messages.combineLatest(
tmdbManager.imageProviderFlow,
pagingInteractor.observe(),
loaded,
::EntryViewState
).toLiveData()
loaded
) { message, imageProvider, pagedList, loaded ->
EntryViewState(message, imageProvider, pagedList, loaded)
}

init {
refresh()
@@ -94,14 +104,16 @@ abstract class EntryViewModel<LI : EntryWithShow<out Entry>, PI : PagingInteract

private fun onError(t: Throwable) {
logger.e(t)
sendMessage(UiResource(Status.ERROR, t.localizedMessage))
viewModelScope.launch {
sendMessage(UiResource(Status.ERROR, t.localizedMessage))
}
}

private fun onSuccess() {
sendMessage(UiResource(Status.SUCCESS))
viewModelScope.launch {
sendMessage(UiResource(Status.SUCCESS))
}
}

private fun sendMessage(uiResource: UiResource) {
messages.onNext(uiResource)
}
private suspend fun sendMessage(uiResource: UiResource) = messages.emit(uiResource)
}
@@ -37,19 +37,19 @@ open class TiviMvRxViewModel<S : MvRxState>(
initialState: S
) : BaseMvRxViewModel<S>(initialState, debugMode = BuildConfig.DEBUG) {

suspend fun <T> Flow<T>.execute(stateReducer: S.(Async<T>) -> S) {
execute({ it }, stateReducer)
}
protected suspend inline fun <T> Flow<T>.execute(
crossinline stateReducer: S.(Async<T>) -> S
) = execute({ it }, stateReducer)

suspend fun <T, V> Flow<T>.execute(
mapper: (T) -> V,
stateReducer: S.(Async<V>) -> S
protected suspend inline fun <T, V> Flow<T>.execute(
crossinline mapper: (T) -> V,
crossinline stateReducer: S.(Async<V>) -> S
) {
setState { stateReducer(Loading()) }

@Suppress("USELESS_CAST")
return map { value -> Success(mapper(value)) as Async<V> }
return map { Success(mapper(it)) as Async<V> }
.catch { emit(Fail(it)) }
.collect { async -> setState { stateReducer(async) } }
.collect { setState { stateReducer(it) } }
}
}
@@ -20,7 +20,7 @@ import androidx.room.Dao
import androidx.room.Query
import app.tivi.data.entities.EpisodeWatchEntry
import app.tivi.data.entities.PendingAction
import io.reactivex.Observable
import io.reactivex.Flowable

@Dao
abstract class EpisodeWatchEntryDao : EntityDao<EpisodeWatchEntry> {
@@ -31,7 +31,7 @@ abstract class EpisodeWatchEntryDao : EntityDao<EpisodeWatchEntry> {
abstract suspend fun watchCountForEpisode(episodeId: Long): Int

@Query("SELECT * FROM episode_watch_entries WHERE episode_id = :episodeId")
abstract fun watchesForEpisodeObservable(episodeId: Long): Observable<List<EpisodeWatchEntry>>
abstract fun watchesForEpisodeObservable(episodeId: Long): Flowable<List<EpisodeWatchEntry>>

@Query("SELECT * FROM episode_watch_entries WHERE id = :id")
abstract suspend fun entryWithId(id: Long): EpisodeWatchEntry?
@@ -19,7 +19,7 @@ package app.tivi.data.daos
import androidx.room.Dao
import androidx.room.Query
import app.tivi.data.entities.Episode
import io.reactivex.Observable
import io.reactivex.Flowable

@Dao
abstract class EpisodesDao : EntityDao<Episode> {
@@ -45,7 +45,7 @@ abstract class EpisodesDao : EntityDao<Episode> {
abstract suspend fun episodeIdWithTraktId(traktId: Int): Long?

@Query("SELECT * from episodes WHERE id = :id")
abstract fun episodeWithIdObservable(id: Long): Observable<Episode>
abstract fun episodeWithIdObservable(id: Long): Flowable<Episode>

@Query("SELECT shows.id FROM shows" +
" INNER JOIN seasons AS s ON s.show_id = shows.id" +
@@ -24,7 +24,7 @@ import app.tivi.data.entities.FollowedShowEntry
import app.tivi.data.entities.PendingAction
import app.tivi.data.entities.Season
import app.tivi.data.resultentities.FollowedShowEntryWithShow
import io.reactivex.Observable
import io.reactivex.Flowable

@Dao
abstract class FollowedShowsDao : EntryDao<FollowedShowEntry, FollowedShowEntryWithShow> {
@@ -74,7 +74,7 @@ abstract class FollowedShowsDao : EntryDao<FollowedShowEntry, FollowedShowEntryW
abstract suspend fun entryWithShowId(showId: Long): FollowedShowEntry?

@Query("SELECT COUNT(*) FROM myshows_entries WHERE show_id = :showId AND pending_action != 'delete'")
abstract fun entryCountWithShowIdNotPendingDeleteObservable(showId: Long): Observable<Int>
abstract fun entryCountWithShowIdNotPendingDeleteObservable(showId: Long): Flowable<Int>

@Query("SELECT COUNT(*) FROM myshows_entries WHERE show_id = :showId")
abstract suspend fun entryCountWithShowId(showId: Long): Int
@@ -18,14 +18,14 @@ package app.tivi.data.daos

import app.tivi.data.MultipleEntry
import app.tivi.data.resultentities.EntryWithShow
import io.reactivex.Observable
import io.reactivex.Flowable

/**
* This interface represents a DAO which contains entities which are part of a collective list for a given show.
*/
interface PairEntryDao<EC : MultipleEntry, LI : EntryWithShow<EC>> : EntityDao<EC> {
fun entries(showId: Long): List<EC>
fun entriesWithShows(showId: Long): List<LI>
fun entriesWithShowsObservable(showId: Long): Observable<List<LI>>
fun entriesWithShowsObservable(showId: Long): Flowable<List<LI>>
suspend fun deleteWithShowId(showId: Long)
}
@@ -21,7 +21,7 @@ import androidx.room.Query
import androidx.room.Transaction
import app.tivi.data.entities.RelatedShowEntry
import app.tivi.data.resultentities.RelatedShowEntryWithShow
import io.reactivex.Observable
import io.reactivex.Flowable

@Dao
abstract class RelatedShowsDao : PairEntryDao<RelatedShowEntry, RelatedShowEntryWithShow> {
@@ -35,7 +35,7 @@ abstract class RelatedShowsDao : PairEntryDao<RelatedShowEntry, RelatedShowEntry

@Transaction
@Query("SELECT * FROM related_shows WHERE show_id = :showId ORDER BY order_index")
abstract override fun entriesWithShowsObservable(showId: Long): Observable<List<RelatedShowEntryWithShow>>
abstract override fun entriesWithShowsObservable(showId: Long): Flowable<List<RelatedShowEntryWithShow>>

@Query("DELETE FROM related_shows WHERE show_id = :showId")
abstract override suspend fun deleteWithShowId(showId: Long)
@@ -22,13 +22,13 @@ import androidx.room.Transaction
import app.tivi.data.entities.Season
import app.tivi.data.entities.Season.Companion.NUMBER_SPECIALS
import app.tivi.data.resultentities.SeasonWithEpisodesAndWatches
import io.reactivex.Observable
import io.reactivex.Flowable

@Dao
abstract class SeasonsDao : EntityDao<Season> {
@Transaction
@Query("SELECT * FROM seasons WHERE show_id = :showId ORDER BY number=$NUMBER_SPECIALS, number")
abstract fun seasonsWithEpisodesForShowId(showId: Long): Observable<List<SeasonWithEpisodesAndWatches>>
abstract fun seasonsWithEpisodesForShowId(showId: Long): Flowable<List<SeasonWithEpisodesAndWatches>>

@Query("SELECT * FROM seasons WHERE show_id = :showId ORDER BY number=$NUMBER_SPECIALS, number")
abstract suspend fun seasonsForShowId(showId: Long): List<Season>

0 comments on commit 71d668c

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