Skip to content

Commit 2b95fd5

Browse files
authored
Marvel API to TheMoviedb (#390)
* Marvel to Movies * Update CI * Update e2e tests and app id * Use Wiremock * Use Wiremock latest * Upload script permission * Update script * Remove Marvel JSON files * Fix upload script * Build a custom wiremock * Build a custom wiremock * Update docker image name * Trigger workflow * Use custom wiremock * Fix some bugs * Fix mapping * Update code * Edge2edge * Update things * Use JUnit instead of kotest * Dependency updates * Update AGP, fix deprecated gradle syntax * Use Compose BOM, use material3 * Update README and screenshots
1 parent d505a07 commit 2b95fd5

File tree

95 files changed

+1600
-1495
lines changed

Some content is hidden

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

95 files changed

+1600
-1495
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Build WireMock Image
2+
3+
on:
4+
push:
5+
paths:
6+
- 'wiremock/**'
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout repo
14+
uses: actions/checkout@v4
15+
16+
- name: Set up Docker Buildx
17+
uses: docker/setup-buildx-action@v3
18+
19+
- name: Log in to GHCR
20+
uses: docker/login-action@v3
21+
with:
22+
registry: ghcr.io
23+
username: ${{ github.actor }}
24+
password: ${{ secrets.GITHUB_TOKEN }}
25+
26+
- name: Build and push Docker image
27+
run: |
28+
docker build -t ghcr.io/lordraydenmk/wiremock ./wiremock
29+
docker push ghcr.io/lordraydenmk/wiremock

.github/workflows/ci.yml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ jobs:
1818

1919
- name: Setup secrets
2020
run: |
21-
echo marvel_public_api_key=${{ secrets.MARVEL_PUBLIC_API_KEY }} >> gradle.properties
22-
echo marvel_private_api_key=${{ secrets.MARVEL_PRIVATE_API_KEY }} >> gradle.properties
21+
echo tmdb_api_key=${{ secrets.TMDB_API_KEY }} >> gradle.properties
2322
2423
- name: Setup Java
2524
uses: actions/setup-java@v4
@@ -43,6 +42,13 @@ jobs:
4342
test:
4443
runs-on: ubuntu-latest
4544
timeout-minutes: 30
45+
46+
services:
47+
wiremock:
48+
image: ghcr.io/lordraydenmk/wiremock
49+
ports:
50+
- 8080:8080
51+
4652
steps:
4753
- name: Checkout
4854
uses: actions/checkout@v4
@@ -64,8 +70,7 @@ jobs:
6470

6571
- name: Setup secrets
6672
run: |
67-
echo marvel_public_api_key=${{ secrets.MARVEL_PUBLIC_API_KEY }} >> gradle.properties
68-
echo marvel_private_api_key=${{ secrets.MARVEL_PRIVATE_API_KEY }} >> gradle.properties
73+
echo tmdb_api_key=${{ secrets.TMDB_API_KEY }} >> gradle.properties
6974
7075
- name: Install Maestro
7176
run: |
@@ -79,7 +84,7 @@ jobs:
7984
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
8085
disable-animations: true
8186
script: |
82-
./gradlew installDebug
87+
./gradlew installDebug -Ptmdb_base_url=http://10.0.2.2:8080/
8388
chmod +x e2e.sh
8489
./e2e.sh
8590

README.md

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,38 @@
1-
# Superheroes App
1+
# Movies App
22

3-
A sample project using the Marvel API to show a list of superheroes and some stats about them.
3+
A sample project using the TMDB API to show a list of popular movies and some stats about them.
44

5-
| Superheroes List | Superhero Details | Error & Retry |
6-
|----------------------------------------------|-------------------------------------------|-------------------------------------|
7-
| ![Superheroes List](/images/superheroes.png) | ![Superhero Details](/images/details.png) | ![Error Loading](/images/error.png) |
5+
| Popular Movies List | Movie Details | Error & Retry |
6+
|-------------------------------------------------|---------------------------------------|-------------------------------------|
7+
| ![Popular Movies List](/images/popular.png) | ![Movie Details](/images/details.png) | ![Error Loading](/images/error.png) |
88

9-
## Marvel API KEY
9+
## TMDB API KEY
1010

11-
The project need `marvel_public_api_key` and `marvel_private_api_key` to build. You can add them to your home level `gradle.properties` file (located in `~/.gradle` on Unix based systems):
11+
The project need `tmdb_api_key` to build. You can add it to your home level `gradle.properties` file (located in `~/.gradle` on Unix based systems):
1212

1313
```
14-
marvel_public_api_key=<PUBLIC API KEY HERE>
15-
marvel_private_api_key=<PRIVATE API KEY HERE>
14+
tmdb_api_key=<API KEY HERE>
1615
```
1716

18-
or using `-Pmarvel_public_api_key=<PUBLIC API KEY HERE> -Pmarvel_private_api_key=<PRIVATE API KEY HERE>` to each gradle command e.g.:
17+
or using `-Ptmdb_api_key=<API KEY HERE>` to each gradle command e.g.:
1918

2019
```
21-
./gradlew assembleDebug -Pmarvel_public_api_key=<PUBLIC API KEY HERE> -Pmarvel_private_api_key=<PRIVATE API KEY HERE>
20+
./gradlew assembleDebug -Ptmdb_api_key=<PUBLIC API KEY HERE>
2221
```
2322

24-
Check out the [Marvel Developer portal][mdp] for more info.
23+
Check out the [TMDB developer documentation][tmdb] for more info.
2524

2625
## App Architecture
2726

2827
The app uses a reactive architecture built atop Flow. The app follows a layered architecture with data, domain and presentation layer. The package structure is an attempt to package by feature, however both screens share the data and domain layers. The app uses a single activity + fragments and the Jetpack Navigation Component.
2928

3029
### Data Layer
3130

32-
The API call is modeled using Retrofit, KotlinX Serialization as the converter. The data layer converts the DTO objects to Domain objects. Any expected errors that happen up to this point are mapped to a sealed class `SuperheroError`. A custom exception `SuperheroException` that contains a property of the error is delivered as an `Flow` error in the stream.
31+
The API call is modeled using Retrofit, KotlinX Serialization as the converter. The data layer converts the DTO objects to Domain objects. Any expected errors that happen up to this point are mapped to a sealed class `MovieError`. A custom exception `MovieException` that contains a property of the error is delivered as an `Flow` error in the stream.
3332

3433
### Domain Layer
3534

36-
The main class here is `Superhero`. It has a static `create` function that converts the string that comes from the API (as a thumbnail) into a `HttpUrl` also making sure it's https (so it works on Android).
35+
The main class here is `Movie`. It has a static `create` function that converts the string that comes from the API into a typesafe `HttpUrl`.
3736

3837
### Presentation Layer
3938

@@ -59,9 +58,9 @@ The logic is written as extension functions on top of a module (collection of de
5958

6059
### Testing
6160

62-
This sample uses [kotest][kotest] as a testing library. The presentation logic is tested by mocking the Retrofit Service and using a `TestViewModel` that uses `MutableSharedFlow` instead of `MutableStateFlow` and remembers all events. Tests use the real schedulers and Turbine for testing `Flow`.
61+
This sample uses JUnit as a testing library and [kotest assertions][kotest] for assertions. The presentation logic is tested by mocking the Retrofit Service and using a `TestViewModel` that uses `MutableSharedFlow` instead of `MutableStateFlow` and remembers all events. Tests use the real schedulers and Turbine for testing `Flow`.
6362

64-
The view is tested in isolation using Espresso, by setting a ViewState and verifying the correct elements are displayed/hidden and the text matches the expected.
63+
The view is tested in isolation using Paparazzi, by setting a ViewState and verifying the current image matches the golden images from the repo.
6564

6665
There is also one E2E (black box) test using Maestro that tests both fragments + activity together.
6766

@@ -72,9 +71,9 @@ Approaches is this sample are heavily inspired by open source code I have read.
7271
- [47degrees/FunctionalStreamsSpringSample][fun-stream]
7372
- [Simple Kotlin DI][simple-di]
7473

75-
[mdp]: https://developer.marvel.com/
74+
[tmdb]: https://developer.themoviedb.org/docs/getting-started
7675
[fun-stream]: https://github.com/47degrees/FunctionalStreamsSpringSample
7776
[simple-di]: https://gist.github.com/raulraja/97e2d5bf60e9d96680cf1fddcc90ee67
7877
[view-binding]: https://developer.android.com/topic/libraries/view-binding
79-
[fork]: app/src/main/java/io/github/lordraydenmk/superheroesapp/common/observable.kt
78+
[fork]: app/src/main/java/io/github/lordraydenmk/themoviedbapp/common/observable.kt
8079
[kotest]: https://github.com/kotest/kotest

app/build.gradle

Lines changed: 41 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,28 @@ plugins {
77
}
88

99
android {
10-
namespace 'io.github.lordraydenmk.superheroesapp'
11-
compileSdk 35
10+
namespace 'io.github.lordraydenmk.themoviedbapp'
11+
compileSdkVersion 36
1212

1313
defaultConfig {
14-
applicationId "io.github.lordraydenmk.superheroesapp"
14+
applicationId "io.github.lordraydenmk.themoviedbapp"
1515
minSdkVersion 26
16-
targetSdkVersion 35
16+
targetSdkVersion 36
1717
versionCode 1
1818
versionName "1.0"
1919

2020
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
2121

22-
if (project.hasProperty("marvel_public_api_key")) {
23-
def marvelPublicKey = '"' + project.property("marvel_public_api_key") + '"'
24-
def marvelPrivateKey = '"' + project.property("marvel_private_api_key") + '"'
25-
buildConfigField("String", "MARVEL_PUBLIC_API_KEY", marvelPublicKey)
26-
buildConfigField("String", "MARVEL_PRIVATE_API_KEY", marvelPrivateKey)
22+
def baseUrl = '"' + project.property("tmdb_base_url") + '"'
23+
buildConfigField("String", "TMDB_BASE_URL", baseUrl)
24+
25+
if (project.hasProperty("tmdb_api_key")) {
26+
def tmbdApiKey = '"' + project.property("tmdb_api_key") + '"'
27+
buildConfigField("String", "TMDB_API_KEY", tmbdApiKey)
2728
} else {
28-
// the app needs `marvel_public_api_key` and `marvel_private_api_key`
29-
// as gradle properties to run
30-
// to get one visit: https://developer.marvel.com/account
31-
throw new GradleException("Please provide the Marvel API keys as gradle properties")
29+
// the app needs a `tmdb_api_key as a gradle property to run
30+
// to get one visit: https://developer.themoviedb.org/reference/intro/getting-started
31+
throw new GradleException("Please provide the TMDB API key as a gradle property")
3232
}
3333
}
3434

@@ -42,14 +42,9 @@ android {
4242
test.java.srcDirs += "src/paparazzi/kotlin"
4343
}
4444

45-
compileOptions {
46-
sourceCompatibility JavaVersion.VERSION_21
47-
targetCompatibility JavaVersion.VERSION_21
48-
}
49-
// For Kotlin projects
50-
kotlinOptions {
51-
jvmTarget = "21"
52-
}
45+
kotlin {
46+
jvmToolchain(21)
47+
}
5348

5449
testOptions {
5550
unitTests.all {
@@ -71,54 +66,50 @@ android {
7166
}
7267

7368
dependencies {
74-
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
69+
def coroutines_version = "1.10.2"
70+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
7571

76-
implementation 'androidx.core:core-ktx:1.16.0'
72+
implementation 'androidx.core:core-ktx:1.17.0'
7773
implementation 'androidx.appcompat:appcompat:1.7.1'
78-
implementation 'androidx.activity:activity-compose:1.10.1'
79-
def compose = "1.8.3"
80-
implementation "androidx.compose.ui:ui:$compose"
81-
implementation "androidx.compose.foundation:foundation:1.8.3"
82-
implementation "androidx.compose.material:material:1.8.3"
74+
implementation 'androidx.activity:activity-compose:1.12.0'
75+
def composeBom = platform('androidx.compose:compose-bom:2025.11.01')
76+
implementation composeBom
77+
implementation "androidx.compose.ui:ui"
78+
implementation "androidx.compose.foundation:foundation"
79+
implementation "androidx.compose.material3:material3"
8380
implementation "androidx.compose.material:material-icons-core:1.7.8"
84-
implementation "androidx.compose.ui:ui-tooling:$compose"
81+
implementation "androidx.compose.ui:ui-tooling"
8582

8683
implementation 'androidx.appcompat:appcompat:1.7.1'
8784
implementation 'com.google.android.material:material:1.13.0'
88-
def lifecycle = "2.9.1"
85+
def lifecycle = "2.10.0"
8986
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle"
9087
implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle"
91-
implementation 'androidx.fragment:fragment-ktx:1.8.8'
92-
def nav_version = "2.9.0"
88+
implementation 'androidx.fragment:fragment-ktx:1.8.9'
89+
def nav_version = "2.9.6"
9390
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
9491
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
9592

9693
implementation 'com.jakewharton.timber:timber:5.0.1'
9794

98-
def coil_version = "2.7.0"
99-
implementation "io.coil-kt:coil-compose:$coil_version"
95+
def coil_version = "3.3.0"
96+
implementation "io.coil-kt.coil3:coil-compose:$coil_version"
97+
implementation "io.coil-kt.coil3:coil-network-okhttp:$coil_version"
10098

99+
implementation(platform("com.squareup.okhttp3:okhttp-bom:5.3.2"))
100+
implementation("com.squareup.okhttp3:okhttp")
101+
implementation("com.squareup.okhttp3:logging-interceptor")
101102
def retrofit = "3.0.0"
102-
def okhttp = "4.12.0"
103103
implementation "com.squareup.retrofit2:retrofit:$retrofit"
104104
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"
105105
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0"
106-
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp"
107106

108-
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose"
107+
testImplementation composeBom
108+
debugImplementation "androidx.compose.ui:ui-test-manifest"
109109

110-
def kotest = "5.9.1"
111-
testImplementation "io.kotest:kotest-runner-junit5-jvm:$kotest"
112-
testImplementation "io.kotest:kotest-assertions-core-jvm:$kotest"
110+
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
111+
testImplementation "io.kotest:kotest-assertions-core-jvm:6.0.7"
113112
testImplementation 'app.cash.turbine:turbine:1.2.1'
114-
testImplementation 'org.junit.vintage:junit-vintage-engine:5.14.0'
115-
testImplementation "io.coil-kt:coil-test:$coil_version"
116-
117-
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
118-
def espresso = "3.6.1"
119-
androidTestImplementation "androidx.test.espresso:espresso-core:$espresso"
120-
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso"
121-
androidTestImplementation "com.squareup.okhttp3:mockwebserver:$okhttp"
122-
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose"
123-
113+
testImplementation 'org.junit.vintage:junit-vintage-engine:6.0.1'
114+
testImplementation "io.coil-kt.coil3:coil-test:$coil_version"
124115
}

app/proguard-rules.pro

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@
2323
# region KotlinX
2424
-keepattributes *Annotation*, InnerClasses
2525
-dontnote kotlinx.serialization.SerializationKt
26-
-keep,includedescriptorclasses class io.github.lordraydenmk.superheroesapp.**$$serializer { *; }
27-
-keepclassmembers class io.github.lordraydenmk.superheroesapp.** {
26+
-keep,includedescriptorclasses class io.github.lordraydenmk.themoviedbapp.**$$serializer { *; }
27+
-keepclassmembers class io.github.lordraydenmk.themoviedbapp.** {
2828
*** Companion;
2929
}
30-
-keepclasseswithmembers class io.github.lordraydenmk.superheroesapp.** {
30+
-keepclasseswithmembers class io.github.lordraydenmk.themoviedbapp.** {
3131
kotlinx.serialization.KSerializer serializer(...);
3232
}
3333
# endregion

app/src/debug/assets/3dman.json

Lines changed: 0 additions & 31 deletions
This file was deleted.

0 commit comments

Comments
 (0)