Des concepts fondamentaux, des constructeurs remarquables et des méthodes utiles gèrent avec élégance les tâches asynchrones
Dans le monde réel, les contrats conclus entre les parties par des entités juridiques impliquent le déplacement (par vente ou location) d’un actif ou d’une marchandise d’une partie à une autre pour un prix spécifié à une période spécifiée.
Ces contrats sont appelés contrats à terme.
Dans le langage Dart, le Future
classe a un concept de travail similaire à un contrat à terme réel, qui existe dans le monde réel, d’où son nom. Contrairement au contrat du monde réel, celui-ci implique plutôt des transactions de données.
Les contrats à terme nous fournissent une méthode de gestion des tâches asynchrones d’une manière plus soignée et gérable. Chaque fois que nous utilisons async-wait pour gérer nos tâches asynchrones, Dart impose que la valeur de retour soit de type Future. Cela nous permet de le résoudre sans casser le programme.
Comme il s’agit d’un sujet très vaste et qu’il mérite d’être reconnu et expliqué clairement, avec des exemples, j’ai décidé d’en faire un article en deux parties.
Dans cette première partie, j’aurai pour objectif de :
- Fournir une compréhension conceptuelle de
Future
s, comment cela fonctionne avec la boucle d’événements pour gérer les tâches asynchrones et les états qui agissent comme des indicateurs. - Discutez des constructeurs notables et de leurs utilisations potentielles.
- Discutez des principales méthodes impliquées et de la manière dont elles nous aident à résoudre les problèmes liés aux opérations asynchrones.
Dart est un langage de programmation monothread. Il utilise la boucle d’événements pour sélectionner une tâche après l’autre et l’exécuter de manière séquentielle. C’est ainsi que l’illusion d’exécution des tâches en arrière-plan est réalisée par Dart, même en l’absence d’un fil d’arrière-plan.
Future
nous fournit les API nécessaires pour faciliter l’accès à la boucle d’événements et gérer les tâches asynchrones. Il nous permet de faire ce qui suit :
- Encapsuler la tâche et l’envoyer pour traitement
- Identifier l’état actuel de la tâche et son achèvement
- Obtenir le résultat de la tâche si elle est réussie
- Obtenir l’erreur qui a provoqué l’échec de la tâche
Pour nous permettre de suivre l’état actuel d’un Future
on s’appuie sur trois états distincts :
- Inachevé : la tâche est toujours en cours.
- Terminé avec les données : la tâche est terminée et les données sont prêtes.
- Terminé avec erreur : la tâche est terminée, mais avec une erreur.
Une fois terminé, nous utiliserons then()
pour obtenir le résultat, ou nous utiliserons catchError()
pour obtenir l’erreur renvoyée. Cela ressemblera à ceci :
var list = coffeeList
.then((value) => (value)) // Success
.catchError((error) => (error)); // Failure
Ces états nous permettent de gérer facilement les transitions d’interface utilisateur lors d’un processus asynchrone. Nous pourrions afficher une interface utilisateur de chargement lorsque l’état est inachevé, afficher les données lorsqu’il est complété avec des données ou afficher une fenêtre contextuelle d’erreur lorsqu’il est complété avec une erreur.
Un point à noter ici est que les futures API et méthodes établissent également des parallèles étroits avec les Promise
objet en JavaScript. On peut observer l’utilisation de then()
et comment il déballe les données en résolvant le futur.
En raison de cette ressemblance avec Promise
les développeurs ayant une expérience de JavaScript auront plus de facilité à assimiler les usages et les méthodologies de Future.
Avant d’explorer nos différents concepts, établissons une base pour notre expérimentation.
Imaginons que nous développions une application qui facilitera la consultation de la dernière sélection de cafés dans notre café local de l’autre côté de la rue.
Nos données pour les cafés chauds proviendront d’ici : https://api.sampleapis.com/coffee/hot
Cette API nous renvoie un tableau de cafés chauds, et un seul objet ressemblera à ceci :
{
"title":"Black",
"description":"Black coffee is as simple as it gets with ground coffee beans steeped in hot water, served warm.",
"ingredients":[
"Coffee"
],
"image":"https://upload.wikimedia.org/wikipedia/commons/thumb/4/45/A_small_cup_of_coffee.JPG/640px-A_small_cup_of_coffee.JPG",
"id":1
}
Les constructeurs sont des méthodes que nous utilisons pour initier une instance d’une classe. Quelques constructeurs sont très utiles lors de la création d’une instance d’un Future
cela nous donne des avantages clés.
Avant de passer à des implémentations plus complexes d’un Future
, examinons une manière très simple de l’initialiser. Sur la base du problème mentionné ci-dessus, nous pouvons créer un Future
qui récupère les données pour nous comme ceci:
void main() {
Future<http.Response> coffeeData =
http.get(Uri.parse('https://api.sampleapis.com/coffee/hot'));
}
Cette coffeeData
détient maintenant les données dont nous avons besoin, et nous pouvons facilement résoudre ce problème en utilisant then()
et nous pouvons voir s’il y a une erreur en utilisant catchError()
. C’est aussi simple que ça.
coffeeData.then((value) => (value)) // Success
.catchError((error) => (error)); // Failure
Énoncé du problème
Aux premiers stades du développement, il se peut que nous n’ayons pas d’API à partir de laquelle obtenir des données, car elle pourrait être en cours de développement par l’équipe backend.
Nous devons développer l’interface utilisateur de notre application de café car la conception est prête et nous disposons d’un ensemble d’exemples de données disponibles pour travailler sur la couche de présentation.
Comment se moquer de l’illusion d’une requête et d’une réponse HTTP pour développer l’interface utilisateur avec l’état de chargement et de réussite bien pris en compte ?
La solution
C’est là que nous pouvons tirer parti de la Future.delayed()
constructeur. Ce constructeur nous permet de créer un Future
tâche qui s’exécute dans son encapsulation après un délai que nous spécifions.
Ainsi, si nous voulions développer un écran de chargement qui s’affichera pendant la récupération, puis affichera l’interface utilisateur sans qu’une API réelle soit présente, nous pouvons le faire comme ceci :
void main() {
List<Coffee> coffeeList = [
const Coffee(
id: 1,
title: 'Black',
description:
"If you want to sound fancy, you can call black coffee by its proper name: cafe noir.",
ingredients: ['Coffee'],
image:
"https://upload.wikimedia.org/wikipedia/commons/thumb/4/45/A_small_cup_of_coffee.JPG/640px-A_small_cup_of_coffee.JPG",
)
];Future.delayed(const Duration(milliseconds: 2000), () {
// The code below will be executed after a delay of 2 seconds
return coffeeList;
});
}
Comme vous pouvez le voir dans le code ci-dessus, nous avons utilisé Future.delayed()
pour retarder le retour des données fictives de deux secondes. Cela nous permet de créer une illusion de demande-réponse.
Nous pouvons maintenant développer l’écran de chargement et l’écran de résultat, et nous pouvons également le voir en action sans dépendre d’une API.
Énoncé du problème
Nous nous sommes moqués du scénario dans lequel nous pouvons avoir un état de chargement avant qu’une liste de données ne nous soit renvoyée. Ainsi, nous avons terminé le développement de notre application café pour avoir une interface utilisateur de chargement et une liste de cafés pouvant être affichées. Mais Future.delayed()
nous renvoie toujours une valeur positive.
Et si nous voulions émuler un scénario où la demande échoue pour une raison quelconque ?
La solution
C’est ici que Future.delayed()
entre en scène. Nous pouvons créer un Future
qui renverra une erreur au lieu d’une valeur positive et l’utilisera pour développer l’interface utilisateur de scénario d’échec correspondante. Nous pouvons le faire simplement comme ceci:
void main() {
final Future<Exception> apiErrorTest = Future.delayed(const Duration(milliseconds: 2000), () {
// The code below will return an error after a delay of 2 seconds
return Future.error(
Exception('Failed to fetch data from coffee endpoint.'), // Throw
);
});
}
Comme vous pouvez le voir dans le code ci-dessus, nous retardons la réponse en utilisant Future.delayed()
mais cette fois il renvoie un Future.error()
, qui contient une exception. En utilisant cela, nous pouvons émuler l’échec d’un appel de récupération d’API.
Il y a quatre méthodes principales que nous aborderons dans cette section, qui je pense sont très importantes pour le fonctionnement global de Future
en tant que gestionnaire d’événements asynchrones.
alors()
Cette méthode a reçu des exemples de son fonctionnement à quelques endroits dans cet article, mais ne la traite pas spécifiquement. then()
est une méthode de rappel qui nous permet de résoudre le contrat qui nous est donné dans le Future
et obtenir le résultat le Future
tient s’il réussit.
void main() {
Future<http.Response> coffeeData =
http.get(Uri.parse('https://api.sampleapis.com/coffee/hot'));coffeeData.then((value) => (value));
}
Dans l’exemple ci-dessus, then()
nous fournit la liste des cafés que nous obtenons de l’appel API.
Mais que se passe-t-il si c’est un échec ? Si nous le regardons à sa valeur nominale, il semble que l’appel échouera silencieusement, nous rendant incapables d’attraper l’erreur. Mais si nous regardons l’implémentation sous-jacente, elle a un onError
appel de fonction :
Future<R> then<R>(FutureOr<R> onValue(T value), {Function? onError});
Cela permet then()
pour intercepter l’erreur via le gestionnaire d’erreurs global, qui capture les erreurs non interceptées dans l’ensemble de l’application. Cette solution de secours existe pour garantir que les erreurs n’échouent pas silencieusement.
catchErreur()
Nous avons vu comment then()
nous renvoie une valeur lorsqu’il réussit et signale toujours une erreur sans le laisser échouer en silence. Même si then()
il peut le faire en s’appuyant sur le gestionnaire d’erreurs global, il est souvent recommandé qu’un gestionnaire d’erreurs distinct soit enregistré pour gérer les erreurs avec élégance.
C’est ici que catchError()
entre en scène. Similaire à then()
, catchError()
est également une fonction de rappel. Cette méthode ne fonctionne pas individuellement et est associée à then()
pour apporter une solution holistique. Mais contrairement à, then()
, catchError()
renvoie l’erreur de l’opération asynchrone :
void main() {
Future<http.Response> coffeeData =
http.get(Uri.parse('https://api.sampleapis.com/coffee/hot'));coffeeData.then((value) => (value))
.catchError((error) => (error));
}
Dans l’exemple ci-dessus, si l’appel d’API échoue, nous pouvons gérer l’erreur via le catchError((error) => (error))
bloc de code.
catchError()
nous permet d’y ajouter des tests. Regardons sa mise en œuvre ci-dessous :
Future<T> catchError(Function onError, {bool test(Object error)?});
Comme nous pouvons le voir à partir de l’implémentation, nous pouvons éventuellement ajouter des tests à catchError()
. Cela nous permet de savoir à quel type d’erreur nous sommes confrontés. Prenons un exemple de test :
coffeeListFuture.then((value) => (coffeeList = getCoffeeList(value)))
// Custom error catch block
.catchError(
(
Object error,
StackTrace stackTrace,
) {
print(error.toString());
},
test: (Object error) {
return error is HttpException;
}
);
Dans le bloc de code ci-dessus, nous pouvons voir qu’un catchError()
le rappel a été ajouté avec un test pour vérifier s’il s’agit d’un type d’exception HTTP. Si le test renvoie un true
valeur booléenne, nous pouvons traiter cette exception en fonction des exigences commerciales spécifiques pour les exceptions HTTP.
Énoncé du problème
L’inconvénient d’inclure des tests est que ce bloc de catchError()
n’attrapera que les erreurs qui réussissent ce test. Ceux qui ne correspondent pas à ce test seront rejetés comme des erreurs non interceptées. Comment capter ceux qui ne correspondent pas ? Et si nous avions besoin d’identifier différents types d’erreurs plutôt qu’une seule ?
La solution
C’est là que la possibilité d’enregistrer plusieurs catchError()
les mêmes méthodes Future
entre en jeu. Nous pouvons avoir plus d’un catchError()
méthode, chacun écoutant un type spécifique d’erreur. Voyons comment avec le code suivant :
coffeeListFuture.then((value) => (coffeeList = getCoffeeList(value)))
// Custom error catch block
.catchError(
(
Object error,
StackTrace stackTrace,
) {
print(error.toString());
},
test: (Object error) => error is CustomException)
// Http error catch block
.catchError(
(
Object error,
StackTrace stackTrace,
) {
print(error.toString());
},
test: (Object error) => error is HttpException)
Dans l’exemple ci-dessus, nous pouvons voir deux catchError()
les méthodes étant liées à un seul Future
la première catchError()
intercepte les erreurs personnalisées définies par nous, et la seconde nous permet de capturer les exceptions HTTPS.
coffeeListFuture.then((value) => (coffeeList = getCoffeeList(value)))
// Http error catch block
.catchError(
(
Object error,
StackTrace stackTrace,
) {
print(error.toString());
},
test: (Object error) => error is HttpException)
// General error catch block
.catchError(
(error) => print(error)
);
Dans l’exemple ci-dessus, nous avons créé une sécurité intégrée pour capturer les erreurs qui ne relèvent pas du HttpException
type pour éviter que les erreurs ne soient traitées comme des exceptions non gérées.
Pouvoir joindre plusieurs catchError()
, nous permet de définir différentes manières de gérer les erreurs pour notre application. Cela nous permet de répondre différemment à plusieurs scénarios d’erreur, améliorant ainsi l’expérience utilisateur globale de notre application.
Les applications ont besoin de continuité, quel que soit le résultat d’une seule tâche asynchrone. Nous avons discuté de deux scénarios ci-dessus, un scénario positif lorsque la tâche est réussie et une tâche de scénario négatif est un échec, en utilisant then()
et catchError()
respectivement.
Énoncé du problème
Mais que se passe-t-il si nous voulons qu’un morceau de code s’exécute quel que soit le résultat de cette tâche asynchrone ? Et si nous voulions, soi-disant, appeler une méthode pour récupérer une liste de cafés froids, quel que soit le point de terminaison ne nous fournissant pas une liste de cafés chauds ?
La solution
C’est ici que whenComplete()
entre en scène. Cette méthode nous permet d’exécuter des codes qui s’exécuteront indépendamment du fait que Future
se termine par un résultat ou une erreur.
Pour ceux d’entre nous qui viennent d’un milieu JavaScript, cela agit de la même manière qu’un finally()
travaille pour un Promise
.
Voyons comment cela peut être mis en œuvre. Nous allons créer une fonction qui retourne un Future
avec une réponse HTTP contenant la liste des cafés basée sur le point de terminaison hot
ou cold
.
Future<http.Response> fetchCoffeeData({required String coffeeTemp}) async {
const baseURL = 'https://api.sampleapis.com/';
final coffeeResponse =
await http.get(Uri.parse(baseURL + '/coffee/' + coffeeTemp));if (coffeeResponse.statusCode == 200) {
return coffeeResponse;
} else {
throw Exception('Failed to fetch coffees');
}
}
Maintenant, créons un Future
et attachez then()
, catchError()
et whenComplete()
pour lui montrer ce dont nous avons discuté.
Future<http.Response> coffeeListFuture = fetchCoffeeData(coffeeTemp: 'hot');coffeeListFuture
.then((value) => value)
.catchError((error) => error)
.whenComplete(() => fetchCoffeeData(coffeeTemp: 'cold'));
Comme on peut l’observer, nous pouvons exécuter une instruction qui récupère une liste de cafés froids, quel que soit le résultat de coffeeListFuture
. Cela nous donne une idée claire de ce qui doit se passer immédiatement après la première Future
est terminé.
À partir des discussions et des explications fournies ci-dessus, nous avons couvert les concepts clés entourant le Future
class et comment cela nous facilite la vie lorsqu’il s’agit de tâches asynchrones.
J’espère que ceux qui ont lu cet article ont maintenant une bonne compréhension de ce qui suit :
- Comment
Future
fonctionne dans un environnement à thread unique et nous permet d’interagir avec la boucle d’événements. - Comment, en substance, il est très similaire à
Promise
en JavaScript. - Comment l’utilisons-nous pour gérer les tâches asynchrones
- Comment pouvons-nous simuler le chargement de l’API en utilisant
Future
- Comment savoir si les appels asynchrones réussissent ou s’ils ont échoué
- Comment simuler les états de chargement, de réussite et d’erreur d’un appel d’API à l’aide des constructeurs.
- Quelles sont les méthodes remarquables dont nous disposons et pourquoi en avons-nous besoin pour gérer les tâches asynchrones via Futures.
Dans Futures in Dart Explained (Partie 2), je discuterai des façons d’utiliser Futures pour résoudre légèrement des problèmes de programmation plus complexes.