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`.
This commit is contained in:
2025-06-12 23:44:05 +02:00
parent 714cdb6795
commit a6202c5383
12 changed files with 302 additions and 2 deletions

View File

@ -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)
}

View File

@ -0,0 +1,10 @@
# Deviations from defaults
formatting:
TrailingCommaOnCallSite:
active: true
autoCorrect: true
useTrailingCommaOnCallSite: true
TrailingCommaOnDeclarationSite:
active: true
autoCorrect: true
useTrailingCommaOnDeclarationSite: true

View File

@ -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<AirportResponse>
}

View File

@ -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
}

View File

@ -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<RouteResponse>
@GET("/views/locate/5/routes/{language}/airport/{departure}")
suspend fun getRoutes(
@Path("language") languageCode: String,
@Path("departure") departureAirportCode: String,
): List<RouteResponse>
}

View File

@ -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)
}

View File

@ -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?
)
}

View File

@ -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<Trip>?
)
@JsonClass(generateAdapter = true)
data class Trip(
@Json(name = "dates") val dates: List<TripDate>?,
@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<TripFlight>?
)
@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<String>?,
@Json(name = "duration") val duration: String?,
@Json(name = "segments") val segments: List<Segment>?,
@Json(name = "operatedBy") val operatedBy: String?,
)
@JsonClass(generateAdapter = true)
data class RegularFare(
@Json(name = "fares") val fares: List<TripFare>?
)
@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<String>?,
@Json(name = "duration") val duration: String?
)

View File

@ -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?
)
}
}