Aller au contenu principal

Appels HTTP avec DIO 🙏

DIO est une bibliothĂšque Dart pour envoyer des requĂȘtes HTTP. Vous crĂ©ez un client dio et appelez directement ses mĂ©thodes get, post, etc.

Dans cette recette une requĂȘte va ĂȘtre faite Ă  l'API de REST Countries, pour rechercher un pays, et en affichant l'emoji de son drapeau ainsi que sa signification. Si ce n'est pas dĂ©jĂ  fait, crĂ©ez vous une clĂ© d'API de REST Countries.

Avant de commancer

Il est fortement recommandĂ© d'avoir consultĂ© la recette de Bruno 🐕 et d'avoir mis en place les Secrets đŸ€« et la Configuration 🧑‍🔧 avant de se lancer dans cette recette.

1. DĂ©pendances 🚬🚬🚬​

Depuis le dossier de votre projet Flutter, ajouter dio :

flutter pub add dio

Ces dĂ©pendances seront utiles pour nous aider Ă  passer de rĂ©sultat requĂȘte HTTP Ă  objet Dart.

flutter pub add json_annotation
flutter pub add --dev build_runner json_serializable
astuce

Le drapeau --dev indique que la dĂ©pendance doit ĂȘtre installĂ©e uniquement en dĂ©veloppement, soit quand un dĂ©veloppeur travaille sur l'application. Ces packages ne seront pas inclus dans le paquetage final.

2. Architecture 📐​

Puisque vous commencez Ă  savoir ce qu'est une requĂȘte HTTP, nous allons surtout nous concentrer sur l'architecture de notre application. Notre objectif sera d'avoir une premiĂšre version viable, que nous allons ensuite retravailler.

Voici ce que nous vous proposons pour commencer :

lib/
│── network/
│ └── api_client.dart // On centralise le client qui effectue les requĂȘtes.
│ └── dto/ // Classes de transfert
│ └── country_details_response.dart
│── pages/
│ └── country_search_page.dart // Page qui va afficher le rĂ©sultat des appels.
└── services/ // Regrouper les requĂȘtes HTTP
└── country_service.dart

Voici le contenu des nouveaux fichiers. Prenez le temps de les lire, surtout les commentaires plutĂŽt que de simplement copier-coller.

3. lib/network/dto/country_details_response.dart​

Tel que vu dans la recette de Bruno 🐕, la rĂ©ponse de la requĂȘte HTTP de recherche d'un pays est Ă©norme, mĂȘme lorsqu'il n'y a qu'un seul pays trouvĂ©.

Nous allons donc vouloir cibler les quelques champs qui nous intéressent.

// Inclure le fichier généré qui facilite les transformations
// json -> objet dart et objet dart -> json
// Restera rouge tant que cette commande n'aura pas été exécutée :
// dart run build_runner build
// N'ayez pas peur de jeter un coup d'oeil à ce qui est dans ce fichier généré.
// C'est moins complexe qu'il n'y parait.
part 'country_details_response.g.dart';

()
class CountryDetailsResponse {
final String description;
final String emoji;

CountryDetailsResponse({required this.description, required this.emoji});

// DrĂŽle de code! Permet de passer de json Ă  CountryDetailsResponse.
// Ça utilise une fonction gĂ©nĂ©rĂ© dans country_details_response.g.dart
factory CountryDetailsResponse.fromJson(Map<String, dynamic> json) =>
_$CountryDetailsResponseFromJson(json);

// Autre drĂŽle de code! Permet de passer de CountryDetailsResponse Ă  json.
// Ça utilise aussi une fonction gĂ©nĂ©rĂ© dans country_details_response.g.dart
Map<String, dynamic> toJson() => _$CountryDetailsResponseToJson(this);
}

4. lib/network/api_client.dart 🛜​

// Le client est rĂ©utilisĂ© Ă  chaque requĂȘte
class CountryApiClient {
late final Dio dio;

CountryApiClient() {
final AppConfig config = ConfigFactory.create();
// CrĂ©er le DIO qui va faire effectuer les requĂȘtes
dio = Dio(
BaseOptions(
// Toutes les requĂȘtes vont ĂȘtre envoyĂ©es Ă  l'URL dans AppConfig.apiUrl
baseUrl: config.countryApiUrl,
),
);

// Comme en 4W6, on ajoute un intercepteur pour inclure le token d'authorisation dans les headers Ă  chaque requĂȘte.
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] =
'Bearer ${dotenv.env['REST_COUNTRIES_API_KEY']}';
// Permet Ă  un autre intercepteur d'Ă©ventuellement modifier la requĂȘte sortante.
handler.next(options);
},
),
);
}
}

3.4 lib/services/country_service.dart 🐕‍đŸŠș​

Le service utilise CountryApiClient pour effectuer ses appels de service.

class CountryService {
final CountryApiClient _countryApiClient;

// CountryService a besoin d'un CountryApiClient pour effectuer ses requĂȘtes.
CountryService(this._countryApiClient);

// Notez le mot clé Future. C'est l'équivalent de Task en C#
// Ça indique que l'exĂ©cution de la fonction peut ĂȘtre retardĂ©e
// Encore une fois comme en C#, le mot clé async indique que la fonction est asynchrone
// Elle ne s'exécutera donc pas séquentiellement
Future<List<CountryDetailsResponse>> getCountryDetails(String country) async {
Response<dynamic> response = await _countryApiClient.dio.get(
'/countries/v5?q=$country',
);
dynamic jsonRawContent = response.data!;
// Regardez le rĂ©sultat de cette requĂȘte dans Bruno pour mieux comprendre ce qui est fait ici
List<dynamic> objects = jsonRawContent["data"]["objects"];
return objects
.map((dynamic json) => CountryDetailsResponse.fromJson(json["flag"]))
.toList();
}
}

3.5 lib/pages/country_search_page.dart​

Prenons cette interface graphique :

class CountrySearchPage extends StatefulWidget {
const CountrySearchPage({super.key});


State<CountrySearchPage> createState() => _CountrySearchPageState();
}

class _CountrySearchPageState extends State<CountrySearchPage> {
final TextEditingController _countryTextController = TextEditingController(
text: 'peru',
);
final CountryService _countryService = CountryService(CountryApiClient());

// Contiendra la liste des pays trouvés
List<CountryDetailsResponse> _countries = [];

// D'autre code viendra ici


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("DIO"),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_queryTextField(),
SizedBox(height: 24),
_searchButtons(),
SizedBox(height: 16),
_countryListView(),
],
),
),
);
}

Expanded _countryListView() {
return Expanded(
child: ListView.builder(
itemCount: _countries.length,
itemBuilder: (BuildContext context, int index) => Row(
children: [
Text(_countries[index].emoji, style: TextStyle(fontSize: 32)),
SizedBox(width: 24),
Flexible(child: Text(_countries[index].description)),
],
),
),
);
}

Row _searchButtons() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ElevatedButton(
onPressed: _getCountryDetailsAwait,
child: Text("Appel avec await"),
),
ElevatedButton(
onPressed: _getCountryDetailsThen,
child: Text("Appel avec then"),
),
],
);
}

TextField _queryTextField() {
return TextField(
controller: _countryTextController,
decoration: InputDecoration(
// Si vous voulez tester, le nom des pays doivent ĂȘtre en anglais. "uni" est un bon terme de recherche. hintText: 'Ex : peru',
),
);
}
}

Ce qui nous intéresse le plus, ce sont les fonctions que les 2 boutons appellent :

Le fait que getCountryDetails (du service) soit asynchrone force la fonction qui l'appelle à le gérer. C'est un peu comme les exception :

  1. Soit on propage le fait que la fonction est asynchrone (la fonction qui l'appelle devra le gérer)
  2. Soit on le gĂšre directement dans la fonction

_getCountryDetailsAwait​

Ici on le propage : la fonction retourne un type Futur{'<'}> et est async.

Future<void> _getCountryDetailsAwait() async {
// Ressemble à la syntaxe utilisée en C# et en Javascript
try {
final List<CountryDetailsResponse> response = await _countryService
.getCountryDetails(_countryTextController.text);
print(response);
setState(() {
_countries = response;
});
} on DioException catch (e) {
print(e.error);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.response!.data["errors"][0]["message"])),
);
}
}

_getCountryDetailsThen​

Ici nous avons exactement le mĂȘme comportement que pour _getCountryDetailsAwait, mais on prend la 2ieme façon de gĂ©rer l'appel. L'asynchronisme est gĂ©rĂ© directement dans la fonction. Remarquez que Future{'<'}>, async et await ont disparus puisque la fonction anonyme pour gĂ©rer le retour sera appelĂ©e quand l'appel se terminera.

void _getCountryDetailsThen() {
// PremiÚre ligne exécutée ci dessous
_countryService
.getCountryDetails(_countryTextController.text)
.then(
// Ce qu'on fait quand tout s'est passé comme on le souhaite
(List<CountryDetailsResponse> response) {
// TroisiÚme ligne exécutée, lorsque l'appel HTTP est terminé
print(response);
setState(() {
_countries = response;
});
},
// Ce qu'on fait quand une erreur est survenue
onError: (e) {
print(e);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.response!.data["errors"][0]["message"])),
);
},
);
// DeuxiÚme ligne exécutée
print('allo');
}

Il n'y a pas toujours une façon de faire qui est meilleure que l'autre. Tout dépend du contexte et de ce qu'on veut faire avec le résultat.

astuce

Notez que la syntaxe avec then existe aussi en Javascript.

Mot de la fin​

Un projet de référence de ce qui a été démontré dans cette recette est disponible ici.