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

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