show scrolling list of movies

This commit is contained in:
Owen LeJeune
2022-02-10 00:01:03 -05:00
parent b5db7422d0
commit 05afce953e
16 changed files with 297 additions and 14 deletions

View File

@@ -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

View File

@@ -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())
}

View File

@@ -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<PopularMoviesResponse>
// suspend fun getPopularMovies(@Query("page") page: Int = 1): PopularMoviesResponse
}

View File

@@ -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<PopularMoviesResponse> {
override fun onResponse(
call: Call<PopularMoviesResponse>,
response: Response<PopularMoviesResponse>
) {
response.body()?.let { body ->
callback.invoke(true, body)
} ?: run {
callback.invoke(false, null)
}
}
override fun onFailure(call: Call<PopularMoviesResponse>, t: Throwable) {
callback.invoke(false, null)
}
})
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
)

View File

@@ -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<PopularMovie>
)

View File

@@ -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<Int, PopularMovie>() {
//
// 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, PopularMovie>): Int? {
// return state.anchorPosition
// }
//
// override suspend fun load(params: LoadParams<Int>): LoadResult<Int, PopularMovie> {
// 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)
// }
// }
//}

View File

@@ -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<PagingData<PopularMovie>> = Pager(PagingConfig(pageSize = PopularMovieSource.MAX_PAGE)) {
// PopularMovieSource()
// }.flow.cachedIn(viewModelScope)
//}

View File

@@ -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 <T: Any> LazyGridScope.items(
lazyPagingItems: LazyPagingItems<T>,
itemContent: @Composable LazyItemScope.(value: T?) -> Unit
) {
items(lazyPagingItems.itemCount) { index ->
itemContent(lazyPagingItems[index])
}
}
@OptIn(ExperimentalFoundationApi::class)
fun <T: Any> LazyGridScope.items(
items: List<T>,
itemContent: @Composable (value: T?) -> Unit
) {
items(items.size) { index ->
itemContent(items[index])
}
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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<PopularMovie> = moviesList.collectAsLazyPagingItems()
val moviesList = remember { mutableStateOf(emptyList<PopularMovie>()) }
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()
}
)
}
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="5dp" />
<solid android:color="@android:color/darker_gray" />
</shape>

View File

@@ -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}"
}
}

View File

@@ -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"
}