Évitez que votre application freeze
Vous recevez un nouveau message de Margaret pour vous expliquer la suite de la mission :
Bonjour,
Un point essentiel dont nous discuterons lors de cette phase concerne la stabilité et la fluidité de l'application. Nous aborderons comment éviter les plantages et rendre l'expérience utilisateur aussi fluide que possible grâce à l'utilisation de coroutines.
En parallèle, nous travaillerons sur l'interface utilisateur de l'application. Adele, notre spécialiste en design d'interfaces, fournira les éléments nécessaires pour créer une interface conviviale. L'objectif est de permettre aux utilisateurs de visualiser rapidement les conditions météorologiques et de planifier leurs séances d'observation.
Cordialement,
Margaret Hamilton, Dev Senior – Projet "Planète Exploration"
Dans ce nouveau chapitre, nous allons collecter et afficher les informations dans l’application.
Si nous reprenons notre exemple du serveur qui va chercher vos commandes en cuisine, il y a un aspect que nous n’avons pas traité : que fait le client pendant ce temps ? Il patiente.
Dans notre contexte, une application mobile qui patiente est une application qui gèle/freeze. Elle ne fonctionne plus et l’OS Android vous propose de la fermer/tuer (kill). En tant que développeur consciencieux, nous ne voulons pas de ça. Par conséquent, nous allons utiliser des méthodes asynchrones pour éviter cet inconvénient.
Découvrez les coroutines
En programmation, une coroutine peut être vue comme une unité de travail qui permet d'effectuer des tâches de manière asynchrone sans bloquer le fil d'exécution principal de l'application. Les coroutines sont des structures légères et flexibles, facilitant la gestion des opérations longues sans sacrifier la lisibilité du code.
La gestion asynchrone est cruciale dans le développement d'applications Android pour éviter les retards et les gels de l'interface utilisateur. Les coroutines offrent une alternative moderne et élégante aux approches traditionnelles telles que la gestion manuelle de callbacks, AsyncTask, ou encore l'utilisation de threads qui peuvent être gourmands en ressources.
Par exemple, dans le développement d'applications, on peut avoir un "Thread UI" qui gère l'interface utilisateur, assurant une réponse fluide aux interactions de l'utilisateur, tandis qu'un "Thread réseau" s'occupe des opérations de communication avec un serveur distant, comme le chargement de données.
L'utilisation de threads distincts améliore la réactivité de l'application en évitant que des tâches intensives bloquent l'interface utilisateur, assurant ainsi une expérience utilisateur plus fluide.
Utilisez une méthode asynchrone
Pour mieux comprendre l'utilité d'une méthode asynchrone à l'aide des coroutines, examinons d'abord un contre-exemple. Copiez le code suivant dans votre MainActivity, puis lancez l'application. Vous remarquerez qu'Android vous propose de fermer l'application en raison du gel de la méthode suivante :
for (i in 1..10000) {
Thread.sleep(5)
}
Le gel se produit parce que la méthode effectue un traitement long et bloquant directement sur le thread principal, ce qui provoque un gel de l'interface utilisateur. On vous propose de tuer l’application. Android, par défaut, utilise toujours le thread principal si on ne spécifie rien.
Maintenant, pour résoudre ce problème, nous allons transformer cette méthode en une version asynchrone en utilisant des coroutines. Refaites la même manipulation que précédemment avec le code suivant, et vous constaterez que vous obtenez ce résultat dans les logs cette fois-ci :
GlobalScope.launch(Dispatchers.IO) {
for (i in 1..10000) {
Thread.sleep(5)
}
println("Sleepy function done")
}
En utilisant le contexte Dispatchers.IO
, le traitement long s'effectue dans un thread dédié au réseau, laissant le thread principal libre pour gérer les interactions de l'interface utilisateur. Ainsi, l'application reste réactive et ne se fige pas pendant le traitement asynchrone.
L'application peut utiliser le thread UI/principal pour dessiner la vue sans être bloquée, assurant ainsi une meilleure expérience utilisateur.
La création d’une coroutine sur le thread réseau nous permet d’attendre le retour de notre requête sans bloquer l’application. Par défaut, Retrofit utilise une coroutine sur le thread réseau, nous n’aurons donc pas sa déclaration à faire de notre côté.
Découvrez l’utilité d’un repository
À cette étape, nous allons créer un repository, un composant essentiel de l'architecture logicielle de notre application Android.
Le repository présente plusieurs avantages pour la gestion des données dans une application Android :
Il simplifie l'accès aux différentes sources de données en fournissant des méthodes retournant des données.
Il facilite l'implémentation de la logique métier, en isolant le ViewModel de la complexité des opérations sur les données.
Il est responsable de la qualité des données que nous transmettons à notre ViewModel ; en effet, vu que nous pouvons avoir plusieurs sources (locales ou en ligne), le jeu de données peut changer. On utilisera donc notre objet
DomainModel
ici pour être sûr de ce que reçoit notre ViewModel .
Le repository sera donc notre magasin où nous stockerons temporairement nos produits (données). Les clients (ViewModels) peuvent se rendre au magasin pour obtenir ce dont ils ont besoin, sans avoir à se soucier des détails internes de gestion des stocks. Est-ce que les données sont présentes localement ou en ligne ? Le ViewModel ne s’en inquiète pas, c’est au repository de gérer les données. Avec notre application météo, nous ne faisons que des interactions en ligne, mais si besoin, c’est dans le repository qu’il faudrait faire appel au cache, par exemple.
Mettez en place un repository
Pour illustrer la mise en œuvre d'un repository, commençons par définir une méthode qui utilise le concept de flow (flux), une fonctionnalité avancée de Kotlin Coroutine. Cela nous permettra de gérer de manière asynchrone les flux de données, ce qui est particulièrement utile pour les opérations qui peuvent prendre du temps, comme les requêtes réseau. Flow est une coroutine qui permet d'émettre des données en continu si besoin.
Créons une classe nommée data.repository.WeatherRepository
, comme nous en avons déjà créé auparavant.
import android.util.Log
import com.openclassrooms.stellarforecast.data.network.WeatherClient
import com.openclassrooms.stellarforecast.domain.model.WeatherReportModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
class WeatherRepository(private val dataService: WeatherClient) {
private val API_KEY = "REPLACE WITH YOUR API KEY HERE"
fun fetchForecastData(latouble, lngouble): Flow<List<WeatherReportModel>> = flow {
val result = dataService.getWeatherByPosition(lat, lng, API_KEY)
val model = result.body()?.toDomainModel() ?: throw Exception("Invalid data")
emit(model)
}.catch { error ->
Log.e("WeatherRepository", error.message ?: "")
}
}
Voici en détail ce que vous pouvez lire dans ce snippet de code :
Rôle du
WeatherRepository
:Agit comme un gardien des prévisions météorologiques, interagissant avec Retrofit et donc notre API pour récupérer ces données et les rendre accessibles à d'autres parties de l'application.
Définit la clé API nécessaire pour l'interrogation du service météorologique distant.
Fonction
fetchForecastData
:Prend en paramètres la latitude (lat) et la longitude (lng) pour spécifier l'emplacement désiré.
Utilise un flux (flow) pour gérer les données de manière asynchrone, créant ainsi une coroutine. À chaque fois qu’une nouvelle donnée sera disponible, nous n’aurons qu'à émettre avec la fonction “emit” propre au “flow”.
Séquence d'instructions dans la
flow
:Utilise la clé API pour demander les prévisions météorologiques au
dataService
, qui représente le serveur distant.Traite le résultat : transforme les données en un modèle adapté à l'application à l'aide de
toDomainModel()
.Émet ce modèle dans la
flow
avecemit
.Gestion des erreurs :
Utilise
catch
pour gérer toute exception potentielle.Affiche un message d'erreur dans le journal en cas de problème ; nous emploierons cette gestion des erreurs dans un prochain chapitre.
En résumé, le WeatherRepository
assure une communication entre votre ViewModel et le service météorologique distant, garantissant que les prévisions météorologiques sont récupérées et traitées correctement, même en cas d'obstacles sur le chemin.
Pour permettre à Hilt de configurer automatiquement notre repository, nous allons créer un nouveau module, de la même manière que nous l'avons fait pour Retrofit. Créez une classe nommée di.DataModule
et copiez le code suivant :
import com.openclassrooms.stellarforecast.data.network.WeatherClient
import com.openclassrooms.stellarforecast.data.repository.WeatherRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
@Singleton
@Provides
fun provideWeatherRepository(dataClient: WeatherClient): WeatherRepository {
return WeatherRepository(dataClient)
}
}
Étant donné que nous avons déjà défini WeatherClient
avec Hilt, il sera en mesure de le fournir à WeatherRepository
et de créer l'objet en conséquence.
À vous de jouer
Contexte
Alors que vous contemplez à travers la fenêtre les premiers flocons de neige de l'année, une sensation de fraîcheur hivernale envahit l'air.
La saison tant attendue est enfin arrivée ! (Si malheureusement, nous sommes en plein été, imaginez-vous en Antarctique :)).
Avec l'arrivée de cette nouvelle période, il est impératif de se concentrer sur la gestion des données qui seront nécessaires pour créer une interface utilisateur informative.
Pour cela, la classe Repository
, notre sauveur de données, doit être définie. Elle jouera un rôle crucial dans la récupération des informations essentielles, permettant ainsi de concrétiser la conception de la vue tant désirée.
Consignes
Votre mission actuelle consiste à :
définir une classe
Repository
.
Corrigé
Le corrigé est disponible sur GitHub à cette adresse :
Déclaration du Repository .
En résumé
Les coroutines sont des structures légères permettant la gestion asynchrone du code. Elles simplifient le travail avec des tâches longues et des opérations asynchrones sans le fardeau de la gestion manuelle des threads.
Les coroutines, telles que
GlobalScope.launch(Dispatchers.IO)
, offrent une gestion asynchrone efficace en nous permettant de sélectionner notre thread, évitant le gel de l'application. Retrofit utilise ces coroutines.L'utilisation de threads distincts, tels que le "Thread UI" et le "Thread réseau", optimise la réactivité de l'application.
Le repository simplifie l'accès aux données avec des méthodes améliorant la logique métier dans l'application Android. La méthode
fetchForecastData()
utilise des coroutines pour récupérer de manière asynchrone les prévisions météorologiques, préservant ainsi la réactivité de l'interface utilisateur.
Nous en avons terminé avec la configuration et la gestion des données ; orientons-nous vers le ViewModel qui nous permettra de nourrir notre interface graphique.