Skip to content

1. OkHTTP

Pré-requis

  • Un projet Android avec Kotlin et les coroutines (fournies par androidx.lifecycle:lifecycle-viewmodel-ktx).
  • Gradle prêt à ajouter quelques dépendances.
  • Un minimum de familiarité avec les data classes et le JSON.

💡 On prendra l'API OpenWeather comme exemple. Tu peux créer gratuitement une clé et remplacer YOUR_API_KEY par la tienne.


1. HTTP "à la main" avec OkHttp

OkHttp est une bibliothèque très fiable pour gérer les connexions HTTP(S). Tu peux l'utiliser seule, sans Retrofit.

1.1. Ajouter OkHttp

On part du principe que ton projet utilise un catalogue de versions (gradle/libs.versions.toml). Ajoute-y les entrées suivantes :

[versions]
okhttpBom = "5.2.1"

[libraries]
okhttp = { module = "com.squareup.okhttp3:okhttp" }
logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor" }
okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttpBom" }

Puis, dans app/build.gradle.kts :

dependencies {
    implementation(platform(libs.okhttp.bom))
    implementation(libs.okhttp)
    implementation(libs.logging.interceptor) // optionnel mais pratique en debug
}

Le BOM garantit que toutes les dépendances OkHttp utilisent la même version. Lorsque tu créeras ton OkHttpClient, implementation(platform(libs.okhttp.bom)) fera en sorte que logging-interceptor et okhttp utilisent bien la version 5.2.1.

💡 Il faudra peut-être mettre à jour le reste des dépendances de ton projet pour que tout soit compatible avec OkHttp 5.x.

1.2. Écrire une requête GET

Dans un premier temps, on va créer un fichier WeatherRepository.kt dans lequel on écrira le code pour récupérer la météo actuelle d'une ville donnée. Voici une fonction de ce repository qui fait une requête HTTP GET simple avec OkHttp, en utilisant les coroutines Kotlin :

class WeatherRepository(private val client: OkHttpClient) {
    suspend fun fetchWeather(city: String): String = withContext(Dispatchers.IO) {
        val request = Request.Builder()
            .url("https://api.openweathermap.org/data/2.5/weather?q=$city&appid=YOUR_API_KEY&units=metric&lang=fr")
            .build()

        client.newCall(request).execute().use { response ->
            if (!response.isSuccessful) error("Réponse HTTP ${'$'}{response.code}")
            response.body?.string() ?: error("Corps de réponse vide")
        }
    }
}
  • withContext(Dispatchers.IO) déplace le travail sur un thread pensé pour les opérations bloquantes.
  • newCall(...).execute() est une API synchrone : on l'entoure donc d'un withContext pour ne pas bloquer le thread principal.
  • use { ... } ferme automatiquement la réponse.

À ce stade tu obtiens du JSON brut (String). C'est suffisant pour comprendre la mécanique, mais pas très pratique pour manipuler les données : d'où l'intérêt de Moshi que l'on verra plus tard.

Pour vérifier rapidement que tout fonctionne dans ton application Android, tu peux brancher ce repository dans une ViewModel ultra-simple :

class RawWeatherViewModel(private val repository: WeatherRepository) : ViewModel() {
    private val _preview = MutableStateFlow("")
    val preview: StateFlow<String> = _preview

    fun refresh(city: String) {
        viewModelScope.launch {
            runCatching { repository.fetchWeather(city) }
                .onSuccess { json -> _preview.value = json }
                .onFailure { error -> _preview.value = "Erreur : ${'$'}{error.message}" }
        }
    }
}

Pour importer viewModelScope, ajoute cet import : import androidx.lifecycle.viewModelScope

C'est quoi un StateFlow ? C'est un flux de données observable, qui conserve en mémoire la dernière valeur émise. Ici : - _preview est un MutableStateFlow privé qui stocke le JSON brut ou un message d'erreur. Il n'est connu qu'à l'intérieur de la ViewModel. - preview est un StateFlow public en lecture seule, que l'UI pourra observer. Ce modèle est courant pour exposer des données depuis une ViewModel vers l'UI tout en s'assurant que l'UI ne peut pas modifier ces données. L'autre avantage, c'est que si preview subit une mutation, toute UI qui l'observe sera automatiquement notifiée et pourra se mettre à jour.

Dans un écran Compose, on reste dans le monde des coroutines : viewModelScope.launch lance une coroutine attachée au cycle de vie de la ViewModel. Quand la ViewModel est détruite (écran fermé ou configuration changée), toutes les coroutines encore en cours sont annulées automatiquement. C'est ce qui nous permet d'appeler le réseau sans avoir à gérer manuellement des threads.

1.3. Côté Compose

Dans un setup 100 % Compose : l’Activity fournit le ViewModelStoreOwner et on instancie la ViewModel avec le délégué viewModels { … } côté Activity, en passant une factory.

Un ViewModelStoreOwner est une interface Android qui “possède” un ou plusieurs ViewModel. Autrement dit : c’est l’objet responsable de conserver et gérer la durée de vie des ViewModel.

Un ViewModel vit tant que son ViewModelStoreOwner vit.

Quand le ViewModelStoreOwner est détruit, toutes les ViewModel qu’il possède sont automatiquement détruites aussi.

Les principales classes qui implémentent ViewModelStoreOwner sont :

  • ComponentActivity → c’est le cas de toutes les Activity modernes (MainActivity, RawWeatherActivity, etc.).
  • Fragment → les ViewModel peuvent aussi être propres à un Fragment (on verra ça un jour).
  • NavBackStackEntry → utile si tu utilises Navigation Compose (idem).

Exemple

Dans le code :

class MainActivity : ComponentActivity() {
    private val viewModel: RawWeatherViewModel by viewModels {
        RawWeatherViewModelFactory(okHttpClient)
    }
}

Ici :

  • MainActivity implémente ViewModelStoreOwner.
  • Le délégué by viewModels { ... } crée la RawWeatherViewModel dans le ViewModelStore de ton Activity.
  • Android garde cette instance tant que l’activité n’est pas détruite définitivement (ex. rotation d’écran = recréation de l’UI, mais la ViewModel survit).

C’est pour ça que quand tu tournes ton téléphone, tu ne perds pas la météo déjà chargée : la ViewModel reste en mémoire dans le ViewModelStore de l’Activity.

💡 Remarque : Il existe aussi une fonction viewModel() (fournie par androidx.lifecycle.viewmodel.compose) qui permet de créer ou retrouver une ViewModel directement depuis un composable.

Ce n’est pas ce que nous faisons ici : nous préférons instancier la ViewModel dans l’Activity à l’aide du délégué by viewModels { ... } pour plus de clarté et un contrôle explicite du cycle de vie.

Du code !

D'abord, la factory pour la ViewModel :

// RawWeatherViewModelFactory.kt
class RawWeatherViewModelFactory(
    private val okHttpClient: OkHttpClient
) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        val repository = WeatherRepository(okHttpClient)
        @Suppress("UNCHECKED_CAST")
        return RawWeatherViewModel(repository) as T
    }
}

Cela permet de passer OkHttpClient à la ViewModel.

Ensuite, dans l’Activity :

// MainActivity.kt
class MainActivity : ComponentActivity() {
    private val okHttpClient by lazy { OkHttpClient() }

    // Lifecycle-aware ViewModel scoped to this Activity
    private val viewModel: RawWeatherViewModel by viewModels {
        RawWeatherViewModelFactory(okHttpClient)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            WeatherTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    RawWeatherScreen(
                        modifier = Modifier.padding(innerPadding),
                        viewModel = viewModel,
                        city = "Paris"
                    )
                }
            }
        }
    }
}

On fait appel à OkHttpClient via by lazy { ... } pour s’assurer qu’une seule instance est créée et réutilisée tant que l’Activity est en vie. On récupère la RawWeatherViewModel avec le délégué by viewModels { ... }, en lui passant la factory.

On termine avec le composable RawWeatherScreen :

// RawWeatherScreen.kt
@Composable
fun RawWeatherScreen(modifier: Modifier, viewModel: RawWeatherViewModel, city: String) {
    val preview by viewModel.preview.collectAsStateWithLifecycle()

    // Trigger initial load and refresh if city changes
    LaunchedEffect(city) { viewModel.refresh(city) }

    // Log each update for quick inspection (success or error)
    LaunchedEffect(preview) {
        if (preview.isNotEmpty()) {
            Log.d("RawWeather", "OpenWeather response: $preview")
        }
    }

    Surface(modifier = modifier.fillMaxSize().padding(24.dp)) {
        if (preview.isBlank()) {
            Text(text = "Loading…")
        } else {
            Text(text = preview)
        }
    }
}

Ici, on observe le StateFlow preview de la ViewModel avec collectAsStateWithLifecycle(). Cela crée un State Compose qui se met à jour automatiquement quand preview change.

Et voila ! Tu as un écran Compose qui affiche le JSON brut de la météo actuelle d’une ville, en utilisant OkHttp.