From c2abaf15a6b93673e0c3d4654fcd6c20e3372871 Mon Sep 17 00:00:00 2001 From: Satyajit Mishra <55646021+smish-hash@users.noreply.github.com> Date: Tue, 3 Jan 2023 14:09:48 +0530 Subject: [PATCH] Added Pagination 3 support - with type specific --- app/build.gradle | 3 + .../smish/abda/data/api/MovieApiService.kt | 2 + .../data/repository/MovieRepositoryImpl.kt | 4 +- .../datasource/MoviePagingSource.kt | 2 + .../datasource/MovieRemoteDataSource.kt | 3 +- .../MovieRemoteDataSourceImpl.kt | 7 +- .../abda/domain/repository/MovieRepository.kt | 3 +- .../smish/abda/domain/usecase/GetMovies.kt | 6 +- .../movieList/components/MovieListScreen.kt | 71 ++--------- .../abda/ui/viewmodel/MoviesViewmodel.kt | 117 +++--------------- 10 files changed, 37 insertions(+), 181 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4de2507..2d1be9f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -84,6 +84,9 @@ dependencies { implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-rc01" implementation "androidx.navigation:navigation-compose:2.5.0-rc01" implementation 'androidx.paging:paging-compose:1.0.0-alpha17' + implementation("androidx.paging:paging-runtime:$paging_version") + // alternatively - without Android dependencies for tests + testImplementation("androidx.paging:paging-common:$paging_version") // coroutines implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1' diff --git a/app/src/main/java/com/smish/abda/data/api/MovieApiService.kt b/app/src/main/java/com/smish/abda/data/api/MovieApiService.kt index 5a08cd3..3cd8194 100644 --- a/app/src/main/java/com/smish/abda/data/api/MovieApiService.kt +++ b/app/src/main/java/com/smish/abda/data/api/MovieApiService.kt @@ -15,6 +15,8 @@ interface MovieApiService { searchQuery: String, @Query("page") page: Int, + @Query("type") + type: String, @Query("apikey") apiKey: String = BuildConfig.MOVIE_API_KEY ): Movie diff --git a/app/src/main/java/com/smish/abda/data/repository/MovieRepositoryImpl.kt b/app/src/main/java/com/smish/abda/data/repository/MovieRepositoryImpl.kt index b1fe935..d0a021b 100644 --- a/app/src/main/java/com/smish/abda/data/repository/MovieRepositoryImpl.kt +++ b/app/src/main/java/com/smish/abda/data/repository/MovieRepositoryImpl.kt @@ -14,8 +14,8 @@ class MovieRepositoryImpl( private val movieLocalDataSource: MovieLocalDataSource ): MovieRepository { - override fun getMovies(searchQuery: String, page: Int): Flow> { - return movieRemoteDataSource.getMovies(searchQuery, page) + override fun getMovies(searchQuery: String, type: String, page: Int): Flow> { + return movieRemoteDataSource.getMovies(searchQuery, type, page) /*if (response.isSuccessful) { response.body()?.let { res -> if (res.response == "True") { diff --git a/app/src/main/java/com/smish/abda/data/repository/datasource/MoviePagingSource.kt b/app/src/main/java/com/smish/abda/data/repository/datasource/MoviePagingSource.kt index 2371706..3e27f16 100644 --- a/app/src/main/java/com/smish/abda/data/repository/datasource/MoviePagingSource.kt +++ b/app/src/main/java/com/smish/abda/data/repository/datasource/MoviePagingSource.kt @@ -11,6 +11,7 @@ private const val OMDB_STARTING_PAGE_INDEX = 1 class MoviePagingSource( private val query: String, + private val type: String, private val movieApiService: MovieApiService ): PagingSource() { @@ -30,6 +31,7 @@ class MoviePagingSource( return try { val response = movieApiService.getMovies( searchQuery = query, + type = type, page = pageIndex ) // can return load state . error here diff --git a/app/src/main/java/com/smish/abda/data/repository/datasource/MovieRemoteDataSource.kt b/app/src/main/java/com/smish/abda/data/repository/datasource/MovieRemoteDataSource.kt index 3709da8..ebc141c 100644 --- a/app/src/main/java/com/smish/abda/data/repository/datasource/MovieRemoteDataSource.kt +++ b/app/src/main/java/com/smish/abda/data/repository/datasource/MovieRemoteDataSource.kt @@ -1,7 +1,6 @@ package com.smish.abda.data.repository.datasource import androidx.paging.PagingData -import com.smish.abda.data.model.movie.Movie import com.smish.abda.data.model.movie.Search import com.smish.abda.data.model.moviedetail.MovieDetail import kotlinx.coroutines.flow.Flow @@ -10,6 +9,6 @@ import retrofit2.Response interface MovieRemoteDataSource { // defining functions to communicate with the API // use for both search and all - fun getMovies(searchQuery: String, page: Int): Flow> + fun getMovies(searchQuery: String, type: String, page: Int): Flow> suspend fun getMovieDetails(imdbId: String): Response } \ No newline at end of file diff --git a/app/src/main/java/com/smish/abda/data/repository/datasourceimpl/MovieRemoteDataSourceImpl.kt b/app/src/main/java/com/smish/abda/data/repository/datasourceimpl/MovieRemoteDataSourceImpl.kt index 6bae0f6..4d95655 100644 --- a/app/src/main/java/com/smish/abda/data/repository/datasourceimpl/MovieRemoteDataSourceImpl.kt +++ b/app/src/main/java/com/smish/abda/data/repository/datasourceimpl/MovieRemoteDataSourceImpl.kt @@ -4,7 +4,6 @@ import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.PagingData import com.smish.abda.data.api.MovieApiService -import com.smish.abda.data.model.movie.Movie import com.smish.abda.data.model.movie.Search import com.smish.abda.data.model.moviedetail.MovieDetail import com.smish.abda.data.repository.datasource.MoviePagingSource @@ -16,16 +15,14 @@ class MovieRemoteDataSourceImpl( private val movieAPI: MovieApiService ): MovieRemoteDataSource { - override fun getMovies(searchQuery: String, page: Int): Flow> { -// return movieAPI.getMovies(searchQuery, page) - // have the implementation here for paging data source, return pager here + override fun getMovies(searchQuery: String, type: String, page: Int): Flow> { return Pager( config = PagingConfig( pageSize = 10, enablePlaceholders = false ), pagingSourceFactory = { - MoviePagingSource(query = searchQuery, movieApiService = movieAPI) + MoviePagingSource(query = searchQuery, type = type, movieApiService = movieAPI) } ).flow } diff --git a/app/src/main/java/com/smish/abda/domain/repository/MovieRepository.kt b/app/src/main/java/com/smish/abda/domain/repository/MovieRepository.kt index 480abd5..1943d00 100644 --- a/app/src/main/java/com/smish/abda/domain/repository/MovieRepository.kt +++ b/app/src/main/java/com/smish/abda/domain/repository/MovieRepository.kt @@ -1,7 +1,6 @@ package com.smish.abda.domain.repository import androidx.paging.PagingData -import com.smish.abda.data.model.movie.Movie import com.smish.abda.data.model.movie.Search import com.smish.abda.data.model.moviedetail.MovieDetail import com.smish.abda.util.Resource @@ -9,7 +8,7 @@ import kotlinx.coroutines.flow.Flow interface MovieRepository { // this will be used for both get all movies and search - fun getMovies(searchQuery: String, page: Int): Flow> + fun getMovies(searchQuery: String, type: String, page: Int): Flow> suspend fun getMovieDetails(imdbId: String): Resource suspend fun bookmarkMovie(movie: Search) diff --git a/app/src/main/java/com/smish/abda/domain/usecase/GetMovies.kt b/app/src/main/java/com/smish/abda/domain/usecase/GetMovies.kt index c24025b..2577d8e 100644 --- a/app/src/main/java/com/smish/abda/domain/usecase/GetMovies.kt +++ b/app/src/main/java/com/smish/abda/domain/usecase/GetMovies.kt @@ -1,14 +1,12 @@ package com.smish.abda.domain.usecase import androidx.paging.PagingData -import com.smish.abda.data.model.movie.Movie import com.smish.abda.data.model.movie.Search import com.smish.abda.domain.repository.MovieRepository -import com.smish.abda.util.Resource import kotlinx.coroutines.flow.Flow class GetMovies(private val movieRepository: MovieRepository) { - fun getMovies(searchQuery: String, page: Int): Flow> { - return movieRepository.getMovies(searchQuery, page) + fun getMovies(searchQuery: String, type: String, page: Int): Flow> { + return movieRepository.getMovies(searchQuery, type, page) } } \ No newline at end of file diff --git a/app/src/main/java/com/smish/abda/ui/movieList/components/MovieListScreen.kt b/app/src/main/java/com/smish/abda/ui/movieList/components/MovieListScreen.kt index 420fa60..b70c9e7 100644 --- a/app/src/main/java/com/smish/abda/ui/movieList/components/MovieListScreen.kt +++ b/app/src/main/java/com/smish/abda/ui/movieList/components/MovieListScreen.kt @@ -1,12 +1,8 @@ package com.smish.abda.ui.movieList.components -import androidx.compose.animation.core.animateIntAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.items import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.RoundedCornerShape @@ -20,7 +16,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.colorResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight @@ -33,7 +28,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.items import com.smish.abda.R import com.smish.abda.data.model.movie.Type import com.smish.abda.data.model.movie.getAllTypes @@ -42,16 +36,12 @@ import com.smish.abda.data.model.moviedetail.MovieDetail import com.smish.abda.ui.viewmodel.MoviesViewmodel import kotlinx.coroutines.launch -enum class ExpandedType { - HALF, FULL, COLLAPSED -} @OptIn(ExperimentalMaterialApi::class) @Composable fun MovieListScreen( viewmodel: MoviesViewmodel = hiltViewModel() ) { - val state = viewmodel.movieState.value val movieDetailState = viewmodel.movieDetailState.value val textState = remember { mutableStateOf(TextFieldValue("")) } @@ -67,19 +57,6 @@ fun MovieListScreen( mutableStateOf(false) } - val configuration = LocalConfiguration.current - val screenHeight = configuration.screenHeightDp - var expandedType by remember { - mutableStateOf(ExpandedType.COLLAPSED) - } - val height by animateIntAsState( - when (expandedType) { - ExpandedType.HALF -> screenHeight / 2 - ExpandedType.FULL -> screenHeight - ExpandedType.COLLAPSED -> 0 - } - ) - val movieList = viewmodel.getPagingMovies().collectAsLazyPagingItems() ModalBottomSheetLayout( sheetState = bottomSheetModalState, @@ -89,7 +66,6 @@ fun MovieListScreen( movie = movieDetailState.movies LaunchedEffect(bottomSheetModalState) { bottomSheetModalState.show() -// bottomSheetScaffoldState.bottomSheetState.expand() } } Column( @@ -140,7 +116,7 @@ fun MovieListScreen( selectedType = selectedType.value, onSelectedChanged = { selectedType.value = getType(it) - viewmodel.getSpecificType(it) + viewmodel.setSpecificType(it) } ) @@ -159,32 +135,6 @@ fun MovieListScreen( } ) - /*LazyColumn( - modifier = Modifier.fillMaxSize() - ) { - items(movieList) { movie -> - if (movie != null) { - val (isChecked, setChecked) = remember { mutableStateOf(false) } - MovieListItem( - movie = movie, - onMovieClick = { - // call the movie detail api and trigger the bottom sheet - showModalSheet.value = !showModalSheet.value - viewmodel.getMovieDetails(movie.imdbID) - }, - isChecked, - onBookmarkClick = { - setChecked(!isChecked) - if (!isChecked) - viewmodel.bookmarkMovie(movie) - else - viewmodel.removeMovie(movie) - } - ) - } - } - }*/ - /*LazyVerticalGrid( modifier = Modifier.fillMaxSize(), columns = GridCells.Adaptive(minSize = 135.dp) @@ -213,7 +163,7 @@ fun MovieListScreen( }*/ } - if (state.error.isNotBlank()) { + /*if (state.error.isNotBlank()) { Text( text = state.error, color = MaterialTheme.colors.error, @@ -223,12 +173,12 @@ fun MovieListScreen( .padding(horizontal = 20.dp) .align(Alignment.Center) ) - } + }*/ // Progress Bar - if (state.isLoading) { + /*if (state.isLoading) { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) - } + }*/ } } @@ -325,8 +275,8 @@ fun ChipGroup( Chip( name = it.value, isSelected = selectedType == it, - onSelectionChanged = { - onSelectedChanged(it) + onSelectionChanged = { type -> + onSelectedChanged(type) }, ) } @@ -365,13 +315,6 @@ fun Chip( } } -@Preview(showBackground = false) -@Composable -fun SearchViewPreview() { - val textState = remember { mutableStateOf(TextFieldValue("")) } -// SearchView(textState) -} - @Preview(showBackground = false) @Composable fun TypeGroupPreview() { diff --git a/app/src/main/java/com/smish/abda/ui/viewmodel/MoviesViewmodel.kt b/app/src/main/java/com/smish/abda/ui/viewmodel/MoviesViewmodel.kt index 3ff2d65..d6a6364 100644 --- a/app/src/main/java/com/smish/abda/ui/viewmodel/MoviesViewmodel.kt +++ b/app/src/main/java/com/smish/abda/ui/viewmodel/MoviesViewmodel.kt @@ -4,25 +4,24 @@ import android.app.Application import android.content.Context import android.net.ConnectivityManager import android.net.NetworkCapabilities -import android.util.Log import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.* +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.liveData +import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn import com.smish.abda.data.model.movie.Movie import com.smish.abda.data.model.movie.Search import com.smish.abda.data.model.movie.Type -import com.smish.abda.data.repository.datasource.MoviePagingSource import com.smish.abda.domain.usecase.* import com.smish.abda.ui.movieList.MovieDetailState -import com.smish.abda.ui.movieList.MovieListState import com.smish.abda.util.Resource import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import javax.inject.Inject @HiltViewModel @@ -37,69 +36,14 @@ class MoviesViewmodel @Inject constructor( val movies: MutableLiveData> = MutableLiveData() - private val _movieState = mutableStateOf(MovieListState()) - val movieState: State = _movieState - - private var topMovies: List? = emptyList() - private val _movieDetailState = mutableStateOf(MovieDetailState()) val movieDetailState: State = _movieDetailState - var query = mutableStateOf("") - private set - - private lateinit var pagingSource : MoviePagingSource - - /*init { - getPagingMovies("") - }*/ - - /*fun getMovies(searchQuery: String, page: Int) = viewModelScope.launch { Dispatchers.IO -// movies.postValue(Resource.Loading()) - _movieState.value = MovieListState(isLoading = true) - try { - if (isNetworkAvailable(app)) { - val result = getMovies.getMovies(searchQuery, page) - result.cachedIn(viewModelScope) - *//*Log.d("movies", "getMovies: ${result.data}") - when (result) { - is Resource.Error -> _movieState.value = - MovieListState(error = result.message ?: "Unexpected Error occurred") - is Resource.Loading -> _movieState.value = MovieListState(isLoading = true) - is Resource.Success -> { - _movieState.value = MovieListState(movies = result.data) - topMovies = _movieState.value.movies?.movies - *//* - *//*if (page == 5) - _movieState.value = MovieListState( - movies = Movie( - "True", - topMovies, - topMovies.size.toString() - ) - ) - else { - result.data?.movies?.forEach { - if (it != null) { - topMovies.add(it) - } - } - }*//**//* - } - }*//* -// movies.postValue(result) - } else { -// movies.postValue(Resource.Error("Internet is not available")) - _movieState.value = MovieListState(error = "Internet is not available") - } - } catch (e: Exception) { -// movies.postValue(Resource.Error(e.message ?: "An unknown error occurred")) - _movieState.value = MovieListState(error = e.message ?: "An unknown error occurred") - } - }*/ + private var query = mutableStateOf("") + private var _type = mutableStateOf("") fun getPagingMovies(): Flow> { - val pager = getMovies.getMovies(query.value, 1) + val pager = getMovies.getMovies(query.value, _type.value, 1) pager.cachedIn(viewModelScope) return pager } @@ -113,9 +57,7 @@ class MoviesViewmodel @Inject constructor( _movieDetailState.value = MovieDetailState(isLoading = true) try { if (isNetworkAvailable(app)) { - val result = getMovieDetail.getMovieDetails(imdbId) - Log.d("movies", "getMovieDetails: ${result.data}") - when (result) { + when (val result = getMovieDetail.getMovieDetails(imdbId)) { is Resource.Error -> _movieDetailState.value = MovieDetailState(error = result.message ?: "Unexpected Error occurred") is Resource.Loading -> _movieDetailState.value = MovieDetailState(isLoading = true) is Resource.Success -> { @@ -123,10 +65,10 @@ class MoviesViewmodel @Inject constructor( } } } else { - _movieState.value = MovieListState(error = "Internet is not available") + _movieDetailState.value = MovieDetailState(error = "Internet is not available") } } catch (e: Exception) { - _movieState.value = MovieListState(error = e.message ?: "An unknown error occurred") + _movieDetailState.value = MovieDetailState(error = e.message ?: "An unknown error occurred") } } @@ -146,50 +88,21 @@ class MoviesViewmodel @Inject constructor( return result } - /*fun performQuery(query: String, page: Int) { - getPagingMovies(query, page) - }*/ - - fun getSpecificType(type: String) { + fun setSpecificType(type: String) { when (type) { Type.HOME.value -> { - if (topMovies.isNullOrEmpty()) - _movieState.value = MovieListState(error = "Unknown error occurred, please try again") - else - _movieState.value = MovieListState(movies = Movie("True", topMovies, topMovies?.size.toString())) + _type.value = "" } Type.MOVIE.value -> { - var movieList = topMovies - movieList = movieList?.filter { - it?.type == Type.MOVIE.value - } - if (movieList.isNullOrEmpty()) - _movieState.value = MovieListState(error = "Match not found") - else - _movieState.value = MovieListState(movies = Movie("True", movieList, movieList.size.toString())) + _type.value = Type.MOVIE.value } Type.SERIES.value -> { - var movieList = topMovies - movieList = movieList?.filter { - it?.type == Type.SERIES.value - } - if (movieList.isNullOrEmpty()) - _movieState.value = MovieListState(error = "Match not found") - else - _movieState.value = MovieListState(movies = Movie("True", movieList, movieList.size.toString())) + _type.value = Type.SERIES.value } - Type.EPISODE.value -> { - var movieList = topMovies - movieList = movieList?.filter { - it?.type == Type.EPISODE.value - } - if (movieList.isNullOrEmpty()) - _movieState.value = MovieListState(error = "Match not found") - else - _movieState.value = MovieListState(movies = Movie("True", movieList, movieList.size.toString())) + _type.value = Type.EPISODE.value } } }