DefaultMovieRepository.kt
package com.louisfn.somovie.data.repository
import androidx.annotation.AnyThread
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.louisfn.somovie.core.common.annotation.DefaultDispatcher
import com.louisfn.somovie.data.database.DatabaseHelper
import com.louisfn.somovie.data.database.datasource.MovieLocalDataSource
import com.louisfn.somovie.data.database.datasource.RemoteKeyLocalDataSource
import com.louisfn.somovie.data.datastore.datasource.DataStoreLocalDataSource
import com.louisfn.somovie.data.datastore.model.SessionData
import com.louisfn.somovie.data.mapper.ExploreCategoryMapper
import com.louisfn.somovie.data.mapper.MovieMapper
import com.louisfn.somovie.data.network.Constants.PAGINATION_FIRST_PAGE_INDEX
import com.louisfn.somovie.data.network.datasource.MovieRemoteDataSource
import com.louisfn.somovie.data.repository.paging.MoviesRemoteMediator
import com.louisfn.somovie.data.repository.paging.mapPaging
import com.louisfn.somovie.domain.model.ExploreCategory
import com.louisfn.somovie.domain.model.Movie
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.invoke
import java.util.concurrent.TimeUnit
import javax.inject.Inject
interface MovieRepository {
@AnyThread
fun moviesPagingChanges(
category: ExploreCategory,
pagingConfig: PagingConfig,
cacheTimeout: Long = DEFAULT_CACHE_TIMEOUT,
): Flow<PagingData<Movie>>
@AnyThread
fun moviesChanges(category: ExploreCategory, limit: Int): Flow<List<Movie>>
@AnyThread
suspend fun refreshMovies(category: ExploreCategory, cacheTimeout: Long = DEFAULT_CACHE_TIMEOUT)
@AnyThread
fun movieChanges(movieId: Long): Flow<Movie>
@AnyThread
suspend fun refreshMovie(movieId: Long)
companion object {
private val DEFAULT_CACHE_TIMEOUT = TimeUnit.HOURS.toMillis(12)
}
}
internal class DefaultMovieRepository @Inject constructor(
private val remoteDataSource: MovieRemoteDataSource,
private val localDataSource: MovieLocalDataSource,
private val remoteKeyLocalDataSource: RemoteKeyLocalDataSource,
private val mapper: MovieMapper,
private val categoryMapper: ExploreCategoryMapper,
private val databaseHelper: DatabaseHelper,
private val sessionLocalDataSource: DataStoreLocalDataSource<SessionData>,
@DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
) : MovieRepository {
//region Explore movies
override fun moviesPagingChanges(
category: ExploreCategory,
pagingConfig: PagingConfig,
cacheTimeout: Long,
): Flow<PagingData<Movie>> =
Pager(
config = pagingConfig,
remoteMediator = createMoviesRemoteMediator(category, cacheTimeout),
pagingSourceFactory = {
localDataSource.getPagingMovies(categoryMapper.mapToEntity(category))
},
)
.flow
.mapPaging { mapper.mapToDomain(it) }
.flowOn(defaultDispatcher)
@AnyThread
private fun createMoviesRemoteMediator(
category: ExploreCategory,
cacheTimeout: Long,
) = MoviesRemoteMediator(
cacheTimeout = cacheTimeout,
remoteKeyLocalDataSource = remoteKeyLocalDataSource,
localDataSource = localDataSource,
remoteDataSource = remoteDataSource,
movieMapper = mapper,
categoryMapper = categoryMapper,
databaseHelper = databaseHelper,
category = category,
)
override fun moviesChanges(category: ExploreCategory, limit: Int): Flow<List<Movie>> =
localDataSource.moviesChanges(categoryMapper.mapToEntity(category), limit)
.map(mapper::mapToDomain)
.flowOn(defaultDispatcher)
override suspend fun refreshMovies(category: ExploreCategory, cacheTimeout: Long) =
defaultDispatcher {
val remoteKeyTypeEntity = categoryMapper.mapToRemoteKeyTypeEntity(category)
if (!remoteKeyLocalDataSource.isExpired(remoteKeyTypeEntity, cacheTimeout)) {
return@defaultDispatcher
}
val response = remoteDataSource.getMovies(category, PAGINATION_FIRST_PAGE_INDEX).results
val categoryEntity = categoryMapper.mapToEntity(category)
databaseHelper.withTransaction {
with(localDataSource) {
deleteExploreMovies(categoryEntity)
insertOrIgnoreMovies(
category = categoryEntity,
movies = mapper.mapToEntity(response, false),
page = PAGINATION_FIRST_PAGE_INDEX,
)
}
remoteKeyLocalDataSource.updateNextKey(
type = remoteKeyTypeEntity,
nextKey = (PAGINATION_FIRST_PAGE_INDEX + 1).toString(),
reset = true,
)
}
}
//endregion
//region Movie
override fun movieChanges(movieId: Long): Flow<Movie> =
localDataSource.movieChanges(movieId)
.map(mapper::mapToDomain)
.flowOn(defaultDispatcher)
override suspend fun refreshMovie(movieId: Long) {
defaultDispatcher {
val detailsDeferred = async { remoteDataSource.getMovieDetails(movieId) }
val accountStateDeferred = async {
if (sessionLocalDataSource.getData().account != null) {
remoteDataSource.getMovieAccountStates(movieId)
} else {
null
}
}
localDataSource.insertOrUpdateMovie(
mapper.mapToEntity(
detailsResponse = detailsDeferred.await(),
accountStateResponse = accountStateDeferred.await(),
),
)
}
}
//endregion
}