Skip to content

3. Retrofit

Retrofit simplifie les appels réseau en encapsulant OkHttp et en gérant automatiquement la conversion JSON grâce à des converters (ici Moshi). Il utilise des interfaces pour définir les endpoints de l’API.

On garde le parsing dans la couche data : Retrofit appelle l’API, Moshi convertit en objets Kotlin, et le repository retourne un modèle (pas de JSON brut). La ViewModel expose un flux d’objet (forecast) et un flux d’erreur (error).


1. Dépendances

Dans gradle/libs.versions.toml :

[versions]
retrofit = "3.0.0"
moshi = "1.15.2"

[libraries]
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" }
moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" }

Dans app/build.gradle.kts :

dependencies {
    implementation(libs.retrofit)
    implementation(libs.converter.moshi)
    implementation(libs.moshi.kotlin)
}

2. Interface Retrofit

// WeatherService.kt

import retrofit2.http.GET
import retrofit2.http.Query

interface WeatherService {
    @GET("data/2.5/weather")
    suspend fun getCurrentWeather(
        @Query("q") city: String,
        @Query("appid") apiKey: String,
        @Query("units") units: String = "metric",
        @Query("lang") lang: String = "fr"
    ): ForecastResponse
}

3. Construction des clients

// NetworkDependencies.kt

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import java.util.concurrent.TimeUnit
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory

object NetworkDependencies {
    private val logging = HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY
    }

    val okHttpClient: OkHttpClient = OkHttpClient.Builder()
        .addInterceptor(logging)
        .callTimeout(30, TimeUnit.SECONDS)
        .build()

    private val moshi: Moshi = Moshi.Builder()
        .add(KotlinJsonAdapterFactory()) // reflection-based adapters for Kotlin
        .build()

    private val retrofit: Retrofit = Retrofit.Builder()
        .baseUrl("https://api.openweathermap.org/")
        .client(okHttpClient)
        .addConverterFactory(MoshiConverterFactory.create(moshi))
        .build()

    val weatherService: WeatherService = retrofit.create(WeatherService::class.java)
}

4. Repository : Retrofit + Moshi (retourne un objet)

// WeatherRepository.kt
package your.package.data

import your.package.model.ForecastResponse
import your.package.network.WeatherService

const val OPEN_WEATHER_API_KEY = "ba42b5ddc8ac6f1d786c1af6e3be8b9a"

class WeatherRepository(
    private val service: WeatherService
) {
    // Fetch weather using Retrofit service and return typed model
    suspend fun fetchWeather(city: String): ForecastResponse {
        return service.getCurrentWeather(city = city, apiKey = OPEN_WEATHER_API_KEY)
    }
}

5. ViewModel : deux flux simples (objet + erreur)

// ForecastViewModel.kt

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import your.package.data.WeatherRepository
import your.package.model.ForecastResponse

class ForecastViewModel(
    private val repository: WeatherRepository
) : ViewModel() {

    // Expose parsed model or null when loading/not loaded
    private val _forecast = MutableStateFlow<ForecastResponse?>(null)
    val forecast: StateFlow<ForecastResponse?> = _forecast

    // Expose latest error message (null when no error)
    private val _error = MutableStateFlow<String?>(null)
    val error: StateFlow<String?> = _error

    // Load weather for the given city
    fun load(city: String) {
        viewModelScope.launch {
            // Reset previous error; keep last forecast until new value arrives
            _error.value = null

            runCatching { repository.fetchWeather(city) }
                .onSuccess { response ->
                    _forecast.value = response
                }
                .onFailure { e ->
                    _error.value = e.message ?: "Unable to fetch weather"
                }
        }
    }
}

6. Compose (exemple)

@Composable
fun ForecastScreen(viewModel: ForecastViewModel, city: String) {
    val forecast by viewModel.forecast.collectAsStateWithLifecycle()
    val error by viewModel.error.collectAsStateWithLifecycle()

    // Trigger a load when the city changes
    LaunchedEffect(city) { viewModel.load(city) }

    when {
        error != null -> {
            Text(text = "Erreur : $error")
        }
        forecast == null -> {
            Text(text = "Chargement…")
        }
        else -> {
            val data = forecast!!
            val description = data.weather.firstOrNull()?.description
                ?.replaceFirstChar { it.uppercaseChar() }
                .orEmpty()

            Column {
                Text(text = data.name, style = MaterialTheme.typography.headlineMedium)
                Text(text = "${data.main.temp} °C")
                Text(text = description)
            }
        }
    }
}