Connecter le ViewModel à l’interface
Notre CountryViewModel expose maintenant un StateFlow<CountryUiState>.
Il ne nous reste plus qu’à l’observer dans CountryScreen et à afficher le contenu correspondant à l’état courant.
Pour le moment, l’écran utilise encore les données statiques :
CountryList(
countries = AppData.countries
)
Nous allons remplacer cette logique par les données exposées par le ViewModel.
Instancier le ViewModel dans CountryScreen
Ouvre le fichier ui/screen/CountryScreen.kt.
Nous allons modifier ce fichier sans remplacer les composables CountryList et CountryRow qui existent déjà.
Pour créer le ViewModel, repère la déclaration actuelle de CountryScreen :
@Composable
fun CountryScreen(
modifier: Modifier = Modifier,
) {
...
}
Ajoute un paramètre viewModel après modifier :
...
import androidx.lifecycle.viewmodel.compose.viewModel
import com.composechef.countryexplorer.data.CountryApi
import com.composechef.countryexplorer.data.CountryRepository
import com.composechef.countryexplorer.data.httpClient
...
@Composable
fun CountryScreen(
modifier: Modifier = Modifier,
viewModel: CountryViewModel = viewModel<CountryViewModel>(
factory = CountryViewModel.Factory(
repository = CountryRepository(
api = CountryApi(httpClient)
)
)
)
) {
...
}
Le HttpClient est transmis à CountryApi, CountryApi est transmis à CountryRepository et la Factory utilise le repository pour créer CountryViewModel.
Nous construisons les dépendances ici pour garder le projet simple. Dans une application plus grande, cette création serait généralement gérée par un framework d’injection de dépendances.
Observer le StateFlow
Pour déléguer la valeur avec by et observer le flux en respectant le cycle de vie, ajoute les imports suivants :
...
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
...
À l’intérieur de CountryScreen, ajoute cette ligne avant l’appel à CountryContent :
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
collectAsStateWithLifecycle() transforme le StateFlow en un état observable par Compose.
Lorsque la valeur de uiState change, l’interface est automatiquement recomposée.
La collecte respecte également le cycle de vie de l’écran : le flux est observé lorsque l’interface est active.
Transmettre l’état à CountryContent
Repère l’appel actuel à CountryContent dans CountryScreen.
Remplace-le par :
CountryContent(
uiState = uiState,
onRetry = viewModel::reloadCountries,
modifier = modifier
)
uiState représente le contenu à afficher.
onRetry permet de relancer la requête après une erreur.
À ce stade, CountryScreen doit ressembler à ceci :
@Composable
fun CountryScreen(
modifier: Modifier = Modifier,
viewModel: CountryViewModel = viewModel<CountryViewModel>(
factory = CountryViewModel.Factory(
repository = CountryRepository(
api = CountryApi(httpClient)
)
)
)
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
CountryContent(
uiState = uiState,
onRetry = viewModel::reloadCountries,
modifier = modifier
)
}
Modifier les paramètres de CountryContent
Repère la déclaration de CountryContent.
Remplace sa signature actuelle par :
@Composable
private fun CountryContent(
uiState: CountryUiState,
onRetry: () -> Unit,
modifier: Modifier = Modifier
) {
...
}
Le reste du composable, notamment Scaffold et Box, reste en place.
Remplacer les données statiques par un when
À l’intérieur de CountryContent, repère l’appel qui affiche actuellement les données statiques :
CountryList(
countries = AppData.countries
)
Supprime cet appel et ajoute à sa place :
...
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.ui.Alignment
...
when (uiState) {
CountryUiState.Loading -> CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
is CountryUiState.Error -> ErrorContent(
message = uiState.message,
onRetry = onRetry
)
is CountryUiState.Success -> CountryList(
countries = uiState.countries
)
}
Grâce à l’interface scellée CountryUiState, nous pouvons traiter les trois situations possibles avec un when.
Loading affiche un indicateur centré, Success transmet la liste reçue à CountryList et Error délègue l’affichage à ErrorContent.
Nous allons maintenant créer ErrorContent.
Ajouter ErrorContent
Dans CountryScreen.kt, place le curseur après la fonction CountryContent et ajoute le composable suivant :
...
import androidx.compose.material3.Button
...
@Composable
private fun ErrorContent(
message: String,
onRetry: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = message,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.error
)
Button(onClick = onRetry) {
Text(text = "Réessayer")
}
}
}
Lorsque l’utilisateur appuie sur le bouton, onRetry appelle viewModel.reloadCountries().
Le ViewModel replace alors l’écran dans l’état Loading, puis relance la requête.
Supprimer les données statiques
Comme l’écran utilise maintenant les pays reçus depuis REST Countries, supprime l’import suivant de CountryScreen.kt :
import com.composechef.countryexplorer.data.AppData
Le fichier AppData.kt peut également être supprimé du projet.
La version finale de CountryScreen
À ce stade, CountryScreen.kt doit contenir :
package com.composechef.countryexplorer.ui.screen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.composechef.countryexplorer.data.CountryApi
import com.composechef.countryexplorer.data.CountryRepository
import com.composechef.countryexplorer.data.httpClient
import com.composechef.countryexplorer.model.Country
@Composable
fun CountryScreen(
modifier: Modifier = Modifier,
viewModel: CountryViewModel = viewModel<CountryViewModel>(
factory = CountryViewModel.Factory(
repository = CountryRepository(
api = CountryApi(httpClient)
)
)
)
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
CountryContent(
uiState = uiState,
onRetry = viewModel::reloadCountries,
modifier = modifier
)
}
@Composable
private fun CountryContent(
uiState: CountryUiState,
onRetry: () -> Unit,
modifier: Modifier = Modifier
) {
Scaffold { innerPadding ->
Box(
modifier = modifier
.fillMaxSize()
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
) {
when (uiState) {
CountryUiState.Loading -> CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
is CountryUiState.Error -> ErrorContent(
message = uiState.message,
onRetry = onRetry
)
is CountryUiState.Success -> CountryList(
countries = uiState.countries
)
}
}
}
}
@Composable
private fun CountryList(
countries: List<Country>,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp)
) {
item("title") {
Text(
text = "Country Explorer",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 4.dp)
)
}
items(
items = countries,
key = { it.name }
) { country ->
CountryRow(country = country)
HorizontalDivider()
}
}
}
@Composable
private fun CountryRow(
country: Country,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Text(
text = "\${country.flag} \${country.name} \${country.fifaCode}",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
@Composable
private fun ErrorContent(
message: String,
onRetry: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = message,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.error
)
Button(onClick = onRetry) {
Text(text = "Réessayer")
}
}
}
Tester l’application
Lance l’application sur un émulateur ou un appareil connecté à Internet.

Tu devrais observer :
- un indicateur de chargement ;
- la liste des pays récupérés depuis REST Countries ;
- les drapeaux affichés sous forme d’emojis.
Pour tester l’état d’erreur, désactive temporairement la connexion Internet, puis relance l’application.
L’écran devrait afficher un message d’erreur ainsi que le bouton Réessayer.
Rétablis ensuite la connexion et appuie sur le bouton. La liste des pays devrait être chargée.
Notre application utilise maintenant de véritables données provenant d’une API REST.