From a6202c5383b7bbff70c701e1101489dd755360f5 Mon Sep 17 00:00:00 2001 From: Adrian Kuta Date: Thu, 12 Jun 2025 23:44:05 +0200 Subject: [PATCH] feat: Add Retrofit and Moshi for network operations This commit introduces Retrofit and Moshi to handle network requests and JSON parsing. Key changes include: - Added Retrofit, Moshi, OkHttp, and OkHttp Logging Interceptor dependencies to `libs.versions.toml` and `model/data/api/build.gradle.kts`. - Created data classes for API responses: `AirportResponse`, `RouteResponse`, and `FlightResponse`. - Defined Retrofit service interfaces: `AirportService`, `RoutesService`, and `FlightService`. - Implemented a Hilt `NetworkModule` to provide Retrofit, Moshi, and OkHttpClient instances. - Added a Detekt configuration file for the `model/data/api` module. - Temporarily commented out `configureFlavors` in `AndroidApplicationConvention.kt` and `ConfigureLibrary.kt`. --- .../kotlin/AndroidApplicationConvention.kt | 2 +- .../adriankuta/partymania/ConfigureLibrary.kt | 2 +- gradle/libs.versions.toml | 8 +++ model/data/api/build.gradle.kts | 7 +++ model/data/api/config/detekt/detekt.yml | 10 ++++ .../model/data/api/AirportService.kt | 12 ++++ .../model/data/api/FlightService.kt | 26 ++++++++ .../model/data/api/RoutesService.kt | 19 ++++++ .../model/data/api/di/NetworkModule.kt | 60 +++++++++++++++++++ .../data/api/entities/AirportResponse.kt | 51 ++++++++++++++++ .../model/data/api/entities/FlightResponse.kt | 57 ++++++++++++++++++ .../model/data/api/entities/RouteResponse.kt | 50 ++++++++++++++++ 12 files changed, 302 insertions(+), 2 deletions(-) create mode 100644 model/data/api/config/detekt/detekt.yml create mode 100644 model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/AirportService.kt create mode 100644 model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/FlightService.kt create mode 100644 model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/RoutesService.kt create mode 100644 model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/di/NetworkModule.kt create mode 100644 model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/entities/AirportResponse.kt create mode 100644 model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/entities/FlightResponse.kt create mode 100644 model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/entities/RouteResponse.kt diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConvention.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConvention.kt index 5138ffb..5beb40f 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConvention.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConvention.kt @@ -57,7 +57,7 @@ class AndroidApplicationConvention : Plugin { } } - configureFlavors(this) + //configureFlavors(this) configureAndroidLint(this) // configureGradleManagedDevices(this) } diff --git a/build-logic/convention/src/main/kotlin/dev/adriankuta/partymania/ConfigureLibrary.kt b/build-logic/convention/src/main/kotlin/dev/adriankuta/partymania/ConfigureLibrary.kt index 0f3b000..e03288a 100644 --- a/build-logic/convention/src/main/kotlin/dev/adriankuta/partymania/ConfigureLibrary.kt +++ b/build-logic/convention/src/main/kotlin/dev/adriankuta/partymania/ConfigureLibrary.kt @@ -40,7 +40,7 @@ internal fun Project.configureLibrary() { } } - configureFlavors(this) + //configureFlavors(this) configureAndroidLint(this) // configureGradleManagedDevices(this) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6b11753..b97ab6e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,7 @@ androidxLifecycle = "2.9.0" androidxNavigation = "2.9.0" appUpdateKtx = "2.1.0" appcompat = "1.7.0" +converterMoshi = "2.11.0" coreTest = "1.6.1" # https://developer.android.com/jetpack/androidx/releases/test detekt = "1.23.8" # https://detekt.dev/changelog detektCompose = "0.4.22" # https://github.com/mrmans0n/compose-rules/releases @@ -31,6 +32,8 @@ kotlinxSerializationJson = "1.8.1" ksp = "2.1.21-2.0.1" # https://github.com/google/ksp/releases material = "1.12.0" mockk = "1.14.2" # https://github.com/mockk/mockk/releases +okhttpBom = "4.12.0" +retrofit = "3.0.0" secrets = "2.0.1" testRules = "1.6.1" # https://developer.android.com/jetpack/androidx/releases/test testRunner = "1.6.2" # https://developer.android.com/jetpack/androidx/releases/test @@ -69,6 +72,7 @@ androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", androidx-ui-text-google-fonts = { module = "androidx.compose.ui:ui-text-google-fonts", version.ref = "uiTextGoogleFonts" } app-update-ktx = { module = "com.google.android.play:app-update-ktx", version.ref = "appUpdateKtx" } appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "converterMoshi" } detekt-compose = { module = "io.nlopez.compose.rules:detekt", version.ref = "detektCompose" } detekt-ktlint = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } @@ -83,6 +87,10 @@ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-t kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" } +okhttp = { module = "com.squareup.okhttp3:okhttp" } +okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttpBom" } +okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor" } +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } truth = { module = "com.google.truth:truth", version.ref = "truth" } diff --git a/model/data/api/build.gradle.kts b/model/data/api/build.gradle.kts index e4f20f4..88540a9 100644 --- a/model/data/api/build.gradle.kts +++ b/model/data/api/build.gradle.kts @@ -10,6 +10,13 @@ android { dependencies { implementation(projects.core.util) + implementation(platform(libs.okhttp.bom)) + + implementation(libs.converter.moshi) + implementation(libs.okhttp) + implementation(libs.okhttp.logging.interceptor) + implementation(libs.retrofit) + implementation(libs.timber) implementation(libs.gson) } diff --git a/model/data/api/config/detekt/detekt.yml b/model/data/api/config/detekt/detekt.yml new file mode 100644 index 0000000..809b757 --- /dev/null +++ b/model/data/api/config/detekt/detekt.yml @@ -0,0 +1,10 @@ +# Deviations from defaults +formatting: + TrailingCommaOnCallSite: + active: true + autoCorrect: true + useTrailingCommaOnCallSite: true + TrailingCommaOnDeclarationSite: + active: true + autoCorrect: true + useTrailingCommaOnDeclarationSite: true \ No newline at end of file diff --git a/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/AirportService.kt b/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/AirportService.kt new file mode 100644 index 0000000..e3d4140 --- /dev/null +++ b/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/AirportService.kt @@ -0,0 +1,12 @@ +package dev.adriankuta.model.data.api + +import dev.adriankuta.model.data.api.entities.AirportResponse +import retrofit2.http.GET +import retrofit2.http.Path + +interface AirportService { + + @GET("/views/locate/5/airports/{language}/active") + suspend fun getAirports(@Path("language") languageCode: String): List + +} diff --git a/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/FlightService.kt b/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/FlightService.kt new file mode 100644 index 0000000..99b5e39 --- /dev/null +++ b/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/FlightService.kt @@ -0,0 +1,26 @@ +package dev.adriankuta.model.data.api + +import dev.adriankuta.model.data.api.entities.FlightResponse +import retrofit2.http.GET +import retrofit2.http.Headers +import retrofit2.http.Query + +interface FlightService { + + // example query: v4/en-gb/Availability?dateout=2025-09-17&origin=WRO&destination=DUB&adt=1&ToUs=AGREED + @Headers( + "client: android", + "client-version: 3.300.0" + ) + @GET("v4/en-gb/Availability") + suspend fun getFlights( + @Query("dateout") date: String, + @Query("origin") origin: String, + @Query("destination") destination: String, + @Query("adt") adult: Int, + @Query("teen") teen: Int, + @Query("chd") child: Int, + @Query("inf") inf: Int = 0, + @Query("ToUs") toUs: String = "AGREED" + ): FlightResponse +} diff --git a/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/RoutesService.kt b/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/RoutesService.kt new file mode 100644 index 0000000..e35a193 --- /dev/null +++ b/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/RoutesService.kt @@ -0,0 +1,19 @@ +package dev.adriankuta.model.data.api + +import dev.adriankuta.model.data.api.entities.RouteResponse +import retrofit2.http.GET +import retrofit2.http.Path + +interface RoutesService { + + @GET("/views/locate/5/routes/{language}") + suspend fun getAllRoutes( + @Path("language") languageCode: String, + ): List + + @GET("/views/locate/5/routes/{language}/airport/{departure}") + suspend fun getRoutes( + @Path("language") languageCode: String, + @Path("departure") departureAirportCode: String, + ): List +} diff --git a/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/di/NetworkModule.kt b/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/di/NetworkModule.kt new file mode 100644 index 0000000..adeddd8 --- /dev/null +++ b/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/di/NetworkModule.kt @@ -0,0 +1,60 @@ +package dev.adriankuta.model.data.api.di + +import com.squareup.moshi.Moshi +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dev.adriankuta.model.data.api.AirportService +import dev.adriankuta.model.data.api.FlightService +import dev.adriankuta.model.data.api.RoutesService +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class NetworkModule { + + @Singleton + @Provides + fun provideMoshi() = Moshi.Builder().build() + + @Singleton + @Provides + fun provideHttpLoggingInterceptor() = HttpLoggingInterceptor().apply { + setLevel(HttpLoggingInterceptor.Level.BODY) + } + + @Singleton + @Provides + fun providesOkHttpClient(loggingInterceptor: HttpLoggingInterceptor) = + OkHttpClient.Builder().also { builder -> + builder.addInterceptor(loggingInterceptor) + }.build() + + @Singleton + @Provides + fun provideRetrofit(client: OkHttpClient, moshi: Moshi): Retrofit = Retrofit.Builder() + // TODO("add URL provided in the task instructions") + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .client(client) + .build() + + @Singleton + @Provides + fun provideAirportService(retrofit: Retrofit): AirportService = + retrofit.create(AirportService::class.java) + + @Singleton + @Provides + fun provideRoutesService(retrofit: Retrofit): RoutesService = + retrofit.create(RoutesService::class.java) + + @Singleton + @Provides + fun provideFlightService(retrofit: Retrofit): FlightService = + retrofit.create(FlightService::class.java) +} diff --git a/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/entities/AirportResponse.kt b/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/entities/AirportResponse.kt new file mode 100644 index 0000000..7b8f30e --- /dev/null +++ b/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/entities/AirportResponse.kt @@ -0,0 +1,51 @@ +package dev.adriankuta.model.data.api.entities + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AirportResponse( + @Json(name = "code") val code: String?, + @Json(name = "name") val name: String?, + @Json(name = "seoName") val seoName: String?, + @Json(name = "base") val isBase: Boolean?, + @Json(name = "timeZone") val timeZone: String?, + @Json(name = "city") val city: City?, + @Json(name = "macCity") val macCity: MacCity?, + @Json(name = "region") val region: Region?, + @Json(name = "country") val country: Country?, + @Json(name = "coordinates") val coordinates: Coordinates? +) { + + @JsonClass(generateAdapter = true) + data class City( + @Json(name = "code") val code: String?, + @Json(name = "name") val name: String? + ) + + @JsonClass(generateAdapter = true) + data class MacCity( + @Json(name = "code") val code: String?, + @Json(name = "macCode") val macCode: String?, + @Json(name = "name") val name: String? + ) + + @JsonClass(generateAdapter = true) + data class Region( + @Json(name = "code") val code: String?, + @Json(name = "name") val name: String? + ) + + @JsonClass(generateAdapter = true) + data class Country( + @Json(name = "code") val code: String?, + @Json(name = "name") val name: String?, + @Json(name = "currency") val currencyCode: String? + ) + + @JsonClass(generateAdapter = true) + data class Coordinates( + @Json(name = "latitude") val latitude: Double?, + @Json(name = "longitude") val longitude: Double? + ) +} diff --git a/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/entities/FlightResponse.kt b/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/entities/FlightResponse.kt new file mode 100644 index 0000000..1a9369b --- /dev/null +++ b/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/entities/FlightResponse.kt @@ -0,0 +1,57 @@ +package dev.adriankuta.model.data.api.entities + + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class FlightResponse( + @Json(name = "currency") val currency: String?, + @Json(name = "currPrecision") val currPrecision: Int?, + @Json(name = "trips") val trips: List? +) + +@JsonClass(generateAdapter = true) +data class Trip( + @Json(name = "dates") val dates: List?, + @Json(name = "origin") val origin: String?, + @Json(name = "destination") val destination: String? +) + +@JsonClass(generateAdapter = true) +data class TripDate( + @Json(name = "dateOut") val dateOut: String?, + @Json(name = "flights") val flights: List? +) + +@JsonClass(generateAdapter = true) +data class TripFlight( + @Json(name = "faresLeft") val faresLeft: Int?, + @Json(name = "regularFare") val regularFare: RegularFare?, + @Json(name = "flightNumber") val flightNumber: String?, + @Json(name = "time") val dateTimes: List?, + @Json(name = "duration") val duration: String?, + @Json(name = "segments") val segments: List?, + @Json(name = "operatedBy") val operatedBy: String?, +) + +@JsonClass(generateAdapter = true) +data class RegularFare( + @Json(name = "fares") val fares: List? +) + +@JsonClass(generateAdapter = true) +data class TripFare( + @Json(name = "type") val type: String?, + @Json(name = "amount") val amount: Double?, + @Json(name = "count") val count: Int? +) + +@JsonClass(generateAdapter = true) +data class Segment( + @Json(name = "origin") val origin: String?, + @Json(name = "destination") val destination: String?, + @Json(name = "flightNumber") val flightNumber: String?, + @Json(name = "time") val dateTimes: List?, + @Json(name = "duration") val duration: String? +) \ No newline at end of file diff --git a/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/entities/RouteResponse.kt b/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/entities/RouteResponse.kt new file mode 100644 index 0000000..3d20b88 --- /dev/null +++ b/model/data/api/src/main/kotlin/dev/adriankuta/model/data/api/entities/RouteResponse.kt @@ -0,0 +1,50 @@ +package dev.adriankuta.model.data.api.entities + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RouteResponse( + @Json(name = "departureAirport") val departureAirport: Airport.Departure?, + @Json(name = "arrivalAirport") val arrivalAirport: Airport.Arrival?, + @Json(name = "connectingAirport") val connectingAirport: Airport.Connecting? +) { + + sealed interface Airport { + + @Json(name = "code") + val code: String? + + @Json(name = "name") + val name: String? + + @Json(name = "macCity") + val macCity: MacCity? + + @JsonClass(generateAdapter = true) + data class Departure( + override val code: String?, + override val name: String?, + override val macCity: MacCity?, + ) : Airport + + @JsonClass(generateAdapter = true) + data class Arrival( + override val code: String?, + override val name: String?, + override val macCity: MacCity? + ) : Airport + + @JsonClass(generateAdapter = true) + data class Connecting( + override val code: String?, + override val name: String?, + override val macCity: MacCity? + ) : Airport + + @JsonClass(generateAdapter = true) + data class MacCity( + @Json(name = "macCode") val macCode: String? + ) + } +}