diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 119e1391..509dce90 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -72,6 +72,8 @@ kotlin { implementation(libs.room.runtime) implementation(libs.sqlite.bundled) + + implementation(libs.bundles.paging) } commonTest.dependencies { diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/datasources/MovieDetailsRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/datasources/MovieDetailsRepositoryImpl.kt index 25287f11..d1cf7541 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/datasources/MovieDetailsRepositoryImpl.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/datasources/MovieDetailsRepositoryImpl.kt @@ -1,5 +1,8 @@ package com.vickbt.composeApp.data.datasources +import androidx.paging.PagingData +import app.cash.paging.Pager +import app.cash.paging.PagingConfig import com.vickbt.composeApp.data.cache.AppDatabase import com.vickbt.composeApp.data.mappers.toDomain import com.vickbt.composeApp.data.mappers.toEntity @@ -7,6 +10,7 @@ import com.vickbt.composeApp.data.network.models.CastDto import com.vickbt.composeApp.data.network.models.MovieDetailsDto import com.vickbt.composeApp.data.network.models.MovieResultsDto import com.vickbt.composeApp.data.network.utils.safeApiCall +import com.vickbt.composeApp.data.paging.BasePagingSource import com.vickbt.composeApp.domain.models.Cast import com.vickbt.composeApp.domain.models.Movie import com.vickbt.composeApp.domain.models.MovieDetails @@ -25,6 +29,8 @@ class MovieDetailsRepositoryImpl( private val appDatabase: AppDatabase ) : MovieDetailsRepository { + private val pagingConfig = PagingConfig(pageSize = 20, enablePlaceholders = false) + override suspend fun fetchMovieDetails(movieId: Int): Result> { val isMovieCached = isMovieFavorite(movieId = movieId).getOrDefault(flowOf(false)) .firstOrNull() @@ -33,8 +39,8 @@ class MovieDetailsRepositoryImpl( getFavoriteMovie(movieId = movieId) } else { safeApiCall { - httpClient.get(urlString = "movie/$movieId").body().toDomain() - } + httpClient.get(urlString = "movie/$movieId").body().toDomain() + } } } @@ -44,17 +50,21 @@ class MovieDetailsRepositoryImpl( } } - override suspend fun fetchSimilarMovies( - movieId: Int, - page: Int - ): Result?>> { - return safeApiCall { + override suspend fun fetchSimilarMovies(movieId: Int): Result>> { + val pagingSource = BasePagingSource { page -> val response = httpClient.get(urlString = "movie/$movieId/similar") { parameter("page", page) }.body() response.movies?.map { it.toDomain() } } + + return runCatching { + Pager( + config = pagingConfig, + pagingSourceFactory = { pagingSource } + ).flow + } } override suspend fun saveFavoriteMovie(movie: MovieDetails) { diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/datasources/MoviesRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/datasources/MoviesRepositoryImpl.kt index 09c38c90..5ccc5bf0 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/datasources/MoviesRepositoryImpl.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/datasources/MoviesRepositoryImpl.kt @@ -1,8 +1,12 @@ package com.vickbt.composeApp.data.datasources +import app.cash.paging.Pager +import app.cash.paging.PagingConfig +import app.cash.paging.PagingData import com.vickbt.composeApp.data.mappers.toDomain import com.vickbt.composeApp.data.network.models.MovieResultsDto import com.vickbt.composeApp.data.network.utils.safeApiCall +import com.vickbt.composeApp.data.paging.BasePagingSource import com.vickbt.composeApp.domain.models.Movie import com.vickbt.composeApp.domain.repositories.MoviesRepository import io.ktor.client.HttpClient @@ -15,10 +19,12 @@ class MoviesRepositoryImpl( private val httpClient: HttpClient ) : MoviesRepository { - override suspend fun fetchNowPlayingMovies(page: Int): Result?>> { + private val pagingConfig = PagingConfig(pageSize = 20, enablePlaceholders = false) + + override suspend fun fetchNowPlayingMovies(): Result?>> { return safeApiCall { val response = httpClient.get(urlString = "movie/now_playing") { - parameter("page", page) + parameter("page", 1) }.body() response.movies?.map { it.toDomain() } @@ -27,35 +33,55 @@ class MoviesRepositoryImpl( override suspend fun fetchTrendingMovies( mediaType: String, - timeWindow: String, - page: Int - ): Result?>> { - return safeApiCall { + timeWindow: String + ): Result>> { + val pagingSource = BasePagingSource { page -> val response = httpClient.get(urlString = "trending/$mediaType/$timeWindow") { parameter("page", page) - }.body() + }.body().movies - response.movies?.map { it.toDomain() } + response?.map { it.toDomain() } + } + + return runCatching { + Pager( + config = pagingConfig, + pagingSourceFactory = { pagingSource } + ).flow } } - override suspend fun fetchPopularMovies(page: Int): Result?>> { - return safeApiCall { + override suspend fun fetchPopularMovies(): Result>> { + val pagingSource = BasePagingSource { page -> val response = httpClient.get(urlString = "movie/popular") { parameter("page", page) - }.body() + }.body().movies - response.movies?.map { it.toDomain() } + response?.map { it.toDomain() } + } + + return runCatching { + Pager( + config = pagingConfig, + pagingSourceFactory = { pagingSource } + ).flow } } - override suspend fun fetchUpcomingMovies(page: Int): Result?>> { - return safeApiCall { + override suspend fun fetchUpcomingMovies(): Result>> { + val pagingSource = BasePagingSource { page -> val response = httpClient.get(urlString = "movie/upcoming") { parameter("page", page) - }.body() + }.body().movies - response.movies?.map { it.toDomain() } + response?.map { it.toDomain() } + } + + return runCatching { + Pager( + config = pagingConfig, + pagingSourceFactory = { pagingSource } + ).flow } } } diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/datasources/SearchRepositoryImpl.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/datasources/SearchRepositoryImpl.kt index 2590b0e1..fb966062 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/datasources/SearchRepositoryImpl.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/datasources/SearchRepositoryImpl.kt @@ -1,8 +1,11 @@ package com.vickbt.composeApp.data.datasources +import app.cash.paging.Pager +import app.cash.paging.PagingConfig +import app.cash.paging.PagingData import com.vickbt.composeApp.data.mappers.toDomain import com.vickbt.composeApp.data.network.models.MovieResultsDto -import com.vickbt.composeApp.data.network.utils.safeApiCall +import com.vickbt.composeApp.data.paging.BasePagingSource import com.vickbt.composeApp.domain.models.Movie import com.vickbt.composeApp.domain.repositories.SearchRepository import io.ktor.client.HttpClient @@ -15,17 +18,23 @@ class SearchRepositoryImpl( private val httpClient: HttpClient ) : SearchRepository { - override suspend fun searchMovie( - movieName: String, - page: Int - ): Result?>> { - return safeApiCall { + private val pagingConfig = PagingConfig(pageSize = 20, enablePlaceholders = false) + + override suspend fun searchMovie(movieName: String): Result>> { + val pagingSource = BasePagingSource { page -> val response = httpClient.get(urlString = "search/movie") { parameter("query", movieName) parameter("page", page) - }.body() + }.body().movies + + response?.map { it.toDomain() } + } - response.movies?.map { it.toDomain() } + return runCatching { + Pager( + config = pagingConfig, + pagingSourceFactory = { pagingSource } + ).flow } } } diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/network/models/MovieDetailsDto.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/network/models/MovieDetailsDto.kt index 012f9075..67a598da 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/network/models/MovieDetailsDto.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/network/models/MovieDetailsDto.kt @@ -10,7 +10,7 @@ data class MovieDetailsDto( val adult: Boolean? = null, @SerialName("backdrop_path") - val backdropPath: String, + val backdropPath: String? = null, @SerialName("genres") val genres: List? = null, @@ -37,7 +37,7 @@ data class MovieDetailsDto( val popularity: Double? = null, @SerialName("poster_path") - val posterPath: String, + val posterPath: String? = null, @SerialName("release_date") val releaseDate: String? = null, diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/network/models/MovieDto.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/network/models/MovieDto.kt index 5bc90ae2..8724047d 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/network/models/MovieDto.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/network/models/MovieDto.kt @@ -9,7 +9,7 @@ data class MovieDto( val adult: Boolean? = null, @SerialName("backdrop_path") - val backdropPath: String, + val backdropPath: String? = null, @SerialName("genre_ids") val genreIds: List? = null, @@ -21,22 +21,22 @@ data class MovieDto( val originalLanguage: String? = null, @SerialName("original_title") - val originalTitle: String, + val originalTitle: String? = null, @SerialName("overview") - val overview: String, + val overview: String? = null, @SerialName("popularity") val popularity: Double? = null, @SerialName("poster_path") - val posterPath: String, + val posterPath: String? = null, @SerialName("release_date") val releaseDate: String? = null, @SerialName("title") - val title: String, + val title: String? = null, @SerialName("video") val video: Boolean? = null, diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/paging/BasePagingSource.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/paging/BasePagingSource.kt new file mode 100644 index 00000000..04fe16b2 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/data/paging/BasePagingSource.kt @@ -0,0 +1,32 @@ +package com.vickbt.composeApp.data.paging + +import app.cash.paging.PagingSource +import app.cash.paging.PagingState +import com.vickbt.composeApp.domain.utils.Constants.STARTING_PAGE_INDEX + +class BasePagingSource(val fetchData: suspend (page: Int) -> List?) : + PagingSource() { + + override suspend fun load(params: LoadParams): LoadResult { + val page = params.key ?: STARTING_PAGE_INDEX + + val data = fetchData(page) ?: emptyList() + + return try { + LoadResult.Page( + data = data, + prevKey = if (page == 1) null else page - 1, + nextKey = if (data.isEmpty()) null else page + 1 + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/models/Movie.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/models/Movie.kt index 85b4f1aa..aebf4b35 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/models/Movie.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/models/Movie.kt @@ -3,23 +3,23 @@ package com.vickbt.composeApp.domain.models data class Movie( val adult: Boolean? = null, - val backdropPath: String, + val backdropPath: String? = null, val id: Int, val originalLanguage: String? = null, - val originalTitle: String, + val originalTitle: String? = null, - val overview: String, + val overview: String? = null, val popularity: Double? = null, - val posterPath: String, + val posterPath: String? = null, val releaseDate: String? = null, - val title: String, + val title: String? = null, val video: Boolean? = null, diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/models/MovieDetails.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/models/MovieDetails.kt index 416f2562..e4f1ada9 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/models/MovieDetails.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/models/MovieDetails.kt @@ -4,7 +4,7 @@ data class MovieDetails( val adult: Boolean? = null, - val backdropPath: String, + val backdropPath: String? = null, val homepage: String? = null, @@ -20,7 +20,7 @@ data class MovieDetails( val popularity: Double? = null, - val posterPath: String, + val posterPath: String? = null, val releaseDate: String? = null, diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/repositories/MovieDetailsRepository.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/repositories/MovieDetailsRepository.kt index c78a3243..e6a65156 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/repositories/MovieDetailsRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/repositories/MovieDetailsRepository.kt @@ -1,9 +1,9 @@ package com.vickbt.composeApp.domain.repositories +import androidx.paging.PagingData import com.vickbt.composeApp.domain.models.Cast import com.vickbt.composeApp.domain.models.Movie import com.vickbt.composeApp.domain.models.MovieDetails -import com.vickbt.composeApp.domain.utils.Constants.STARTING_PAGE_INDEX import kotlinx.coroutines.flow.Flow interface MovieDetailsRepository { @@ -15,10 +15,7 @@ interface MovieDetailsRepository { suspend fun fetchMovieCast(movieId: Int): Result> /** Fetches similar movies from network source*/ - suspend fun fetchSimilarMovies( - movieId: Int, - page: Int = STARTING_PAGE_INDEX - ): Result?>> + suspend fun fetchSimilarMovies(movieId: Int): Result>> /**Save movie details to local cache*/ suspend fun saveFavoriteMovie(movie: MovieDetails) diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/repositories/MoviesRepository.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/repositories/MoviesRepository.kt index 1194b532..ab427421 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/repositories/MoviesRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/repositories/MoviesRepository.kt @@ -1,24 +1,23 @@ package com.vickbt.composeApp.domain.repositories +import app.cash.paging.PagingData import com.vickbt.composeApp.domain.models.Movie -import com.vickbt.composeApp.domain.utils.Constants.STARTING_PAGE_INDEX import kotlinx.coroutines.flow.Flow interface MoviesRepository { /** Fetch Now Playing movies from data source*/ - suspend fun fetchNowPlayingMovies(page: Int = STARTING_PAGE_INDEX): Result?>> + suspend fun fetchNowPlayingMovies(): Result?>> /** Fetch Trending movies from data source*/ suspend fun fetchTrendingMovies( mediaType: String = "movie", - timeWindow: String = "week", - page: Int = STARTING_PAGE_INDEX - ): Result?>> + timeWindow: String = "week" + ): Result>> /** Fetch Popular movies from data source*/ - suspend fun fetchPopularMovies(page: Int = STARTING_PAGE_INDEX): Result?>> + suspend fun fetchPopularMovies(): Result>> /** Fetch Upcoming movies from data source*/ - suspend fun fetchUpcomingMovies(page: Int = STARTING_PAGE_INDEX): Result?>> + suspend fun fetchUpcomingMovies(): Result>> } diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/repositories/SearchRepository.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/repositories/SearchRepository.kt index 35a407de..8ac29653 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/repositories/SearchRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/domain/repositories/SearchRepository.kt @@ -1,14 +1,11 @@ package com.vickbt.composeApp.domain.repositories +import app.cash.paging.PagingData import com.vickbt.composeApp.domain.models.Movie -import com.vickbt.composeApp.domain.utils.Constants.STARTING_PAGE_INDEX import kotlinx.coroutines.flow.Flow interface SearchRepository { // Search movie from network source - suspend fun searchMovie( - movieName: String, - page: Int = STARTING_PAGE_INDEX - ): Result?>> + suspend fun searchMovie(movieName: String): Result>> } diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/components/MovieCardLandscape.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/components/MovieCardLandscape.kt index c8960738..b9aaa0a7 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/components/MovieCardLandscape.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/components/MovieCardLandscape.kt @@ -41,6 +41,9 @@ import com.vickbt.composeApp.utils.capitalizeEachWord import com.vickbt.composeApp.utils.getRating import com.vickbt.composeApp.utils.getReleaseDate import com.vickbt.composeApp.utils.loadImage +import com.vickbt.shared.resources.Res +import com.vickbt.shared.resources.unknown_movie +import org.jetbrains.compose.resources.stringResource @Composable fun MovieCardLandscape( @@ -94,7 +97,7 @@ fun MovieCardLandscape( //region Movie Title Text( modifier = Modifier, - text = movie.title, + text = movie.title ?: stringResource(Res.string.unknown_movie), fontSize = 18.sp, maxLines = 2, style = MaterialTheme.typography.titleMedium, diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/components/MovieCardPager.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/components/MovieCardPager.kt index 45a1c586..8b3509b6 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/components/MovieCardPager.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/components/MovieCardPager.kt @@ -39,6 +39,9 @@ import com.vickbt.composeApp.ui.components.ratingbar.RatingBarStyle import com.vickbt.composeApp.ui.components.ratingbar.StepSize import com.vickbt.composeApp.utils.getRating import com.vickbt.composeApp.utils.loadImage +import com.vickbt.shared.resources.Res +import com.vickbt.shared.resources.unknown_movie +import org.jetbrains.compose.resources.stringResource @Composable fun MovieCardPager( @@ -86,7 +89,7 @@ fun MovieCardPager( ) { Text( modifier = Modifier, - text = movie.title, + text = movie.title ?: stringResource(Res.string.unknown_movie), fontSize = 28.sp, maxLines = 2, style = MaterialTheme.typography.titleMedium, diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/components/MovieCardPortraitCompact.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/components/MovieCardPortraitCompact.kt index a838289e..fa6cf36d 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/components/MovieCardPortraitCompact.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/components/MovieCardPortraitCompact.kt @@ -24,6 +24,9 @@ import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import com.vickbt.composeApp.domain.models.Movie import com.vickbt.composeApp.utils.loadImage +import com.vickbt.shared.resources.Res +import com.vickbt.shared.resources.unknown_movie +import org.jetbrains.compose.resources.stringResource @Composable fun MovieCardPortraitCompact( @@ -57,7 +60,7 @@ fun MovieCardPortraitCompact( Text( modifier = Modifier.width(145.dp), - text = movie.title, + text = movie.title ?: stringResource(Res.string.unknown_movie), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface, fontSize = 14.sp, @@ -67,8 +70,3 @@ fun MovieCardPortraitCompact( ) } } - -@Composable -private fun Preview() { - // MovieCardPortraitCompact(movie = Movie(title = "Cocaine Bear"), onItemClick = {}) -} diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/details/DetailsScreen.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/details/DetailsScreen.kt index 71165fbe..72bd0ecf 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/details/DetailsScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/details/DetailsScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController +import app.cash.paging.compose.collectAsLazyPagingItems import com.vickbt.composeApp.ui.components.ItemMovieCast import com.vickbt.composeApp.ui.components.MovieCardPortrait import com.vickbt.composeApp.ui.components.MovieRatingSection @@ -152,7 +153,8 @@ fun DetailsScreen( //endregion //region Similar Movies - if (!movieDetailsState.similarMovies.isNullOrEmpty()) { + movieDetailsState.similarMovies?.let { + val similarMovies = it.collectAsLazyPagingItems() Text( modifier = Modifier.padding(horizontal = 16.dp), text = stringResource(Res.string.similar_movies), @@ -165,8 +167,10 @@ fun DetailsScreen( contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(10.dp) ) { - items(items = movieDetailsState.similarMovies) { movie -> - MovieCardPortrait(movie = movie, onItemClick = {}) + items(similarMovies.itemCount) { index -> + similarMovies[index]?.let { movie -> + MovieCardPortrait(movie = movie, onItemClick = {}) + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/details/DetailsViewModel.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/details/DetailsViewModel.kt index 7e3cc326..7765b070 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/details/DetailsViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/details/DetailsViewModel.kt @@ -2,6 +2,7 @@ package com.vickbt.composeApp.ui.screens.details import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.cachedIn import com.vickbt.composeApp.domain.models.MovieDetails import com.vickbt.composeApp.domain.repositories.MovieDetailsRepository import com.vickbt.composeApp.utils.DetailsUiState @@ -48,8 +49,11 @@ class DetailsViewModel( fun fetchSimilarMovies(movieId: Int) = viewModelScope.launch(coroutineExceptionHandler) { _movieDetailsState.update { it.copy(isLoading = true) } movieDetailsRepository.fetchSimilarMovies(movieId).onSuccess { data -> - data.collectLatest { movies -> - _movieDetailsState.update { it.copy(similarMovies = movies, isLoading = false) } + _movieDetailsState.update { + it.copy( + similarMovies = data.cachedIn(viewModelScope), + isLoading = false + ) } }.onFailure { error -> _movieDetailsState.update { it.copy(error = error.message, isLoading = false) } diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/home/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/home/HomeScreen.kt index c3ff85fe..191dcb90 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/home/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/home/HomeScreen.kt @@ -12,13 +12,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -29,10 +27,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import com.vickbt.shared.resources.Res -import com.vickbt.shared.resources.popular_movies -import com.vickbt.shared.resources.trending_movies -import com.vickbt.shared.resources.upcoming_movies +import app.cash.paging.compose.collectAsLazyPagingItems import com.vickbt.composeApp.ui.components.MovieCardLandscape import com.vickbt.composeApp.ui.components.MovieCardPager import com.vickbt.composeApp.ui.components.MovieCardPagerIndicator @@ -41,10 +36,14 @@ import com.vickbt.composeApp.ui.components.SectionSeparator import com.vickbt.composeApp.ui.components.appbars.AppBar import com.vickbt.composeApp.ui.theme.DarkPrimaryColor import com.vickbt.composeApp.utils.WindowSize +import com.vickbt.shared.resources.Res +import com.vickbt.shared.resources.popular_movies +import com.vickbt.shared.resources.trending_movies +import com.vickbt.shared.resources.upcoming_movies import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel -@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalFoundationApi::class) @Composable fun HomeScreen( navigator: NavHostController, @@ -56,6 +55,8 @@ fun HomeScreen( val homeUiState = viewModel.homeUiState.collectAsState().value + val movies = homeUiState.trendingMovies?.collectAsLazyPagingItems() + Scaffold( modifier = Modifier .fillMaxSize() @@ -115,7 +116,9 @@ fun HomeScreen( //endregion //region Trending Movies - homeUiState.trendingMovies?.let { + homeUiState.trendingMovies?.let { movies -> + val trendingMovies = movies.collectAsLazyPagingItems() + SectionSeparator( modifier = Modifier .fillMaxWidth() @@ -130,20 +133,24 @@ fun HomeScreen( contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(10.dp) ) { - items(items = it) { item -> - MovieCardPortraitCompact( - movie = item, - onItemClick = { movie -> - navigator.navigate("/details/${movie.id}") - } - ) + items(trendingMovies.itemCount) { index -> + trendingMovies[index]?.let { + MovieCardPortraitCompact( + movie = it, + onItemClick = { movie -> + navigator.navigate("/details/${movie.id}") + } + ) + } } } } //endregion //region Upcoming Movies - homeUiState.upcomingMovies?.let { + homeUiState.upcomingMovies?.let { movies -> + val upcomingMovies = movies.collectAsLazyPagingItems() + SectionSeparator( modifier = Modifier .fillMaxWidth() @@ -156,23 +163,27 @@ fun HomeScreen( horizontalArrangement = Arrangement.spacedBy(14.dp), modifier = Modifier.wrapContentHeight() ) { - items(items = it) { item -> - MovieCardLandscape( - modifier = Modifier - .width(300.dp) - .height(245.dp), - movie = item, - onClickItem = { movie -> - navigator.navigate("/details/${movie.id}") - } - ) + items(upcomingMovies.itemCount) { index -> + upcomingMovies[index]?.let { + MovieCardLandscape( + modifier = Modifier + .width(300.dp) + .height(245.dp), + movie = it, + onClickItem = { movie -> + navigator.navigate("/details/${movie.id}") + } + ) + } } } } //endregion //region Popular Movies - homeUiState.popularMovies?.let { + homeUiState.popularMovies?.let { movies -> + val popularMovies = movies.collectAsLazyPagingItems() + Column(modifier = Modifier.padding(bottom = 90.dp)) { SectionSeparator( modifier = Modifier @@ -186,13 +197,15 @@ fun HomeScreen( contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(10.dp), ) { - items(items = it) { item -> - MovieCardPortraitCompact( - movie = item, - onItemClick = { movie -> - navigator.navigate("/details/${movie.id}") - } - ) + items(popularMovies.itemCount) { index -> + popularMovies[index]?.let { + MovieCardPortraitCompact( + movie = it, + onItemClick = { movie -> + navigator.navigate("/details/${movie.id}") + } + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/home/HomeViewModel.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/home/HomeViewModel.kt index 168d0dbc..4fcd87c7 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/home/HomeViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/home/HomeViewModel.kt @@ -2,6 +2,7 @@ package com.vickbt.composeApp.ui.screens.home import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.cachedIn import com.vickbt.composeApp.domain.repositories.MoviesRepository import com.vickbt.composeApp.utils.HomeUiState import kotlinx.coroutines.CoroutineExceptionHandler @@ -44,8 +45,11 @@ class HomeViewModel(private val moviesRepository: MoviesRepository) : ViewModel( fun fetchTrendingMovies() = viewModelScope.launch(coroutineExceptionHandler) { moviesRepository.fetchTrendingMovies().onSuccess { data -> - data.collectLatest { movies -> - _homeUiState.update { it.copy(trendingMovies = movies, isLoading = false) } + _homeUiState.update { + it.copy( + trendingMovies = data.cachedIn(viewModelScope), + isLoading = false + ) } }.onFailure { error -> _homeUiState.update { it.copy(error = error.message, isLoading = false) } @@ -54,8 +58,11 @@ class HomeViewModel(private val moviesRepository: MoviesRepository) : ViewModel( fun fetchPopularMovies() = viewModelScope.launch(coroutineExceptionHandler) { moviesRepository.fetchPopularMovies().onSuccess { data -> - data.collectLatest { movies -> - _homeUiState.update { it.copy(popularMovies = movies, isLoading = false) } + _homeUiState.update { + it.copy( + popularMovies = data.cachedIn(viewModelScope), + isLoading = false + ) } }.onFailure { error -> _homeUiState.update { it.copy(error = error.message, isLoading = false) } @@ -64,8 +71,11 @@ class HomeViewModel(private val moviesRepository: MoviesRepository) : ViewModel( fun fetchUpcomingMovies() = viewModelScope.launch(coroutineExceptionHandler) { moviesRepository.fetchUpcomingMovies().onSuccess { data -> - data.collectLatest { movies -> - _homeUiState.update { it.copy(upcomingMovies = movies, isLoading = false) } + _homeUiState.update { + it.copy( + upcomingMovies = data.cachedIn(viewModelScope), + isLoading = false + ) } }.onFailure { error -> _homeUiState.update { it.copy(error = error.message, isLoading = false) } diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/search/SearchScreen.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/search/SearchScreen.kt index b4df5572..54f58d2d 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/search/SearchScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/search/SearchScreen.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.rounded.Close @@ -38,10 +37,11 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavHostController -import com.vickbt.shared.resources.Res -import com.vickbt.shared.resources.title_search +import app.cash.paging.compose.collectAsLazyPagingItems import com.vickbt.composeApp.ui.components.MovieCardPortrait import com.vickbt.composeApp.utils.WindowSize +import com.vickbt.shared.resources.Res +import com.vickbt.shared.resources.title_search import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import org.koin.core.annotation.KoinExperimentalAPI @@ -120,6 +120,8 @@ fun SearchScreen( ) } else { searchUiState.movieResults?.let { movieResults -> + val searchResults = movieResults.collectAsLazyPagingItems() + LazyVerticalGrid( modifier = Modifier.fillMaxSize(), columns = if (windowSize == WindowSize.COMPACT) { @@ -131,14 +133,16 @@ fun SearchScreen( verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp) ) { - items(items = movieResults) { item -> - MovieCardPortrait( - modifier = Modifier, - movie = item, - onItemClick = { movie -> - navigator.navigate("/details/${movie.id}") - } - ) + items(searchResults.itemCount) { index -> + searchResults[index]?.let { movies -> + MovieCardPortrait( + modifier = Modifier, + movie = movies, + onItemClick = { movie -> + navigator.navigate("/details/${movie.id}") + } + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/search/SearchViewModel.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/search/SearchViewModel.kt index a348415e..956b11d8 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/search/SearchViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/ui/screens/search/SearchViewModel.kt @@ -2,12 +2,12 @@ package com.vickbt.composeApp.ui.screens.search import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.cachedIn import com.vickbt.composeApp.domain.repositories.SearchRepository import com.vickbt.composeApp.utils.SearchUiState import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -16,9 +16,6 @@ class SearchViewModel(private val searchRepository: SearchRepository) : ViewMode private val _searchUiState = MutableStateFlow(SearchUiState(isLoading = false)) val searchUiState = _searchUiState.asStateFlow() - private val _searchQuery = MutableStateFlow("") - val searchQuery = _searchQuery.asStateFlow() - private val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception -> _searchUiState.update { it.copy(isLoading = false, error = exception.message) } } @@ -27,8 +24,11 @@ class SearchViewModel(private val searchRepository: SearchRepository) : ViewMode _searchUiState.update { it.copy(isLoading = true) } searchRepository.searchMovie(movieName = movieName).onSuccess { data -> - data.collectLatest { movies -> - _searchUiState.update { it.copy(movieResults = movies, isLoading = false) } + _searchUiState.update { + it.copy( + movieResults = data.cachedIn(viewModelScope), + isLoading = false + ) } }.onFailure { error -> _searchUiState.update { it.copy(error = error.message, isLoading = false) } diff --git a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/utils/UiStates.kt b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/utils/UiStates.kt index a45b4059..8a14d4db 100644 --- a/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/utils/UiStates.kt +++ b/composeApp/src/commonMain/kotlin/com/vickbt/composeApp/utils/UiStates.kt @@ -1,8 +1,10 @@ package com.vickbt.composeApp.utils +import androidx.paging.PagingData import com.vickbt.composeApp.domain.models.Actor import com.vickbt.composeApp.domain.models.Movie import com.vickbt.composeApp.domain.models.MovieDetails +import kotlinx.coroutines.flow.Flow data class MainUiState( val selectedTheme: Int? = 0 @@ -12,9 +14,9 @@ data class HomeUiState( val isLoading: Boolean = true, val error: String? = null, val nowPlayingMovies: List? = emptyList(), - val trendingMovies: List? = emptyList(), - val popularMovies: List? = emptyList(), - val upcomingMovies: List? = emptyList(), + val trendingMovies: Flow>? = null, + val popularMovies: Flow>? = null, + val upcomingMovies: Flow>? = null, ) data class DetailsUiState( @@ -22,14 +24,14 @@ data class DetailsUiState( val error: String? = null, val movieDetails: MovieDetails? = null, val movieCast: List? = emptyList(), - val similarMovies: List? = emptyList(), + val similarMovies: Flow>? = null, val isFavorite: Boolean? = false ) data class SearchUiState( val isLoading: Boolean = true, val error: String? = null, - val movieResults: List? = emptyList() + val movieResults: Flow>? = null ) data class FavouritesUiState( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4a734f32..da510e1e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,6 @@ kotlinxSerializationJson = "1.7.1" kotlinxDateTime = "0.6.0" napier = "2.7.1" ktor = "2.3.12" -sqlDelight = "2.0.2" buildKonfig = "0.13.3" materialWindowSizeClass = "0.3.0" navigation = "2.7.0-alpha07" @@ -29,6 +28,7 @@ datastore = "1.1.1" ksp = "2.0.20-1.0.24" sqlite = "2.5.0-SNAPSHOT" room = "2.7.0-alpha08" +paging = "3.3.0-alpha02-0.5.1" #Android Versions androidxActivity = "1.8.2" @@ -85,9 +85,11 @@ coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" coil-compose-core = { module = "io.coil-kt.coil3:coil-compose-core", version.ref = "coil" } coil-ktor = { module = "io.coil-kt.coil3:coil-network-ktor", version.ref = "coil" } coil-multiplatform = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } -room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } -room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } +room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } +room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" } +paging-common = { module = "app.cash.paging:paging-common", version.ref = "paging" } +paging-compose = { module = "app.cash.paging:paging-compose-common", version.ref = "paging" } #Android Lib Dependencies androidX-core = { module = "androidx.core:core-ktx", version.ref = "androidxCore" } @@ -111,4 +113,6 @@ turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } [bundles] ktor = ["ktor-core", "ktor-contentNegotiation", "ktor-json", "ktor-logging"] -coil = ["coil-compose-core", "coil-compose", "coil-ktor", "coil-multiplatform"] \ No newline at end of file +coil = ["coil-compose-core", "coil-compose", "coil-ktor", "coil-multiplatform"] +paging = ["paging-common", "paging-compose"] +#paging = ["paging-compose"] \ No newline at end of file