
Bonjour Jeune Étoile,
Nous sommes maintenant prêts à plonger dans une nouvelle partie de notre aventure ensemble. À présent, nous allons perfectionner notre application en nous concentrant sur la qualité de notre code. L’expérience utilisateur est aussi cruciale que la précision de nos télescopes.
Nous allons voir qu’une classe légère et bien implémentée peut faire toute la différence en termes de simplicité de lecture du code ; notre experte Katherine te fournira le code.
Réglons nos télescopes pour viser les sommets,
Cordialement,
Margaret Hamilton, Dev Senior – Projet "Planète Exploration"
Notre application fonctionne à merveille, et c'est génial ! Cependant, envisageons un scénario où des soucis surviennent lors de la récupération des données. Prenons le cas où le téléphone n'a pas de réseau, que les serveurs d’OpenWeather ne répondent plus ou que les données renvoyées par l'API ne sont pas conformes.
Pour faire face à ces éventualités, nous allons introduire une nouvelle classe qui sera responsable de la gestion de l'état d'une demande.
Créez une nouvelle classe, comme nous l’avons déjà fait auparavant, nommée data.repository.Result , et copiez ce code dedans :
sealed class Result<out T> {
object Loading : Result<Nothing>()
data class Failure(
val message: String? = null,
) : Result<Nothing>()
data class Success<out R>(val value: R) : Result<R>()
}Ce code définit une classe appelée Result qui est utilisée pour représenter le résultat d'une opération qui peut réussir, échouer ou être en cours d'exécution.
Voici les trois états que peut renvoyer la classe Result :
Loading (Chargement) :
Il s'agit d'un objet unique représentant l'état où l'opération est en cours d'exécution. Cela peut être utile pour afficher une indication visuelle à l'utilisateur, comme une icône de chargement.
Failure (Échec) :
C'est une classe de données qui représente l'état où l'opération a échoué. Elle peut contenir un message décrivant l'erreur survenue.
Success (Succès) :
C'est une classe de données générique qui stocke le résultat de l'opération en cas de succès. Elle prend un type générique R pour permettre de représenter différents types de résultats.
En résumé, cette approche permet de gérer de manière structurée et exhaustive les différents résultats possibles d'une opération.
Nous allons donc améliorer notre requête au sein de notre repository (magasin, dépôt de données). Désormais, notre flux ( Flow ) ne se contente plus de renvoyer simplement une liste. Il fournit un résultat de requête que nous interpréterons plus tard. Modifiez la méthode fetchForecastData de notre repository :
fun fetchForecastData(lat: Double, lng: Double): Flow<Result<List<WeatherReportModel>>> =
flow {
emit(Result.Loading)
val result = dataService.getWeatherByPosition(
latitude = lat,
longitude = lng,
apiKey = API_KEY
)
val model = result.body()?.toDomainModel() ?: throw Exception("Invalid data")
emit(Result.Success(model))
}.catch { error ->
emit(Result.Failure(error.message))
}Il se peut qu'Android Studio n’importe pas le bon Result , alors vous devrez le définir en ajoutant dans les imports cette ligne :
import com.openclassrooms.stellarforecast.data.repository.ResultVoyons ensemble ce qui a changé avec cette amélioration :
emit(Result.Loading) :
Avant de commencer la requête, nous émettons un état de chargement ( Loading ). Cela indique que la requête est en cours d'exécution. Parce que nous sommes dans une méthode Flow , nous pouvons émettre autant d'informations que nous voulons, il suffit juste d'écouter la requête.
emit(Result.Success(model)) :
Si la récupération des données est réussie, nous émettons un état de succès ( Success ) avec les données météorologiques ( model ).
.catch { error -> emit(Result.Failure(error.message)) } :
En cas d'erreur, nous capturons l'erreur et émettons un état d'échec ( Failure ) avec le message d'erreur associé.
En résumé, le code utilise la classe Result pour représenter les différents états possibles d'une requête. On émet d'abord un état de chargement, puis soit un état de succès avec les données, soit un état d'échec avec un message d'erreur, en fonction du résultat de la requête.
Maintenant que notre repository nous renvoie l'état de sa requête, nous devons également mettre à jour l’affichage de notre application, avec la classe HomeUiState . Nous ajoutons les propriétés isViewLoading et errorMessage pour gérer les états de chargement et d'erreur.
data class HomeUiState(
val forecast: List<WeatherReportModel> = emptyList(),
val isViewLoading: Boolean = false,
val errorMessage: String? = null
)Nous pouvons ainsi adapter notre méthode getForecastData de notre ViewModel de la manière suivante :
private fun getForecastData() {
val latitude = 48.844304
val longitude = 2.374377
dataRepository.fetchForecastData(latitude, longitude).onEach { forecastUpdate ->
when (forecastUpdate) {
is Result.Failure -> _uiState.update { currentState ->
currentState.copy(
isViewLoading = false,
errorMessage = forecastUpdate.message
)
}
Result.Loading -> _uiState.update { currentState ->
currentState.copy(
isViewLoading = true,
errorMessage = null,
)
}
is Result.Success -> _uiState.update { currentState ->
currentState.copy(
forecast = forecastUpdate.value,
isViewLoading = false,
errorMessage = null,
)
}
}
}.launchIn(viewModelScope)Voici les explications de ce qui a changé :
when (forecastUpdate) :
On utilise une instruction when pour traiter les différents états possibles du flux (Loading, Success ou Failure) retourné par la fonction fetchForecastData. Ainsi nous sommes certains de gérer tous les états possibles.
Result.Loading -> :
En cas de chargement en cours, l'état est mis à jour pour refléter le début du chargement avec un indicateur approprié.
is Result.Success -> :
Si la requête est un succès, l'état est mis à jour avec les données météorologiques obtenues, en plus d'indiquer que le chargement est terminé et qu'il n'y a pas d'erreur.
is Result.Failure -> :
Si le résultat est un échec, la mise à jour de l'état de l'interface utilisateur ( _uiState ) se fait en modifiant l'état actuel avec les informations appropriées (arrêt du chargement, affichage du message d'erreur).
Il ne nous reste plus qu'à modifier notre interface graphique pour qu'elle reflète ce dernier jeu de données.
Dans la méthode onCreate , remplacez le contenu de viewModel.uiState.collect{} par :
viewModel.uiState.collect {
updateCurrentWeather(it.forecast)
binding.progressBar.isVisible = it.isViewLoading
if (it.errorMessage?.isNotBlank() == true) {
Snackbar.make(binding.root, it.errorMessage, Snackbar.LENGTH_LONG)
.show()
}
}Et dans le fichier main_activity.xml, ajoutez ceci juste après l’ouverture du RelativeLayout :
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />Nous avons ajouté de nouvelles lignes, en voici l’explication :
binding.progressBar.isVisible = it.isViewLoading :
Cela ajuste la visibilité d'une barre de progression dans l'interface utilisateur en fonction de la propriété isViewLoading de l'état actuel. Si le chargement est en cours, la barre de progression est rendue visible.
if (it.errorMessage?.isNotBlank() == true) { ... } :
Cela vérifie si la propriété errorMessage de l'état actuel est non nulle et non vide. Si c'est le cas, cela signifie qu'une erreur s'est produite pendant la récupération des données.
Snackbar.make(binding.root, it.errorMessage, Snackbar.LENGTH_LONG).show() :
En cas d'erreur, un message SnackBar est créé avec le message d'erreur. Ce message d'erreur est affiché, en bas de l'écran pendant quelques secondes, à l'utilisateur pour l'informer du problème.
En résumé, ce code gère tous les états qu’une vue peut rencontrer : chargement, succès et erreur. On met à jour l'interface utilisateur en fonction des nouvelles données météorologiques, ajuste la visibilité de la barre de progression en fonction de l'état de chargement, et affiche un message d'erreur SnackBar s'il y a une erreur pendant la récupération des données.
Relancez l’application avec votre téléphone en mode avion pour voir une erreur apparaître !
Cher électron libre,
Je suis ravie de t'informer que nous avons une opportunité excitante d'améliorer encore davantage notre application en intégrant une fonction de GPS. Cela permettra d'offrir à nos utilisateurs une expérience plus personnalisée et adaptée à leur emplacement.
La bonne nouvelle, c'est que le code nécessaire pour cette fonctionnalité est déjà à notre disposition, gracieuseté de Hedy, notre spécialiste en communication longues distances. Son expertise dans ce domaine garantit que l'intégration se fera sans heurts.
Cordialement,
Margaret Hamilton, Dev Senior – Projet "Planète Exploration"
Pour incorporer l'utilisation du GPS dans notre application, une légère adaptation de notre code existant est nécessaire. Il sera également nécessaire d'ajouter des lignes de code pour localiser l'utilisateur après avoir obtenu son consentement. Ce processus permettra d'améliorer l'expérience utilisateur en offrant des fonctionnalités plus adaptées à sa position géographique.
La ligne suivante doit être ajoutée dans le fichier Gradle pour inclure la bibliothèque de services de localisation de Google :
implementation("com.google.android.gmslay-services-location:18.0.0")Les deux permissions suivantes sont à ajouter au fichier manifeste pour permettre à l'application d'accéder à la localisation de l'appareil :
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />Dans notre classe HomeViewModel , nous pouvons supprimer la méthode init et venir modifier la méthode getForecastData pour pouvoir l’appeler directement depuis l’activity :
fun getForecastData(latitude : Double, longitude : Double) {
…
}Déclarez les éléments suivants au début de la classe MainActivity , en insérant le code ci-dessous juste après l'accolade ouvrante :
private val MY_PERMISSIONS_REQUEST_LOCATION = 99
private var fusedLocationProvider: FusedLocationProviderClient? = null
private val locationRequest: LocationRequest = LocationRequest.create().apply {
interval = 100000
priority = LocationRequest.PRIORITY_LOW_POWER
maxWaitTime = 600000
}
private var locationCallback: LocationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
val locationList = locationResult.locations
if (locationList.isNotEmpty()) {
val location = locationList.last()
viewModel.getForecastData(location.latitude, location.longitude)
}
}
}Ces variables sont initialisées pour gérer la localisation. fusedLocationProvider est une classe qui permet d'obtenir la localisation du téléphone. locationRequest est utilisée pour définir des paramètres de demande de localisation, tels que l'intervalle entre les mises à jour, ou la priorité.
Dans la méthode onCreate de l'activité, les étapes suivantes sont à ajouter :
fusedLocationProvider = LocationServices.getFusedLocationProviderClient(this)
requestLocationPermission()On initialise la variable fusedLocationProvider en utilisant la classe LocationServices pour obtenir une instance de FusedLocationProviderClient , grâce au contexte de l’application.
On appelle la méthode requestLocationPermission() pour demander la permission de localisation, que nous allons définir plus bas.
Dans les méthodes onResume et onPause du Lifecycle, des modifications sont apportées pour gérer les mises à jour de localisation en fonction du cycle de vie de l'activité.
override fun onResume() {
super.onResume()
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED
) {
fusedLocationProvider?.requestLocationUpdates(
locationRequest,
locationCallback,
Looper.getMainLooper()
)
}
}
override fun onPause() {
super.onPause()
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
== PackageManager.PERMISSION_GRANTED
) {
fusedLocationProvider?.removeLocationUpdates(locationCallback)
}
}Dans onResume , si la permission de localisation est accordée, on demande des mises à jour de localisation en utilisant requestLocationUpdates .
Dans onPause , on supprime les mises à jour de localisation pour économiser des ressources lorsque l'activité est en arrière-plan, principalement de la batterie.
On demande à l’utilisateur la permission de géolocaliser son téléphone, en ajoutant le code suivant toujours dans la classe MainActivity :
private fun requestLocationPermission() {
ActivityCompat.requestPermissions(this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION,),
MY_PERMISSIONS_REQUEST_LOCATION
)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
MY_PERMISSIONS_REQUEST_LOCATION -> {
// If request is cancelled, the result arrays are empty.
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted!
if (ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
fusedLocationProvider?.requestLocationUpdates(
locationRequest,
locationCallback,
Looper.getMainLooper()
)
}
} else {
//permission not granted
return
}
}
}
}La fonction requestLocationPermission() est utilisée pour demander la permission de localisation à l'utilisateur. Elle utilise la méthode ActivityCompat.requestPermissions pour afficher la boîte de dialogue standard demandant la permission, que nous avons au préalable définie dans le manifeste.
La méthode onRequestPermissionsResult est appelée lorsque l'utilisateur répond à la demande de permission. Elle vérifie si la réponse correspond à la demande de localisation en fonction du code de demande (dans notre cas MY_PERMISSIONS_REQUEST_LOCATION).
Si la permission est accordée (PackageManager.PERMISSION_GRANTED), la méthode demande des mises à jour de localisation en utilisant fusedLocationProvider?.requestLocationUpdates , comme nous le faisions déjà dans la méthode onResume .
Si la permission n'est pas accordée, aucune action n'est entreprise.
Mais on fait appel à requestLocationUpdates à deux reprises ? On se répète ?
La raison pour laquelle nous utilisons requestLocationUpdates deux fois, une fois dans onResume et une fois dans onRequestPermissionsResult , est la suivante : ces deux appels servent des finalités différentes.
L'appel dans la méthode onResume est automatiquement déclenché lors de la création de la vue, et il demande automatiquement la géolocalisation de l'utilisateur. Cela est possible car l'utilisateur a déjà donné son consentement précédemment.
En revanche, dans la méthode onRequestPermissionsResult , nous vérifions spécifiquement si l'utilisateur a répondu positivement à la fenêtre de demande d'autorisation que nous venons de lui présenter.
Pour des raisons de visibilité, notre code ne gère pas le cas où l’utilisateur refuserait la permission. Mais vous pouvez retrouver la MainActivity avec ces conditions supplémentaires ici.

Votre application est utilisée par tous les membres du service, c’est un succès ! Félicitations. On vous propose de la fournir aux stations voisines aussi.
Vous acceptez, car après une amélioration du chargement des données et une géolocalisation de l’utilisateur dans sa station de ski, vous aurez une application fonctionnelle pour toutes les stations du monde !
Votre application quitte le mont Blanc, elle est devenue grande !
Votre mission actuelle consiste à :
localiser l’utilisateur avant de lui fournir les informations météorologiques pertinentes ;
afficher un état de chargement et d’erreur au besoin, avec la classe Result .
Le corrigé est disponible sur GitHub :
Commit mettant à jour l’application pour utiliser la classe Result .
La classe sealed Result est utilisée pour gérer de manière structurée les différents états d'une opération, comprenant Loading , Success et Failure .
Le repository renvoie un flux ( Flow ) de résultats ( Result ) pour représenter les différentes étapes de la récupération des données et informer visuellement l'utilisateur.
Les propriétés isViewLoading et errorMessage sont ajoutées à la classe HomeUiState pour gérer les états de chargement et d'erreur de l'interface utilisateur, fournissant ainsi une représentation visuelle de l'état de notre application.
Notre application est bien plus fiable maintenant, mais nous pouvons encore améliorer certains points en testant notre produit avant de le fournir à nos utilisateurs ! Soyons nos propres testeurs.