show poster on details screen

This commit is contained in:
Owen LeJeune
2022-02-11 10:27:10 -05:00
parent 5e7a0852de
commit a7ba8cfb4c
21 changed files with 183 additions and 72 deletions

View File

@@ -65,6 +65,7 @@ dependencies {
implementation "com.google.accompanist:accompanist-systemuicontroller:${Versions.compose_accompanist}" implementation "com.google.accompanist:accompanist-systemuicontroller:${Versions.compose_accompanist}"
implementation "androidx.navigation:navigation-compose:${Versions.compose_navigation}" implementation "androidx.navigation:navigation-compose:${Versions.compose_navigation}"
implementation "androidx.paging:paging-compose:${Versions.compose_paging}" implementation "androidx.paging:paging-compose:${Versions.compose_paging}"
implementation "androidx.constraintlayout:constraintlayout-compose:${Versions.compose_constraint_layout}"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle_runtime}" implementation "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle_runtime}"

View File

@@ -0,0 +1,10 @@
package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
import retrofit2.Response
interface DetailService {
suspend fun getById(id: Int): Response<DetailedItem>
}

View File

@@ -1,8 +1,11 @@
package com.owenlejeune.tvtime.api.tmdb package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.DetailedMovie
import com.owenlejeune.tvtime.api.tmdb.model.PopularMovie
import com.owenlejeune.tvtime.api.tmdb.model.PopularMoviesResponse import com.owenlejeune.tvtime.api.tmdb.model.PopularMoviesResponse
import retrofit2.Response import retrofit2.Response
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
interface MoviesApi { interface MoviesApi {
@@ -10,4 +13,7 @@ interface MoviesApi {
@GET("movie/popular") @GET("movie/popular")
suspend fun getPopularMovies(@Query("page") page: Int = 1): Response<PopularMoviesResponse> suspend fun getPopularMovies(@Query("page") page: Int = 1): Response<PopularMoviesResponse>
@GET("movie/{id}")
suspend fun getMovieById(@Path("id") id: Int): Response<DetailedMovie>
} }

View File

@@ -1,11 +1,15 @@
package com.owenlejeune.tvtime.api.tmdb package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import retrofit2.Response
class MoviesService: KoinComponent { class MoviesService: KoinComponent, DetailService {
private val service by lazy { TmdbClient().createMovieService() } private val service by lazy { TmdbClient().createMovieService() }
suspend fun getPopularMovies(page: Int = 1) = service.getPopularMovies(page) suspend fun getPopularMovies(page: Int = 1) = service.getPopularMovies(page)
override suspend fun getById(id: Int): Response<DetailedItem> = service.getMovieById(id) as Response<DetailedItem>
} }

View File

@@ -1,6 +1,6 @@
package com.owenlejeune.tvtime.api.tmdb package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.MediaItem import com.owenlejeune.tvtime.api.tmdb.model.TmdbItem
object TmdbUtils { object TmdbUtils {
@@ -8,8 +8,8 @@ object TmdbUtils {
return "https://image.tmdb.org/t/p/original${posterPath}" return "https://image.tmdb.org/t/p/original${posterPath}"
} }
fun getFullPosterPath(mediaItem: MediaItem): String { fun getFullPosterPath(tmdbItem: TmdbItem): String {
return getFullPosterPath(mediaItem.posterPath) return getFullPosterPath(tmdbItem.posterPath)
} }
} }

View File

@@ -1,8 +1,10 @@
package com.owenlejeune.tvtime.api.tmdb package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.PopularTvResponse import com.owenlejeune.tvtime.api.tmdb.model.PopularTvResponse
import com.owenlejeune.tvtime.api.tmdb.model.DetailedTv
import retrofit2.Response import retrofit2.Response
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
interface TvApi { interface TvApi {
@@ -10,4 +12,7 @@ interface TvApi {
@GET("tv/popular") @GET("tv/popular")
suspend fun getPoplarTv(@Query("page") page: Int = 1): Response<PopularTvResponse> suspend fun getPoplarTv(@Query("page") page: Int = 1): Response<PopularTvResponse>
@GET("tv/{id}")
suspend fun getTvShowById(@Path("id") id: Int): Response<DetailedTv>
} }

View File

@@ -1,10 +1,16 @@
package com.owenlejeune.tvtime.api.tmdb package com.owenlejeune.tvtime.api.tmdb
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import retrofit2.Response
class TvService: KoinComponent { class TvService: KoinComponent, DetailService {
private val service by lazy { TmdbClient().createTvService() } private val service by lazy { TmdbClient().createTvService() }
suspend fun getPopularTv(page: Int = 1) = service.getPoplarTv(page) suspend fun getPopularTv(page: Int = 1) = service.getPoplarTv(page)
override suspend fun getById(id: Int): Response<DetailedItem> {
return service.getTvShowById(id) as Response<DetailedItem>
}
} }

View File

@@ -0,0 +1,3 @@
package com.owenlejeune.tvtime.api.tmdb.model
abstract class DetailedItem(id: Int, title: String, posterPath: String?): TmdbItem(id, title, posterPath)

View File

@@ -0,0 +1,9 @@
package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName
class DetailedMovie(
@SerializedName("id") override val id: Int,
@SerializedName("origina_title") override val title: String,
@SerializedName("poster_path") override val posterPath: String?
): DetailedItem(id, title, posterPath)

View File

@@ -0,0 +1,9 @@
package com.owenlejeune.tvtime.api.tmdb.model
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
class DetailedTv(
id: Int,
title: String,
posterPath: String?
): DetailedItem(id, title, posterPath)

View File

@@ -1,7 +0,0 @@
package com.owenlejeune.tvtime.api.tmdb.model
abstract class MediaItem(
open val posterPath: String?,
@Transient open val title: String,
@Transient open val id: Int
)

View File

@@ -3,7 +3,7 @@ package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
data class PopularMovie( data class PopularMovie(
@SerializedName("poster_path") override val posterPath: String?, @SerializedName("id") override val id: Int,
@SerializedName("title") override val title: String, @SerializedName("title") override val title: String,
@SerializedName("id") override val id: Int @SerializedName("poster_path") override val posterPath: String?
): MediaItem(posterPath, title, id) ): TmdbItem(id, title, posterPath)

View File

@@ -3,7 +3,7 @@ package com.owenlejeune.tvtime.api.tmdb.model
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
data class PopularTv( data class PopularTv(
@SerializedName("poster_path") override val posterPath: String?, @SerializedName("id") override val id: Int,
@SerializedName("name") val name: String, @SerializedName("name") override val title: String,
@SerializedName("id") override val id: Int @SerializedName("poster_path") override val posterPath: String?
): MediaItem(posterPath, name, id) ): TmdbItem(id, title, posterPath)

View File

@@ -0,0 +1,7 @@
package com.owenlejeune.tvtime.api.tmdb.model
abstract class TmdbItem(
@Transient open val id: Int,
@Transient open val title: String,
@Transient open val posterPath: String?
)

View File

@@ -1,6 +1,5 @@
package com.owenlejeune.tvtime.ui.components package com.owenlejeune.tvtime.ui.components
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -15,25 +14,29 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.navigation.NavController import androidx.navigation.NavController
import coil.compose.rememberImagePainter import coil.compose.rememberImagePainter
import coil.transform.RoundedCornersTransformation import coil.transform.RoundedCornersTransformation
import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.TmdbUtils import com.owenlejeune.tvtime.api.tmdb.TmdbUtils
import com.owenlejeune.tvtime.api.tmdb.model.MediaItem import com.owenlejeune.tvtime.api.tmdb.model.TmdbItem
import com.owenlejeune.tvtime.extensions.dpToPx import com.owenlejeune.tvtime.extensions.dpToPx
import com.owenlejeune.tvtime.extensions.listItems import com.owenlejeune.tvtime.extensions.listItems
import com.owenlejeune.tvtime.ui.navigation.MainNavItem import com.owenlejeune.tvtime.ui.navigation.MainNavItem
import com.owenlejeune.tvtime.ui.screens.DetailViewType
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun PosterGrid( fun PosterGrid(
appNavController: NavController, appNavController: NavController,
fetchMedia: (MutableState<List<MediaItem>>) -> Unit type: DetailViewType,
fetchMedia: (MutableState<List<TmdbItem>>) -> Unit
) { ) {
val mediaList = remember { mutableStateOf(emptyList<MediaItem>()) } val mediaList = remember { mutableStateOf(emptyList<TmdbItem>()) }
fetchMedia(mediaList) fetchMedia(mediaList)
LazyVerticalGrid( LazyVerticalGrid(
@@ -41,36 +44,50 @@ fun PosterGrid(
contentPadding = PaddingValues(8.dp) contentPadding = PaddingValues(8.dp)
) { ) {
listItems(mediaList.value) { item -> listItems(mediaList.value) { item ->
PosterItem(appNavController = appNavController, mediaItem = item) PosterItem(
appNavController = appNavController,
mediaItem = item,
type = type
)
} }
} }
} }
@Composable @Composable
fun PosterItem( fun PosterItem(
modifier: Modifier = Modifier,
appNavController: NavController, appNavController: NavController,
mediaItem: MediaItem mediaItem: TmdbItem?,
type: DetailViewType? = null,
width: Dp = 127.dp,
height: Dp = 190.dp
) { ) {
val context = LocalContext.current val context = LocalContext.current
val poster = TmdbUtils.getFullPosterPath(mediaItem) val poster = mediaItem?.let { TmdbUtils.getFullPosterPath(mediaItem) }
Image( Image(
painter = rememberImagePainter( painter = if (mediaItem != null) {
rememberImagePainter(
data = poster, data = poster,
builder = { builder = {
transformations(RoundedCornersTransformation(5f.dpToPx(context))) transformations(RoundedCornersTransformation(5f.dpToPx(context)))
placeholder(R.drawable.placeholder) placeholder(R.drawable.placeholder)
} }
), )
contentDescription = mediaItem.title, } else {
modifier = Modifier rememberImagePainter(ContextCompat.getDrawable(context, R.drawable.placeholder))
.size(190.dp) },
contentDescription = mediaItem?.title,
modifier = modifier
.size(width = width, height = height)
.padding(5.dp) .padding(5.dp)
.clickable { .clickable {
appNavController.navigate("${MainNavItem.DetailView.route}/${mediaItem.id}") type?.let {
// appNavController.n mediaItem?.let {
// Toast appNavController.navigate(
// .makeText(context, "${mediaItem.title} clicked", Toast.LENGTH_SHORT) "${MainNavItem.DetailView.route}/${type}/${mediaItem.id}"
// .show() )
}
}
} }
) )
} }

View File

@@ -6,9 +6,6 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card import androidx.compose.material.Card
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -26,20 +23,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.rememberImagePainter
import coil.transform.RoundedCornersTransformation
import com.owenlejeune.tvtime.R
import com.owenlejeune.tvtime.api.tmdb.TmdbUtils
import com.owenlejeune.tvtime.api.tmdb.model.MediaItem
import com.owenlejeune.tvtime.extensions.dpToPx
import com.owenlejeune.tvtime.extensions.listItems
@Composable @Composable
fun TopLevelSwitch( fun TopLevelSwitch(

View File

@@ -7,7 +7,9 @@ import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.owenlejeune.tvtime.ui.screens.* import com.owenlejeune.tvtime.ui.screens.DetailView
import com.owenlejeune.tvtime.ui.screens.DetailViewType
import com.owenlejeune.tvtime.ui.screens.MainAppView
import com.owenlejeune.tvtime.ui.screens.tabs.FavouritesTab import com.owenlejeune.tvtime.ui.screens.tabs.FavouritesTab
import com.owenlejeune.tvtime.ui.screens.tabs.MoviesTab import com.owenlejeune.tvtime.ui.screens.tabs.MoviesTab
import com.owenlejeune.tvtime.ui.screens.tabs.SettingsTab import com.owenlejeune.tvtime.ui.screens.tabs.SettingsTab
@@ -20,13 +22,17 @@ fun MainNavigationRoutes(navController: NavHostController) {
MainAppView(appNavController = navController) MainAppView(appNavController = navController)
} }
composable( composable(
MainNavItem.DetailView.route.plus("/{ID_KEY}"), MainNavItem.DetailView.route.plus("/{TYPE_KEY}/{ID_KEY}"),
arguments = listOf(navArgument("ID_KEY") { type = NavType.IntType }) arguments = listOf(
navArgument("ID_KEY") { type = NavType.IntType },
navArgument("TYPE_KEY") { type = NavType.EnumType(DetailViewType::class.java) }
)
) { navBackStackEntry -> ) { navBackStackEntry ->
val args = navBackStackEntry.arguments val args = navBackStackEntry.arguments
DetailView( DetailView(
appNavController = navController, appNavController = navController,
itemId = navBackStackEntry.arguments?.getInt("ID_KEY") itemId = args?.getInt("ID_KEY"),
type = args?.getSerializable("TYPE_KEY") as DetailViewType
) )
} }
} }

View File

@@ -1,28 +1,71 @@
package com.owenlejeune.tvtime.ui.screens package com.owenlejeune.tvtime.ui.screens
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.navigation.NavController import androidx.navigation.NavController
import com.owenlejeune.tvtime.api.tmdb.DetailService
import com.owenlejeune.tvtime.api.tmdb.MoviesService
import com.owenlejeune.tvtime.api.tmdb.TvService
import com.owenlejeune.tvtime.api.tmdb.model.DetailedItem
import com.owenlejeune.tvtime.ui.components.PosterItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable @Composable
fun DetailView( fun DetailView(
appNavController: NavController, appNavController: NavController,
itemId: Int? itemId: Int?,
type: DetailViewType
) { ) {
Column( val mediaItem = remember { mutableStateOf<DetailedItem?>(null) }
val service = when(type) {
DetailViewType.MOVIE -> MoviesService()
DetailViewType.TV -> TvService()
}
itemId?.let {
fetchMediaItem(itemId, service, mediaItem)
}
ConstraintLayout(
modifier = Modifier.fillMaxSize()
) {
val (
posterImage,
title
) = createRefs()
PosterItem(
appNavController = appNavController,
mediaItem = mediaItem.value,
modifier = Modifier modifier = Modifier
.fillMaxSize() .constrainAs(posterImage) {
.wrapContentSize(Alignment.Center) top.linkTo(parent.top, margin = 16.dp)
) { start.linkTo(parent.start, margin = 16.dp)
Text( }
text = itemId.toString(),
color = MaterialTheme.colorScheme.onBackground
) )
} }
} }
private fun fetchMediaItem(id: Int, service: DetailService, mediaItem: MutableState<DetailedItem?>) {
CoroutineScope(Dispatchers.IO).launch {
val response = service.getById(id)
if (response.isSuccessful) {
withContext(Dispatchers.Main) {
mediaItem.value = response.body()!!
}
}
}
}
enum class DetailViewType {
MOVIE,
TV
}

View File

@@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.navigation.NavController import androidx.navigation.NavController
import com.owenlejeune.tvtime.api.tmdb.MoviesService import com.owenlejeune.tvtime.api.tmdb.MoviesService
import com.owenlejeune.tvtime.ui.components.PosterGrid import com.owenlejeune.tvtime.ui.components.PosterGrid
import com.owenlejeune.tvtime.ui.screens.DetailViewType
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -16,7 +17,7 @@ fun MoviesTab(appNavController: NavController) {
// val moviesViewModel = viewModel(PopularMovieViewModel::class.java) // val moviesViewModel = viewModel(PopularMovieViewModel::class.java)
// val moviesList = moviesViewModel.moviePage // val moviesList = moviesViewModel.moviePage
// val movieListItems: LazyPagingItems<PopularMovie> = moviesList.collectAsLazyPagingItems() // val movieListItems: LazyPagingItems<PopularMovie> = moviesList.collectAsLazyPagingItems()
PosterGrid(appNavController = appNavController) { moviesList -> PosterGrid(appNavController = appNavController, type = DetailViewType.MOVIE) { moviesList ->
val service = MoviesService() val service = MoviesService()
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val response = service.getPopularMovies() val response = service.getPopularMovies()

View File

@@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.navigation.NavController import androidx.navigation.NavController
import com.owenlejeune.tvtime.api.tmdb.TvService import com.owenlejeune.tvtime.api.tmdb.TvService
import com.owenlejeune.tvtime.ui.components.PosterGrid import com.owenlejeune.tvtime.ui.components.PosterGrid
import com.owenlejeune.tvtime.ui.screens.DetailViewType
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -13,7 +14,7 @@ import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun TvTab(appNavController: NavController) { fun TvTab(appNavController: NavController) {
PosterGrid(appNavController = appNavController) { tvList -> PosterGrid(appNavController = appNavController, type = DetailViewType.TV) { tvList ->
val service = TvService() val service = TvService()
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val response = service.getPopularTv() val response = service.getPopularTv()

View File

@@ -7,6 +7,7 @@ object Versions {
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-alpha14" const val compose_paging = "1.0.0-alpha14"
const val compose_constraint_layout = "1.0.0"
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"