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"
Gérez des erreurs
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.
Définissez une classe d’état
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ériqueR
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.
Écoutez l’état des requêtes
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.Result
Voyons 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éthodeFlow
, 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.
Affichez un message d’erreur à l'utilisateur
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 !
Géolocalisez l’utilisateur
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.
À vous de jouer
Contexte
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 !
Consignes
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
.
Corrigé
Le corrigé est disponible sur GitHub :
Commit mettant à jour l’application pour utiliser la classe
Result
.
En résumé
La classe sealed
Result
est utilisée pour gérer de manière structurée les différents états d'une opération, comprenantLoading
,Success
etFailure
.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
eterrorMessage
sont ajoutées à la classeHomeUiState
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.