DefaultMovieRepository.kt

  1. package com.louisfn.somovie.data.repository

  2. import androidx.annotation.AnyThread
  3. import androidx.paging.Pager
  4. import androidx.paging.PagingConfig
  5. import androidx.paging.PagingData
  6. import com.louisfn.somovie.core.common.annotation.DefaultDispatcher
  7. import com.louisfn.somovie.data.database.DatabaseHelper
  8. import com.louisfn.somovie.data.database.datasource.MovieLocalDataSource
  9. import com.louisfn.somovie.data.database.datasource.RemoteKeyLocalDataSource
  10. import com.louisfn.somovie.data.datastore.datasource.DataStoreLocalDataSource
  11. import com.louisfn.somovie.data.datastore.model.SessionData
  12. import com.louisfn.somovie.data.mapper.ExploreCategoryMapper
  13. import com.louisfn.somovie.data.mapper.MovieMapper
  14. import com.louisfn.somovie.data.network.Constants.PAGINATION_FIRST_PAGE_INDEX
  15. import com.louisfn.somovie.data.network.datasource.MovieRemoteDataSource
  16. import com.louisfn.somovie.data.repository.paging.MoviesRemoteMediator
  17. import com.louisfn.somovie.data.repository.paging.mapPaging
  18. import com.louisfn.somovie.domain.model.ExploreCategory
  19. import com.louisfn.somovie.domain.model.Movie
  20. import kotlinx.coroutines.CoroutineDispatcher
  21. import kotlinx.coroutines.async
  22. import kotlinx.coroutines.flow.Flow
  23. import kotlinx.coroutines.flow.flowOn
  24. import kotlinx.coroutines.flow.map
  25. import kotlinx.coroutines.invoke
  26. import java.util.concurrent.TimeUnit
  27. import javax.inject.Inject

  28. interface MovieRepository {

  29.     @AnyThread
  30.     fun moviesPagingChanges(
  31.         category: ExploreCategory,
  32.         pagingConfig: PagingConfig,
  33.         cacheTimeout: Long = DEFAULT_CACHE_TIMEOUT,
  34.     ): Flow<PagingData<Movie>>

  35.     @AnyThread
  36.     fun moviesChanges(category: ExploreCategory, limit: Int): Flow<List<Movie>>

  37.     @AnyThread
  38.     suspend fun refreshMovies(category: ExploreCategory, cacheTimeout: Long = DEFAULT_CACHE_TIMEOUT)

  39.     @AnyThread
  40.     fun movieChanges(movieId: Long): Flow<Movie>

  41.     @AnyThread
  42.     suspend fun refreshMovie(movieId: Long)

  43.     companion object {
  44.         private val DEFAULT_CACHE_TIMEOUT = TimeUnit.HOURS.toMillis(12)
  45.     }
  46. }

  47. internal class DefaultMovieRepository @Inject constructor(
  48.     private val remoteDataSource: MovieRemoteDataSource,
  49.     private val localDataSource: MovieLocalDataSource,
  50.     private val remoteKeyLocalDataSource: RemoteKeyLocalDataSource,
  51.     private val mapper: MovieMapper,
  52.     private val categoryMapper: ExploreCategoryMapper,
  53.     private val databaseHelper: DatabaseHelper,
  54.     private val sessionLocalDataSource: DataStoreLocalDataSource<SessionData>,
  55.     @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher,
  56. ) : MovieRepository {

  57.     //region Explore movies

  58.     override fun moviesPagingChanges(
  59.         category: ExploreCategory,
  60.         pagingConfig: PagingConfig,
  61.         cacheTimeout: Long,
  62.     ): Flow<PagingData<Movie>> =
  63.         Pager(
  64.             config = pagingConfig,
  65.             remoteMediator = createMoviesRemoteMediator(category, cacheTimeout),
  66.             pagingSourceFactory = {
  67.                 localDataSource.getPagingMovies(categoryMapper.mapToEntity(category))
  68.             },
  69.         )
  70.             .flow
  71.             .mapPaging { mapper.mapToDomain(it) }
  72.             .flowOn(defaultDispatcher)

  73.     @AnyThread
  74.     private fun createMoviesRemoteMediator(
  75.         category: ExploreCategory,
  76.         cacheTimeout: Long,
  77.     ) = MoviesRemoteMediator(
  78.         cacheTimeout = cacheTimeout,
  79.         remoteKeyLocalDataSource = remoteKeyLocalDataSource,
  80.         localDataSource = localDataSource,
  81.         remoteDataSource = remoteDataSource,
  82.         movieMapper = mapper,
  83.         categoryMapper = categoryMapper,
  84.         databaseHelper = databaseHelper,
  85.         category = category,
  86.     )

  87.     override fun moviesChanges(category: ExploreCategory, limit: Int): Flow<List<Movie>> =
  88.         localDataSource.moviesChanges(categoryMapper.mapToEntity(category), limit)
  89.             .map(mapper::mapToDomain)
  90.             .flowOn(defaultDispatcher)

  91.     override suspend fun refreshMovies(category: ExploreCategory, cacheTimeout: Long) =
  92.         defaultDispatcher {
  93.             val remoteKeyTypeEntity = categoryMapper.mapToRemoteKeyTypeEntity(category)
  94.             if (!remoteKeyLocalDataSource.isExpired(remoteKeyTypeEntity, cacheTimeout)) {
  95.                 return@defaultDispatcher
  96.             }

  97.             val response = remoteDataSource.getMovies(category, PAGINATION_FIRST_PAGE_INDEX).results

  98.             val categoryEntity = categoryMapper.mapToEntity(category)
  99.             databaseHelper.withTransaction {
  100.                 with(localDataSource) {
  101.                     deleteExploreMovies(categoryEntity)
  102.                     insertOrIgnoreMovies(
  103.                         category = categoryEntity,
  104.                         movies = mapper.mapToEntity(response, false),
  105.                         page = PAGINATION_FIRST_PAGE_INDEX,
  106.                     )
  107.                 }
  108.                 remoteKeyLocalDataSource.updateNextKey(
  109.                     type = remoteKeyTypeEntity,
  110.                     nextKey = (PAGINATION_FIRST_PAGE_INDEX + 1).toString(),
  111.                     reset = true,
  112.                 )
  113.             }
  114.         }

  115.     //endregion

  116.     //region Movie

  117.     override fun movieChanges(movieId: Long): Flow<Movie> =
  118.         localDataSource.movieChanges(movieId)
  119.             .map(mapper::mapToDomain)
  120.             .flowOn(defaultDispatcher)

  121.     override suspend fun refreshMovie(movieId: Long) {
  122.         defaultDispatcher {
  123.             val detailsDeferred = async { remoteDataSource.getMovieDetails(movieId) }
  124.             val accountStateDeferred = async {
  125.                 if (sessionLocalDataSource.getData().account != null) {
  126.                     remoteDataSource.getMovieAccountStates(movieId)
  127.                 } else {
  128.                     null
  129.                 }
  130.             }
  131.             localDataSource.insertOrUpdateMovie(
  132.                 mapper.mapToEntity(
  133.                     detailsResponse = detailsDeferred.await(),
  134.                     accountStateResponse = accountStateDeferred.await(),
  135.                 ),
  136.             )
  137.         }
  138.     }

  139.     //endregion
  140. }