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_KEYpar 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'unwithContextpour 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
ViewModelvit tant que sonViewModelStoreOwnervit.Quand le
ViewModelStoreOwnerest détruit, toutes lesViewModelqu’il possède sont automatiquement détruites aussi.
Les principales classes qui implémentent ViewModelStoreOwner sont :
ComponentActivity→ c’est le cas de toutes lesActivitymodernes (MainActivity,RawWeatherActivity, etc.).Fragment→ lesViewModelpeuvent aussi être propres à unFragment(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 :
MainActivityimplémenteViewModelStoreOwner.- Le délégué
by viewModels { ... }crée laRawWeatherViewModeldans leViewModelStorede 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 parandroidx.lifecycle.viewmodel.compose) qui permet de créer ou retrouver uneViewModeldirectement depuis un composable.Ce n’est pas ce que nous faisons ici : nous préférons instancier la
ViewModeldans 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.