• 10 heures
  • Facile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 12/03/2024

Améliorez la qualité de votre application

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é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.

É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é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.

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 :

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, 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.

Exemple de certificat de réussite
Exemple de certificat de réussite