Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lesson 5 homework finished #18

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions app/src/main/java/com/strv/movies/data/OfflineMoviesProvider.kt

This file was deleted.

17 changes: 17 additions & 0 deletions app/src/main/java/com/strv/movies/data/mapper/TrailerMapper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.strv.movies.data.mapper

import com.strv.movies.model.Trailer
import com.strv.movies.model.TrailerDTO
import javax.inject.Inject

// Convention is to name a mapper after class of target object.
class TrailerMapper @Inject constructor() : Mapper<TrailerDTO, Trailer> {
override fun map(from: TrailerDTO) =
Trailer(
id = from.id,
name = from.name,
key = from.key,
publishedAt = from.publishedAt,
site = from.site
)
}
34 changes: 24 additions & 10 deletions app/src/main/java/com/strv/movies/model/Trailer.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
package com.strv.movies.model

import com.squareup.moshi.Json

data class TrailersDTO(
val id: Int?,
val results: List<TrailerDTO>?
)

// You can actually not use annotations in DTOs if your naming matches the server name.
// Here it is only used in case of publishedAt, because it is not camelcased in the response we get.
// I would generally recommend using it everywhere, simple name refactor could break things otherwise.
data class TrailerDTO(
val name: String?,
val key: String?,
val site: String?,
@Json(name = "published_at")
val publishedAt: String?,
val id: String?
)

data class Trailer(
val iso_639_1: String,
val iso_3166_1: String,
val name: String,
val key: String,
val site: String,
val size: Int,
val type: String,
val official: Boolean,
val publishedAt: String,
val id: String
val name: String?,
val key: String?,
val site: String?,
val publishedAt: String?,
val id: String?
)
4 changes: 4 additions & 0 deletions app/src/main/java/com/strv/movies/network/MovieApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.strv.movies.network

import com.strv.movies.model.MovieDetailDTO
import com.strv.movies.model.PopularMoviesDTO
import com.strv.movies.model.TrailersDTO
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
Expand All @@ -12,4 +13,7 @@ interface MovieApi {

@GET("movie/popular")
suspend fun getPopularMovies(@Query("page") page: Int = 1): PopularMoviesDTO

@GET("movie/{movieId}/videos")
suspend fun getVideos(@Path("movieId") movieId: Int): TrailersDTO
}
21 changes: 16 additions & 5 deletions app/src/main/java/com/strv/movies/network/MovieRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,37 @@ package com.strv.movies.network

import com.strv.movies.data.mapper.MovieDetailMapper
import com.strv.movies.data.mapper.MovieMapper
import com.strv.movies.data.mapper.TrailerMapper
import com.strv.movies.extension.Either
import com.strv.movies.model.Movie
import com.strv.movies.model.MovieDetail
import com.strv.movies.model.MovieDetailDTO
import com.strv.movies.model.PopularMoviesDTO
import com.strv.movies.model.Trailer
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class MovieRepository @Inject constructor(
private val api: MovieApi,
private val movieDetailMapper: MovieDetailMapper,
private val movieMapper: MovieMapper
private val movieMapper: MovieMapper,
private val trailerMapper: TrailerMapper
) {
suspend fun getMovieDetail(movieId: Int): Either<String, MovieDetail> {
return try {
val movie = api.getMovieDetail(movieId)
Either.Value(movieDetailMapper.map(movie))
} catch (exception: Throwable) {
Either.Error(exception.localizedMessage?: "Network error")
Either.Error(exception.localizedMessage ?: "Network error")
}
}

suspend fun getTrailers(movieId: Int): Either<String, List<Trailer>> {
return try {
val videos = api.getVideos(movieId)
Either.Value(videos.results?.map { trailerMapper.map(it) }
?: emptyList()) // solving nullability from API nicely
} catch (exception: Throwable) {
Either.Error(exception.localizedMessage ?: "Network error")
}
}

Expand All @@ -30,7 +41,7 @@ class MovieRepository @Inject constructor(
val popularMovies = api.getPopularMovies()
Either.Value(popularMovies.results.map { movieMapper.map(it) })
} catch (exception: Throwable) {
Either.Error(exception.localizedMessage?: "Network error")
Either.Error(exception.localizedMessage ?: "Network error")
}
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/com/strv/movies/network/NetworkModule.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.strv.movies.network

import android.util.Log
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.strv.movies.BuildConfig
Expand Down Expand Up @@ -59,6 +60,7 @@ object NetworkModule {
@Provides
@Singleton
fun provideLoggingInterceptor(): HttpLoggingInterceptor = HttpLoggingInterceptor { message ->
Log.d("HttpLoggingInterceptor", message)
}.apply {
level = when (BuildConfig.BUILD_TYPE == "debug") {
true -> HttpLoggingInterceptor.Level.BODY
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views.YouTubePlayerView
import com.strv.movies.R
import com.strv.movies.data.OfflineMoviesProvider
import com.strv.movies.model.MovieDetail
import com.strv.movies.model.Trailer
import com.strv.movies.ui.error.ErrorScreen
import com.strv.movies.ui.loading.LoadingScreen

Expand All @@ -46,6 +46,7 @@ fun MovieDetailScreen(
viewState.movie?.let {
MovieDetail(
movie = it,
trailers = viewState.trailers,
videoProgress = viewState.videoProgress,
setVideoProgress = viewModel::updateVideoProgress
)
Expand All @@ -56,17 +57,20 @@ fun MovieDetailScreen(
@Composable
fun MovieDetail(
movie: MovieDetail,
trailers: Trailer?,
videoProgress: Float = 0f,
setVideoProgress: (second: Float) -> Unit
) {
Column {
Log.d("TAG", "MovieDetail: $videoProgress")

MovieTrailerPlayer(
videoId = OfflineMoviesProvider.getTrailer(movie.id).key,
progressSeconds = videoProgress,
setProgress = setVideoProgress
)
if(trailers?.key != null) {
MovieTrailerPlayer(
videoId = trailers.key,
progressSeconds = videoProgress,
setProgress = setVideoProgress
)
}

Row {
MoviePoster(movie = movie)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import android.util.Log
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.strv.movies.extension.Either
import com.strv.movies.extension.fold
import com.strv.movies.network.MovieRepository
import com.strv.movies.ui.navigation.MoviesNavArguments
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
Expand All @@ -30,24 +32,49 @@ class MovieDetailViewModel @Inject constructor(

init {
viewModelScope.launch {
movieRepository.getMovieDetail(movieId).fold(
{ error ->
val movieDetail = async { movieRepository.getMovieDetail(movieId) }
val trailers = async { movieRepository.getTrailers(movieId) }

val movieDetailResponse = movieDetail.await()
val trailersResponse = trailers.await()

// We want to show error if we do not get movie detail. If trailer is not there, we can just hide it.
when (movieDetailResponse) {
is Either.Error -> {
// Here we use smart cast, so no need to .fold() here. We already checked the type in this WHEN branch.
val error = movieDetailResponse.error
Log.d("TAG", "MovieDetailLoadingError: $error")
_viewState.update {
MovieDetailViewState(
error = error
)
}
},
{ movie ->
Log.d("TAG", "MovieDetailSuccess: ${movie.title}")
_viewState.update {
MovieDetailViewState(
movie = movie
error = error // MovieDetail is crucial for this screen, set error.
)
}
}
)
is Either.Value -> {
trailersResponse.fold(
{ error ->
Log.d("TAG", "MovieTrailerLoadingError: $error")
_viewState.update {
MovieDetailViewState(
movie = movieDetailResponse.value // We do not care about this error too much
)
}
},
{ trailerList ->
Log.d("TAG", "MovieTrailerSuccess: ${trailerList.size}")
_viewState.update {
MovieDetailViewState(
movie = movieDetailResponse.value,
// TODO This could be done better.
// Maybe when first trailer is not working for us, try to pick some other.
// We need to check if trailer is YouTube video. We can also e.g. pick only official trailers.
trailers = trailerList.first()
)
}
}
)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.strv.movies.ui.moviedetail

import com.strv.movies.model.MovieDetail
import com.strv.movies.model.Trailer

data class MovieDetailViewState(
val movie: MovieDetail? = null,
val trailers: Trailer? = null,
val loading: Boolean = false,
val error: String? = null,
val videoProgress: Float = 0f
Expand Down