Skip to content

Commit

Permalink
Migrate trailers module to store implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
thomaskioko committed May 15, 2023
1 parent a93963b commit e5b02bb
Show file tree
Hide file tree
Showing 10 changed files with 206 additions and 145 deletions.
2 changes: 2 additions & 0 deletions data/trailers/api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ kotlin {
api(projects.core.networkutil)

api(libs.coroutines.core)
api(libs.kotlinx.atomicfu)
api(libs.store5)
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.thomaskioko.tvmaniac.data.trailers.implementation

import com.thomaskioko.tvmaniac.core.db.Trailers
import com.thomaskioko.tvmaniac.core.networkutil.Either
import com.thomaskioko.tvmaniac.core.networkutil.Failure
import kotlinx.coroutines.flow.Flow
import org.mobilenativefoundation.store.store5.StoreReadResponse

interface TrailerRepository {
fun isYoutubePlayerInstalled(): Flow<Boolean>
fun observeTrailersByShowId(traktId: Long): Flow<Either<Failure, List<Trailers>>>
fun observeTrailersStoreResponse(traktId: Long): Flow<StoreReadResponse<List<Trailers>>>
suspend fun fetchTrailersByShowId(traktId: Long): List<Trailers>
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import me.tatarka.inject.annotations.Provides
interface TrailerComponent {

@Provides
fun provideTrailerCache(bind: TrailerDaoImpl): TrailerDao = bind
fun provideTrailerDao(bind: TrailerDaoImpl): TrailerDao = bind

@Provides
fun provideTrailerRepository(bind: TrailerRepositoryImpl): TrailerRepository = bind
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.thomaskioko.tvmaniac.data.trailers.implementation

import com.thomaskioko.tvmaniac.core.db.Trailers
import com.thomaskioko.tvmaniac.tmdb.api.model.TrailerResponse

fun List<TrailerResponse>.toEntity(id: Long) = map { trailer ->
Trailers(
id = trailer.id,
trakt_id = id,
key = trailer.key,
name = trailer.name,
site = trailer.site,
size = trailer.size.toLong(),
type = trailer.type,
)
}
Original file line number Diff line number Diff line change
@@ -1,77 +1,28 @@
package com.thomaskioko.tvmaniac.data.trailers.implementation

import co.touchlab.kermit.Logger
import com.thomaskioko.tvmaniac.core.db.Trailers
import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse
import com.thomaskioko.tvmaniac.core.networkutil.Either
import com.thomaskioko.tvmaniac.core.networkutil.Failure
import com.thomaskioko.tvmaniac.core.networkutil.networkBoundResult
import com.thomaskioko.tvmaniac.shows.api.cache.ShowsDao
import com.thomaskioko.tvmaniac.tmdb.api.TmdbService
import com.thomaskioko.tvmaniac.tmdb.api.model.ErrorResponse
import com.thomaskioko.tvmaniac.tmdb.api.model.TrailersResponse
import com.thomaskioko.tvmaniac.util.AppUtils
import com.thomaskioko.tvmaniac.util.ExceptionHandler
import com.thomaskioko.tvmaniac.util.model.AppCoroutineDispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import me.tatarka.inject.annotations.Inject
import org.mobilenativefoundation.store.store5.StoreReadRequest
import org.mobilenativefoundation.store.store5.StoreReadResponse
import org.mobilenativefoundation.store.store5.impl.extensions.get

@Inject
class TrailerRepositoryImpl(
private val apiService: TmdbService,
private val trailerDao: TrailerDao,
private val showsDao: ShowsDao,
private val store: TrailerStore,
private val appUtils: AppUtils,
private val exceptionHandler: ExceptionHandler,
private val dispatchers: AppCoroutineDispatchers,
) : TrailerRepository {

override fun isYoutubePlayerInstalled(): Flow<Boolean> = appUtils.isYoutubePlayerInstalled()

override fun observeTrailersByShowId(traktId: Long): Flow<Either<Failure, List<Trailers>>> =
networkBoundResult(
query = { trailerDao.observeTrailersById(traktId) },
shouldFetch = { it.isNullOrEmpty() },
fetch = {
val show = showsDao.getTvShow(traktId)
apiService.getTrailers(show.tmdb_id!!)
},
saveFetchResult = { it.mapAndCache(traktId) },
exceptionHandler = exceptionHandler,
coroutineDispatcher = dispatchers.io,
)
override suspend fun fetchTrailersByShowId(traktId: Long): List<Trailers> =
store.get(traktId)

private fun ApiResponse<TrailersResponse, ErrorResponse>.mapAndCache(showId: Long) {
when (this) {
is ApiResponse.Success -> {
val cacheList = body.results.map { response ->
Trailers(
id = response.id,
trakt_id = showId,
key = response.key,
name = response.name,
site = response.site,
size = response.size.toLong(),
type = response.type,
)
}
trailerDao.insert(cacheList)
}

is ApiResponse.Error.GenericError -> {
Logger.withTag("observeTrailersByShowId").e("$this")
throw Throwable("$errorMessage")
}

is ApiResponse.Error.HttpError -> {
Logger.withTag("observeTrailersByShowId").e("$this")
throw Throwable("$code - ${errorBody?.message}")
}

is ApiResponse.Error.SerializationError -> {
Logger.withTag("observeTrailersByShowId").e("$this")
throw Throwable("$this")
}
}
}
override fun observeTrailersStoreResponse(traktId: Long): Flow<StoreReadResponse<List<Trailers>>> =
store.stream(StoreReadRequest.cached(key = traktId, refresh = true))
.flowOn(dispatchers.io)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.thomaskioko.tvmaniac.data.trailers.implementation

import com.thomaskioko.tvmaniac.core.db.Trailers
import com.thomaskioko.tvmaniac.core.networkutil.ApiResponse
import com.thomaskioko.tvmaniac.shows.api.cache.ShowsDao
import com.thomaskioko.tvmaniac.tmdb.api.TmdbService
import com.thomaskioko.tvmaniac.util.KermitLogger
import com.thomaskioko.tvmaniac.util.model.AppCoroutineScope
import me.tatarka.inject.annotations.Inject
import org.mobilenativefoundation.store.store5.Fetcher
import org.mobilenativefoundation.store.store5.SourceOfTruth
import org.mobilenativefoundation.store.store5.Store
import org.mobilenativefoundation.store.store5.StoreBuilder

@Inject
class TrailerStore(
private val apiService: TmdbService,
private val trailerDao: TrailerDao,
private val showsDao: ShowsDao,
private val logger: KermitLogger,
private val scope: AppCoroutineScope,
) : Store<Long, List<Trailers>> by StoreBuilder.from<Long, List<Trailers>, List<Trailers>, List<Trailers>>(
fetcher = Fetcher.of { id ->

val show = showsDao.getTvShow(id)

when (val apiResult = apiService.getTrailers(show.tmdb_id!!)) {
is ApiResponse.Success -> apiResult.body.results.toEntity(id)

is ApiResponse.Error.GenericError -> {
logger.error("GenericError", "$apiResult")
throw Throwable("${apiResult.errorMessage}")
}

is ApiResponse.Error.HttpError -> {
logger.error("HttpError", "$apiResult")
throw Throwable("${apiResult.code} - ${apiResult.errorBody?.message}")
}

is ApiResponse.Error.SerializationError -> {
logger.error("SerializationError", "$apiResult")
throw Throwable("$apiResult")
}
}
},
sourceOfTruth = SourceOfTruth.of(
reader = { id -> trailerDao.observeTrailersById(id) },
writer = { _, response ->
trailerDao.insert(response)
},
delete = trailerDao::delete,
deleteAll = trailerDao::deleteAll,
),
)
.scope(scope.io)
.build()
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
package com.thomaskioko.tvmaniac.trailers.testing

import com.thomaskioko.tvmaniac.core.db.Trailers
import com.thomaskioko.tvmaniac.core.networkutil.Either
import com.thomaskioko.tvmaniac.core.networkutil.Failure
import com.thomaskioko.tvmaniac.data.trailers.implementation.TrailerRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import org.mobilenativefoundation.store.store5.StoreReadResponse

class FakeTrailerRepository : TrailerRepository {
private var trailersResult: Flow<Either<Failure, List<Trailers>>> = flowOf()
private var trailerList = mutableListOf<Trailers>()
private var trailersStoreResponse: Flow<StoreReadResponse<List<Trailers>>> = flowOf()

suspend fun setTrailerResult(result: Either<Failure, List<Trailers>>) {
trailersResult = flow { emit(result) }
suspend fun setTrailerResult(result: StoreReadResponse<List<Trailers>>) {
trailersStoreResponse = flow { emit(result) }
}

fun setTrailerList(list: List<Trailers>) {
trailerList.clear()
trailerList.addAll(list.toMutableList())
}

override fun isYoutubePlayerInstalled(): Flow<Boolean> = flowOf()

override fun observeTrailersByShowId(traktId: Long): Flow<Either<Failure, List<Trailers>>> =
trailersResult
override fun observeTrailersStoreResponse(traktId: Long): Flow<StoreReadResponse<List<Trailers>>> =
trailersStoreResponse

override suspend fun fetchTrailersByShowId(traktId: Long): List<Trailers> = trailerList
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import me.tatarka.inject.annotations.Assisted
import me.tatarka.inject.annotations.Inject
import org.mobilenativefoundation.store.store5.StoreReadResponse

@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
@Inject
Expand Down Expand Up @@ -52,8 +53,8 @@ class ShowDetailsStateMachine constructor(
updateShowDetailsState(result, state)
}

collectWhileInState(trailerRepository.observeTrailersByShowId(traktShowId)) { result, state ->
updateTrailerState(result, state)
collectWhileInState(trailerRepository.observeTrailersStoreResponse(traktShowId)) { response, state ->
updateTrailerState(response, state)
}

collectWhileInState(similarShowsRepository.observeSimilarShows(traktShowId)) { result, state ->
Expand Down Expand Up @@ -129,25 +130,40 @@ class ShowDetailsStateMachine constructor(
)

private fun updateTrailerState(
result: Either<Failure, List<Trailers>>,
response: StoreReadResponse<List<Trailers>>,
state: State<ShowDetailsLoaded>,
) = result.fold(
{
state.mutate {
copy(trailerState = TrailersError(it.errorMessage))
}
},
{
) = when (response) {
is StoreReadResponse.NoNewData -> state.noChange()
is StoreReadResponse.Loading -> state.mutate {
copy(
trailerState = (trailerState as TrailersLoaded).copy(
isLoading = true,
),
)
}
is StoreReadResponse.Data -> {
state.mutate {
copy(
trailerState = (trailerState as TrailersLoaded).copy(
isLoading = false,
trailersList = it.toTrailerList(),
trailersList = response.requireData().toTrailerList(),
),
)
}
},
)
}

is StoreReadResponse.Error.Exception -> {
state.mutate {
copy(trailerState = TrailersError(response.error.message))
}
}

is StoreReadResponse.Error.Message -> {
state.mutate {
copy(trailerState = TrailersError(response.message))
}
}
}

private fun updateShowDetailsState(
result: Either<Failure, List<Seasons>>,
Expand Down
Loading

0 comments on commit e5b02bb

Please sign in to comment.