Skip to content

Commit fc5747a

Browse files
committed
Add movies screen
1 parent c5c98d0 commit fc5747a

File tree

81 files changed

+1854
-29
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+1854
-29
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,4 @@ To see Gradle configuration dependencies (jars or projects):
103103
- list used libs
104104
- install Hilt modules in scopes smaller than `SingletonComponent`
105105
- implement a common file resource reader for mock variants
106+
- create a task to run all UI tests
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.pratclot.api
2+
3+
import com.pratclot.dto.MovieDto
4+
import com.pratclot.dto.MoviesDto
5+
6+
interface MoviesApi {
7+
8+
suspend fun getMovies(): MoviesDto
9+
10+
suspend fun getMovieById(id: Int): MovieDto
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.pratclot.api
2+
3+
import javax.inject.Inject
4+
5+
const val API_HOST_IMAGES = "https://image.tmdb.org/t/p/w500"
6+
7+
class PicturePathEvaluator @Inject constructor() {
8+
operator fun invoke(s: String): String {
9+
return API_HOST_IMAGES + s
10+
}
11+
}

api-common/src/main/java/com/pratclot/api/di/CommonApiModule.kt

+1
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ class CommonApiModule {
1616
fun provideJson(): Json = Json {
1717
explicitNulls = false
1818
ignoreUnknownKeys = true
19+
coerceInputValues = true
1920
}
2021
}

api-live/themoviedb/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

api-live/themoviedb/build.gradle

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
plugins {
2+
id 'java-library'
3+
id 'org.jetbrains.kotlin.jvm'
4+
}
5+
6+
java {
7+
sourceCompatibility = JavaVersion.VERSION_1_8
8+
targetCompatibility = JavaVersion.VERSION_1_8
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.pratclot.themoviedb
2+
3+
import com.pratclot.api.MoviesApi
4+
import com.pratclot.dto.MovieDto
5+
import com.pratclot.dto.MoviesDto
6+
import retrofit2.http.GET
7+
import retrofit2.http.Path
8+
import retrofit2.http.Query
9+
10+
const val API_HOST_MOVIES = "https://api.themoviedb.org/"
11+
12+
interface MoviesApiLive : MoviesApi {
13+
14+
@GET("3/discover/movie")
15+
override suspend fun getMovies(): MoviesDto
16+
17+
@GET("3/movie/{id}")
18+
override suspend fun getMovieById(@Path("id") id: Int): MovieDto
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.pratclot.themoviedb.di
2+
3+
import com.pratclot.api.MoviesApi
4+
import com.pratclot.themoviedb.MoviesApiLive
5+
import dagger.Binds
6+
import dagger.Module
7+
import dagger.hilt.InstallIn
8+
import dagger.hilt.components.SingletonComponent
9+
10+
@InstallIn(SingletonComponent::class)
11+
@Module
12+
abstract class MoviesApiModule {
13+
14+
@Binds
15+
abstract fun provideNews(news: MoviesApiLive): MoviesApi
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.pratclot.themoviedb.di
2+
3+
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
4+
import com.pratclot.common.di.OKHTTP_CLIENT_BUILDER_LOGGING
5+
import com.pratclot.common.secrets.SecretKeys
6+
import com.pratclot.common.secrets.SecretsProvider
7+
import com.pratclot.themoviedb.API_HOST_MOVIES
8+
import com.pratclot.themoviedb.MoviesApiLive
9+
import dagger.Module
10+
import dagger.Provides
11+
import dagger.hilt.InstallIn
12+
import dagger.hilt.components.SingletonComponent
13+
import kotlinx.serialization.json.Json
14+
import okhttp3.Interceptor
15+
import okhttp3.MediaType
16+
import okhttp3.OkHttpClient
17+
import retrofit2.Retrofit
18+
import javax.inject.Named
19+
20+
private const val MOVIES_AUTH = "MOVIES_AUTH"
21+
22+
@InstallIn(SingletonComponent::class)
23+
@Module
24+
class MoviesModule {
25+
26+
@Provides
27+
fun provideMovies(
28+
@Named(MOVIES_AUTH) okHttpClient: OkHttpClient,
29+
json: Json,
30+
contentType: MediaType
31+
): MoviesApiLive = Retrofit.Builder()
32+
.client(okHttpClient)
33+
.baseUrl(API_HOST_MOVIES)
34+
.addConverterFactory(json.asConverterFactory(contentType))
35+
.build()
36+
.create(MoviesApiLive::class.java)
37+
38+
@Named(MOVIES_AUTH)
39+
@Provides
40+
fun provideOkHttpClient(
41+
@Named(OKHTTP_CLIENT_BUILDER_LOGGING) okHttpClientBuilder: OkHttpClient.Builder,
42+
@Named(MOVIES_AUTH) authInterceptor: Interceptor,
43+
): OkHttpClient = okHttpClientBuilder
44+
.addInterceptor(authInterceptor)
45+
.build()
46+
47+
@Named(MOVIES_AUTH)
48+
@Provides
49+
fun provideApiKeyInterceptor(secretsProvider: SecretsProvider): Interceptor =
50+
Interceptor { chain ->
51+
with(chain) {
52+
request().run {
53+
val newUrl = url.newBuilder().addQueryParameter(
54+
"api_key",
55+
secretsProvider.getSecretString(SecretKeys.API_MOVIES)
56+
).build()
57+
newBuilder()
58+
.url(newUrl)
59+
.build()
60+
}.let {
61+
proceed(it)
62+
}
63+
}
64+
}
65+
66+
}

api-mock/themoviedb/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

api-mock/themoviedb/build.gradle

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
plugins {
2+
id 'java-library'
3+
id 'org.jetbrains.kotlin.jvm'
4+
}
5+
6+
java {
7+
sourceCompatibility = JavaVersion.VERSION_1_8
8+
targetCompatibility = JavaVersion.VERSION_1_8
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.pratclot.themoviedb
2+
3+
import com.pratclot.DispatcherWrapper
4+
import com.pratclot.api.MoviesApi
5+
import com.pratclot.dto.MovieDto
6+
import com.pratclot.dto.MoviesDto
7+
import dagger.Binds
8+
import dagger.Module
9+
import dagger.hilt.InstallIn
10+
import dagger.hilt.components.SingletonComponent
11+
import kotlinx.coroutines.Dispatchers
12+
import kotlinx.coroutines.suspendCancellableCoroutine
13+
import kotlinx.coroutines.withContext
14+
import kotlinx.serialization.decodeFromString
15+
import kotlinx.serialization.json.Json
16+
import java.io.FileNotFoundException
17+
import java.nio.charset.Charset
18+
import javax.inject.Inject
19+
import kotlin.coroutines.resumeWithException
20+
21+
private const val FILENAME = "raw/themoviesdb.json"
22+
private const val FILENAME_ONE_MOVIE = "raw/onemovie.json"
23+
24+
@InstallIn(SingletonComponent::class)
25+
@Module
26+
abstract class MoviesApiModule {
27+
28+
class MoviesApiMock @Inject constructor(
29+
private val json: Json,
30+
private val dispatcherWrapper: DispatcherWrapper,
31+
) : MoviesApi {
32+
33+
private suspend inline fun <reified T> readFromFile(path: String): T =
34+
withContext(dispatcherWrapper.io) {
35+
suspendCancellableCoroutine { continuation ->
36+
val stream = javaClass.classLoader.getResourceAsStream(path)
37+
if (stream == null)
38+
continuation.resumeWithException(FileNotFoundException("Did not find ${path}"))
39+
else stream.run {
40+
continuation.invokeOnCancellation { close() }
41+
val size = available()
42+
val buffer = ByteArray(size)
43+
read(buffer)
44+
close()
45+
val jsonString = buffer.toString(Charset.defaultCharset())
46+
json.decodeFromString<T>(jsonString).let { list ->
47+
continuation.resume(list) { continuation.resumeWithException(it) }
48+
}
49+
}
50+
}
51+
}
52+
53+
override suspend fun getMovies(): MoviesDto = readFromFile(FILENAME)
54+
55+
override suspend fun getMovieById(id: Int): MovieDto = readFromFile(FILENAME_ONE_MOVIE)
56+
57+
}
58+
59+
@Binds
60+
abstract fun bindMoviesApi(moviesApiMock: MoviesApiMock): MoviesApi
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{
2+
"adult": false,
3+
"backdrop_path": "/askg3SMvhqEl4OL52YuvdtY40Yb.jpg",
4+
"belongs_to_collection": null,
5+
"budget": 175000000,
6+
"genres": [
7+
{
8+
"id": 10751,
9+
"name": "Family"
10+
},
11+
{
12+
"id": 16,
13+
"name": "Animation"
14+
},
15+
{
16+
"id": 14,
17+
"name": "Fantasy"
18+
},
19+
{
20+
"id": 10402,
21+
"name": "Music"
22+
},
23+
{
24+
"id": 35,
25+
"name": "Comedy"
26+
},
27+
{
28+
"id": 12,
29+
"name": "Adventure"
30+
}
31+
],
32+
"homepage": "https://www.pixar.com/feature-films/coco",
33+
"id": 354912,
34+
"imdb_id": "tt2380307",
35+
"original_language": "en",
36+
"original_title": "Coco",
37+
"overview": "Despite his family’s baffling generations-old ban on music, Miguel dreams of becoming an accomplished musician like his idol, Ernesto de la Cruz. Desperate to prove his talent, Miguel finds himself in the stunning and colorful Land of the Dead following a mysterious chain of events. Along the way, he meets charming trickster Hector, and together, they set off on an extraordinary journey to unlock the real story behind Miguel's family history.",
38+
"popularity": 1532.497,
39+
"poster_path": "/gGEsBPAijhVUFoiNpgZXqRVWJt2.jpg",
40+
"production_companies": [
41+
{
42+
"id": 2,
43+
"logo_path": "/wdrCwmRnLFJhEoH8GSfymY85KHT.png",
44+
"name": "Walt Disney Pictures",
45+
"origin_country": "US"
46+
},
47+
{
48+
"id": 3,
49+
"logo_path": "/1TjvGVDMYsj6JBxOAkUHpPEwLf7.png",
50+
"name": "Pixar",
51+
"origin_country": "US"
52+
}
53+
],
54+
"production_countries": [
55+
{
56+
"iso_3166_1": "MX",
57+
"name": "Mexico"
58+
},
59+
{
60+
"iso_3166_1": "US",
61+
"name": "United States of America"
62+
}
63+
],
64+
"release_date": "2017-10-27",
65+
"revenue": 800526015,
66+
"runtime": 105,
67+
"spoken_languages": [
68+
{
69+
"english_name": "English",
70+
"iso_639_1": "en",
71+
"name": "English"
72+
},
73+
{
74+
"english_name": "Spanish",
75+
"iso_639_1": "es",
76+
"name": "Español"
77+
}
78+
],
79+
"status": "Released",
80+
"tagline": "The celebration of a lifetime",
81+
"title": "Coco",
82+
"video": false,
83+
"vote_average": 8.227,
84+
"vote_count": 16438
85+
}

0 commit comments

Comments
 (0)