From 05afce953ed73283021dc28da66d21cb755884ea Mon Sep 17 00:00:00 2001 From: Owen LeJeune Date: Thu, 10 Feb 2022 00:01:03 -0500 Subject: [PATCH] show scrolling list of movies --- app/build.gradle | 5 ++ .../com/owenlejeune/tvtime/api/QueryParam.kt | 7 ++ .../owenlejeune/tvtime/api/tmdb/MoviesApi.kt | 14 ++++ .../tvtime/api/tmdb/MoviesService.kt | 32 +++++++++ .../owenlejeune/tvtime/api/tmdb/TmdbClient.kt | 39 ++++++++++ .../tvtime/api/tmdb/model/PopularMovie.kt | 8 +++ .../api/tmdb/model/PopularMoviesResponse.kt | 9 +++ .../api/tmdb/paging/PopularMovieSource.kt | 38 ++++++++++ .../tmdb/viewmodel/PopularMovieViewModel.kt | 17 +++++ .../tvtime/extensions/ComposeExtensions.kt | 27 +++++++ .../extensions/InterceptorExtensions.kt | 21 ++++++ .../tvtime/extensions/NumberExtensions.kt | 7 ++ .../tvtime/ui/screens/MoviesTab.kt | 72 +++++++++++++++---- app/src/main/res/drawable/placeholder.xml | 6 ++ .../tvtime/buildsrc/Dependencies.kt | 6 ++ .../owenlejeune/tvtime/buildsrc/Versions.kt | 3 + 16 files changed, 297 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/QueryParam.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/PopularMovie.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/PopularMoviesResponse.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/paging/PopularMovieSource.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/api/tmdb/viewmodel/PopularMovieViewModel.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/extensions/InterceptorExtensions.kt create mode 100644 app/src/main/java/com/owenlejeune/tvtime/extensions/NumberExtensions.kt create mode 100644 app/src/main/res/drawable/placeholder.xml diff --git a/app/build.gradle b/app/build.gradle index b0e7263..4c664e5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,6 +38,7 @@ android { } kotlinOptions { jvmTarget = '1.8' + useIR = true } buildFeatures { compose true @@ -55,6 +56,7 @@ android { dependencies { implementation Dependencies.AndroidX.ktxCore + implementation Dependencies.AndroidX.paging implementation Dependencies.Compose.ui implementation Dependencies.Compose.material3 @@ -63,6 +65,7 @@ dependencies { implementation Dependencies.Compose.activity implementation Dependencies.Compose.accompanistSystemUi implementation Dependencies.Compose.navigation + implementation Dependencies.Compose.paging implementation Dependencies.Lifecycle.runtime @@ -74,6 +77,8 @@ dependencies { implementation Dependencies.DI.koin + implementation Dependencies.Coil.coil + testImplementation Dependencies.Testing.junit androidTestImplementation Dependencies.Testing.androidXJunit androidTestImplementation Dependencies.Testing.espressoCore diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/QueryParam.kt b/app/src/main/java/com/owenlejeune/tvtime/api/QueryParam.kt new file mode 100644 index 0000000..901c6a4 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/QueryParam.kt @@ -0,0 +1,7 @@ +package com.owenlejeune.tvtime.api + +class QueryParam(val key: String, val param: String) { + + constructor(key: String, param: Any): this(key, param.toString()) + +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt new file mode 100644 index 0000000..a5073dc --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesApi.kt @@ -0,0 +1,14 @@ +package com.owenlejeune.tvtime.api.tmdb + +import com.owenlejeune.tvtime.api.tmdb.model.PopularMoviesResponse +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Query + +interface MoviesApi { + + @GET("movie/popular") + fun getPopularMovies(@Query("page") page: Int = 1): Call +// suspend fun getPopularMovies(@Query("page") page: Int = 1): PopularMoviesResponse + +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt new file mode 100644 index 0000000..cca35a9 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/MoviesService.kt @@ -0,0 +1,32 @@ +package com.owenlejeune.tvtime.api.tmdb + +import com.owenlejeune.tvtime.api.tmdb.model.PopularMoviesResponse +import org.koin.core.component.KoinComponent +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class MoviesService: KoinComponent { + + private val service by lazy { TmdbClient().createMovieService() } + + fun getPopularMovies(page: Int = 1, callback: (isSuccessful: Boolean, response: PopularMoviesResponse?) -> Unit) { + service.getPopularMovies(page = page).enqueue(object : Callback { + override fun onResponse( + call: Call, + response: Response + ) { + response.body()?.let { body -> + callback.invoke(true, body) + } ?: run { + callback.invoke(false, null) + } + } + + override fun onFailure(call: Call, t: Throwable) { + callback.invoke(false, null) + } + }) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt new file mode 100644 index 0000000..17393e4 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/TmdbClient.kt @@ -0,0 +1,39 @@ +package com.owenlejeune.tvtime.api.tmdb + +import com.owenlejeune.tvtime.BuildConfig +import com.owenlejeune.tvtime.api.Client +import com.owenlejeune.tvtime.api.QueryParam +import com.owenlejeune.tvtime.extensions.addQueryParams +import okhttp3.Interceptor +import okhttp3.Response +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject +import org.koin.core.parameter.parametersOf + +class TmdbClient: KoinComponent { + + companion object { + const val BASE_URL = "https://api.themoviedb.org/3/" + } + + private val client: Client by inject { parametersOf(BASE_URL) } + + init { + client.addInterceptor(TmdbInterceptor()) + } + + fun createMovieService(): MoviesApi { + return client.create(MoviesApi::class.java) + } + + private inner class TmdbInterceptor: Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val apiParam = QueryParam("api_key", BuildConfig.TMDB_ApiKey) + + val request = chain.addQueryParams(apiParam) + + return chain.proceed(request) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/PopularMovie.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/PopularMovie.kt new file mode 100644 index 0000000..405dd53 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/PopularMovie.kt @@ -0,0 +1,8 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +class PopularMovie( + @SerializedName("poster_path") val posterPath: String?, + @SerializedName("title") val title: String +) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/PopularMoviesResponse.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/PopularMoviesResponse.kt new file mode 100644 index 0000000..5c6a1c2 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/model/PopularMoviesResponse.kt @@ -0,0 +1,9 @@ +package com.owenlejeune.tvtime.api.tmdb.model + +import com.google.gson.annotations.SerializedName + +data class PopularMoviesResponse( + @SerializedName("total_results") val count: Int, + @SerializedName("page") val page: Int, + @SerializedName("results") val movies: List +) \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/paging/PopularMovieSource.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/paging/PopularMovieSource.kt new file mode 100644 index 0000000..a86d160 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/paging/PopularMovieSource.kt @@ -0,0 +1,38 @@ +//package com.owenlejeune.tvtime.api.tmdb.paging +// +//import androidx.paging.PagingSource +//import androidx.paging.PagingState +//import com.owenlejeune.tvtime.api.tmdb.TmdbClient +//import com.owenlejeune.tvtime.api.tmdb.model.PopularMovie +//import retrofit2.HttpException +//import java.io.IOException +// +//class PopularMovieSource: PagingSource() { +// +// companion object { +// const val MIN_PAGE = 1 +// const val MAX_PAGE = 1000 +// } +// +// private val movieService by lazy { TmdbClient().createMovieService() } +// +// override fun getRefreshKey(state: PagingState): Int? { +// return state.anchorPosition +// } +// +// override suspend fun load(params: LoadParams): LoadResult { +// return try { +// val nextPage = params.key ?: 1 +// val movieList = movieService.getPopularMovies(page = nextPage) +// LoadResult.Page( +// data = movieList.movies, +// prevKey = if (nextPage == MIN_PAGE) null else nextPage - 1, +// nextKey = if (movieList.count == 0 || nextPage > MAX_PAGE) null else movieList.page + 1 +// ) +// } catch (exception: IOException) { +// return LoadResult.Error(exception) +// } catch (exception: HttpException) { +// return LoadResult.Error(exception) +// } +// } +//} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/viewmodel/PopularMovieViewModel.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/viewmodel/PopularMovieViewModel.kt new file mode 100644 index 0000000..fffac41 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/viewmodel/PopularMovieViewModel.kt @@ -0,0 +1,17 @@ +//package com.owenlejeune.tvtime.api.tmdb.viewmodel +// +//import androidx.lifecycle.ViewModel +//import androidx.lifecycle.viewModelScope +//import androidx.paging.Pager +//import androidx.paging.PagingConfig +//import androidx.paging.PagingData +//import androidx.paging.cachedIn +//import com.owenlejeune.tvtime.api.tmdb.model.PopularMovie +//import com.owenlejeune.tvtime.api.tmdb.paging.PopularMovieSource +//import kotlinx.coroutines.flow.Flow +// +//class PopularMovieViewModel: ViewModel() { +// val moviePage: Flow> = Pager(PagingConfig(pageSize = PopularMovieSource.MAX_PAGE)) { +// PopularMovieSource() +// }.flow.cachedIn(viewModelScope) +//} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt b/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt new file mode 100644 index 0000000..343aacd --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/extensions/ComposeExtensions.kt @@ -0,0 +1,27 @@ +package com.owenlejeune.tvtime.extensions + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.lazy.LazyGridScope +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.runtime.Composable +import androidx.paging.compose.LazyPagingItems + +@OptIn(ExperimentalFoundationApi::class) +fun LazyGridScope.items( + lazyPagingItems: LazyPagingItems, + itemContent: @Composable LazyItemScope.(value: T?) -> Unit +) { + items(lazyPagingItems.itemCount) { index -> + itemContent(lazyPagingItems[index]) + } +} + +@OptIn(ExperimentalFoundationApi::class) +fun LazyGridScope.items( + items: List, + itemContent: @Composable (value: T?) -> Unit +) { + items(items.size) { index -> + itemContent(items[index]) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/extensions/InterceptorExtensions.kt b/app/src/main/java/com/owenlejeune/tvtime/extensions/InterceptorExtensions.kt new file mode 100644 index 0000000..17982d8 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/extensions/InterceptorExtensions.kt @@ -0,0 +1,21 @@ +package com.owenlejeune.tvtime.extensions + +import com.owenlejeune.tvtime.api.QueryParam +import okhttp3.Interceptor +import okhttp3.Request + +fun Interceptor.Chain.addQueryParams(vararg queryParams: QueryParam): Request { + val original = request() + val originalHttpUrl = original.url() + + val urlBuilder = originalHttpUrl.newBuilder() + queryParams.forEach { param -> + urlBuilder.addQueryParameter(param.key, param.param) + } + val url = urlBuilder.build() + + val requestBuilder = original.newBuilder() + .url(url) + + return requestBuilder.build() +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/extensions/NumberExtensions.kt b/app/src/main/java/com/owenlejeune/tvtime/extensions/NumberExtensions.kt new file mode 100644 index 0000000..d16c743 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/extensions/NumberExtensions.kt @@ -0,0 +1,7 @@ +package com.owenlejeune.tvtime.extensions + +import android.content.Context + +fun Float.dpToPx(context: Context): Float { + return this * context.resources.displayMetrics.density +} \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MoviesTab.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MoviesTab.kt index 5fe802d..231c615 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MoviesTab.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MoviesTab.kt @@ -1,24 +1,68 @@ package com.owenlejeune.tvtime.ui.screens -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import android.widget.Toast +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.GridCells +import androidx.compose.foundation.lazy.LazyVerticalGrid import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import coil.compose.rememberImagePainter +import coil.transform.RoundedCornersTransformation +import com.owenlejeune.tvtime.R +import com.owenlejeune.tvtime.api.tmdb.MoviesService +import com.owenlejeune.tvtime.api.tmdb.model.PopularMovie +import com.owenlejeune.tvtime.extensions.dpToPx +import com.owenlejeune.tvtime.extensions.items +@OptIn(ExperimentalFoundationApi::class) @Composable fun MoviesTab() { - Column( - modifier = Modifier - .fillMaxSize() - .wrapContentSize(Alignment.Center) + val context = LocalContext.current +// val moviesViewModel = viewModel(PopularMovieViewModel::class.java) +// val moviesList = moviesViewModel.moviePage +// val movieListItems: LazyPagingItems = moviesList.collectAsLazyPagingItems() + val moviesList = remember { mutableStateOf(emptyList()) } + val service = MoviesService() + service.getPopularMovies { isSuccessful, response -> + if (isSuccessful) { + moviesList.value = response!!.movies + } + } + + LazyVerticalGrid( + cells = GridCells.Fixed(count = 3), + contentPadding = PaddingValues(8.dp) ) { - Text( - text = "Movies Tab", - color = MaterialTheme.colorScheme.onBackground - ) +// items(movieListItems) { item -> + items(moviesList.value) { item -> + val poster = item?.let { i -> + "https://image.tmdb.org/t/p/original${i.posterPath}" + } + Image( + painter = rememberImagePainter( + data = poster, + builder = { + transformations(RoundedCornersTransformation(5f.dpToPx(context))) + placeholder(R.drawable.placeholder) + } + ), + contentDescription = item?.title, + modifier = Modifier + .size(190.dp) + .padding(5.dp) + .clickable { + Toast.makeText(context, "${item?.title} clicked", Toast.LENGTH_SHORT).show() + } + ) + } } } \ No newline at end of file diff --git a/app/src/main/res/drawable/placeholder.xml b/app/src/main/res/drawable/placeholder.xml new file mode 100644 index 0000000..672bb57 --- /dev/null +++ b/app/src/main/res/drawable/placeholder.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/buildSrc/src/main/java/com/owenlejeune/tvtime/buildsrc/Dependencies.kt b/buildSrc/src/main/java/com/owenlejeune/tvtime/buildsrc/Dependencies.kt index 4bcb3a9..de2e34a 100644 --- a/buildSrc/src/main/java/com/owenlejeune/tvtime/buildsrc/Dependencies.kt +++ b/buildSrc/src/main/java/com/owenlejeune/tvtime/buildsrc/Dependencies.kt @@ -5,6 +5,7 @@ object Dependencies { object AndroidX { const val appCompat = "androidx.appcompat:appcompat:${Versions.androidx}" const val ktxCore = "androidx.core:core-ktx:${Versions.core_ktx}" + const val paging = "androidx.paging:paging-common-ktx:${Versions.paging}" } object Compose { @@ -16,6 +17,7 @@ object Dependencies { const val activity = "androidx.activity:activity-compose:${Versions.activity_compose}" const val accompanistSystemUi = "com.google.accompanist:accompanist-systemuicontroller:${Versions.compose_accompanist}" const val navigation = "androidx.navigation:navigation-compose:${Versions.compose_navigation}" + const val paging = "androidx.paging:paging-compose:${Versions.compose_paging}" } object Lifecycle { @@ -47,4 +49,8 @@ object Dependencies { object DI { const val koin = "io.insert-koin:koin-android:${Versions.koin}" } + + object Coil { + const val coil = "io.coil-kt:coil-compose:${Versions.coil}" + } } \ No newline at end of file diff --git a/buildSrc/src/main/java/com/owenlejeune/tvtime/buildsrc/Versions.kt b/buildSrc/src/main/java/com/owenlejeune/tvtime/buildsrc/Versions.kt index f2c12f9..5f6bb2c 100644 --- a/buildSrc/src/main/java/com/owenlejeune/tvtime/buildsrc/Versions.kt +++ b/buildSrc/src/main/java/com/owenlejeune/tvtime/buildsrc/Versions.kt @@ -6,6 +6,7 @@ object Versions { const val compose_material3 = "1.0.0-alpha04" const val compose_accompanist = "0.22.1-rc" const val compose_navigation = "2.4.0" + const val compose_paging = "1.0.0-alpha04" const val gradle = "7.1.0" const val junit = "4.13.2" const val androidx_junit = "1.1.3" @@ -19,5 +20,7 @@ object Versions { const val stetho = "1.6.0" const val gson = "2.8.7" const val koin = "3.1.4" + const val paging = "3.1.0" + const val coil = "1.4.0" } \ No newline at end of file