mirror of
https://github.com/owenlejeune/TVTime.git
synced 2025-11-20 10:40:53 -05:00
show scrolling list of movies
This commit is contained in:
@@ -38,6 +38,7 @@ android {
|
|||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = '1.8'
|
jvmTarget = '1.8'
|
||||||
|
useIR = true
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose true
|
compose true
|
||||||
@@ -55,6 +56,7 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation Dependencies.AndroidX.ktxCore
|
implementation Dependencies.AndroidX.ktxCore
|
||||||
|
implementation Dependencies.AndroidX.paging
|
||||||
|
|
||||||
implementation Dependencies.Compose.ui
|
implementation Dependencies.Compose.ui
|
||||||
implementation Dependencies.Compose.material3
|
implementation Dependencies.Compose.material3
|
||||||
@@ -63,6 +65,7 @@ dependencies {
|
|||||||
implementation Dependencies.Compose.activity
|
implementation Dependencies.Compose.activity
|
||||||
implementation Dependencies.Compose.accompanistSystemUi
|
implementation Dependencies.Compose.accompanistSystemUi
|
||||||
implementation Dependencies.Compose.navigation
|
implementation Dependencies.Compose.navigation
|
||||||
|
implementation Dependencies.Compose.paging
|
||||||
|
|
||||||
implementation Dependencies.Lifecycle.runtime
|
implementation Dependencies.Lifecycle.runtime
|
||||||
|
|
||||||
@@ -74,6 +77,8 @@ dependencies {
|
|||||||
|
|
||||||
implementation Dependencies.DI.koin
|
implementation Dependencies.DI.koin
|
||||||
|
|
||||||
|
implementation Dependencies.Coil.coil
|
||||||
|
|
||||||
testImplementation Dependencies.Testing.junit
|
testImplementation Dependencies.Testing.junit
|
||||||
androidTestImplementation Dependencies.Testing.androidXJunit
|
androidTestImplementation Dependencies.Testing.androidXJunit
|
||||||
androidTestImplementation Dependencies.Testing.espressoCore
|
androidTestImplementation Dependencies.Testing.espressoCore
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
@@ -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)
|
||||||
|
//}
|
||||||
@@ -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])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,24 +1,68 @@
|
|||||||
package com.owenlejeune.tvtime.ui.screens
|
package com.owenlejeune.tvtime.ui.screens
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.layout.wrapContentSize
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.material3.Text
|
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.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.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
|
@Composable
|
||||||
fun MoviesTab() {
|
fun MoviesTab() {
|
||||||
Column(
|
val context = LocalContext.current
|
||||||
modifier = Modifier
|
// val moviesViewModel = viewModel(PopularMovieViewModel::class.java)
|
||||||
.fillMaxSize()
|
// val moviesList = moviesViewModel.moviePage
|
||||||
.wrapContentSize(Alignment.Center)
|
// 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(
|
// items(movieListItems) { item ->
|
||||||
text = "Movies Tab",
|
items(moviesList.value) { item ->
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
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()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
6
app/src/main/res/drawable/placeholder.xml
Normal file
6
app/src/main/res/drawable/placeholder.xml
Normal 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>
|
||||||
@@ -5,6 +5,7 @@ object Dependencies {
|
|||||||
object AndroidX {
|
object AndroidX {
|
||||||
const val appCompat = "androidx.appcompat:appcompat:${Versions.androidx}"
|
const val appCompat = "androidx.appcompat:appcompat:${Versions.androidx}"
|
||||||
const val ktxCore = "androidx.core:core-ktx:${Versions.core_ktx}"
|
const val ktxCore = "androidx.core:core-ktx:${Versions.core_ktx}"
|
||||||
|
const val paging = "androidx.paging:paging-common-ktx:${Versions.paging}"
|
||||||
}
|
}
|
||||||
|
|
||||||
object Compose {
|
object Compose {
|
||||||
@@ -16,6 +17,7 @@ object Dependencies {
|
|||||||
const val activity = "androidx.activity:activity-compose:${Versions.activity_compose}"
|
const val activity = "androidx.activity:activity-compose:${Versions.activity_compose}"
|
||||||
const val accompanistSystemUi = "com.google.accompanist:accompanist-systemuicontroller:${Versions.compose_accompanist}"
|
const val accompanistSystemUi = "com.google.accompanist:accompanist-systemuicontroller:${Versions.compose_accompanist}"
|
||||||
const val navigation = "androidx.navigation:navigation-compose:${Versions.compose_navigation}"
|
const val navigation = "androidx.navigation:navigation-compose:${Versions.compose_navigation}"
|
||||||
|
const val paging = "androidx.paging:paging-compose:${Versions.compose_paging}"
|
||||||
}
|
}
|
||||||
|
|
||||||
object Lifecycle {
|
object Lifecycle {
|
||||||
@@ -47,4 +49,8 @@ object Dependencies {
|
|||||||
object DI {
|
object DI {
|
||||||
const val koin = "io.insert-koin:koin-android:${Versions.koin}"
|
const val koin = "io.insert-koin:koin-android:${Versions.koin}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object Coil {
|
||||||
|
const val coil = "io.coil-kt:coil-compose:${Versions.coil}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ object Versions {
|
|||||||
const val compose_material3 = "1.0.0-alpha04"
|
const val compose_material3 = "1.0.0-alpha04"
|
||||||
const val compose_accompanist = "0.22.1-rc"
|
const val compose_accompanist = "0.22.1-rc"
|
||||||
const val compose_navigation = "2.4.0"
|
const val compose_navigation = "2.4.0"
|
||||||
|
const val compose_paging = "1.0.0-alpha04"
|
||||||
const val gradle = "7.1.0"
|
const val gradle = "7.1.0"
|
||||||
const val junit = "4.13.2"
|
const val junit = "4.13.2"
|
||||||
const val androidx_junit = "1.1.3"
|
const val androidx_junit = "1.1.3"
|
||||||
@@ -19,5 +20,7 @@ object Versions {
|
|||||||
const val stetho = "1.6.0"
|
const val stetho = "1.6.0"
|
||||||
const val gson = "2.8.7"
|
const val gson = "2.8.7"
|
||||||
const val koin = "3.1.4"
|
const val koin = "3.1.4"
|
||||||
|
const val paging = "3.1.0"
|
||||||
|
const val coil = "1.4.0"
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user