Skip to content

Fondamentaux de Dart

Dart est un langage de programmation open-source développé par Google et présenté au public en 2011. Initialement imaginé comme un remplacement de JavaScript pour le développement web, cette ambition a été abandonnée, mais le langage n’a cessé d’évoluer et est maintenant utilisé en interne chez Google sur des projets critiques.

Dart est un langage qui devrait paraître familier aux développeurs connaissant Java, C#, JavaScript, Kotlin ou Swift.

Un programme Dart nécessite une fonction main, son point d’entrée unique.

void main() {
print('Hello, Dart!');
}

Déclaration d’une valeur variable :

var firstname = "Pierre";
firstname = "Jean-Pierre"; // OK
var total = 0.0;
total += 3.0;
total -= 2.5;

Dart propose plusieurs types intégrés :

  • String
  • num, int, double
  • bool
  • List
  • Map
  • Set
  • Record
  • Runes, Symbol
  • Null

Dart (version 2.12 et supérieure) est “null safe”. Par défaut, les variables typées ne peuvent être null.

Pour signaler au compilateur qu’une variable ou qu’un paramètre peut être null, il est nécessaire de l’expliciter grâce au ? :

String? name; // Peut être null
String title = "Hello"; // Ne peut pas être null

Les valeurs dynamic sont “nullables” par défaut.

var title = "Introduction à Dart";
print('${title.split(' ')}');

Il est possible d’expliciter le type des variables :

String name = 'Voyager II';
int maxLength = 9;
double price = 9.99;
List<String> planets = ['Jupiter', 'Saturn', 'Uranus', 'Neptune'];
Map<String, dynamic> imageJupiter = {
'tags': ['jupiter'],
'url': '//path/to/jupiter.jpg'
};

Les variables final ne peuvent être déclarées qu’une seule fois, mais leur contenu est mutable. Grâce à l’inférence de type, le typage explicite est optionnel.

final total = price1 + price2;
total = 30; // ⚠️ Erreur compilation
total += 10; // ⚠️ Erreur compilation

Les variables const sont initialisées pendant la compilation et sont entièrement immuables.

const defaultErrorMessage = 'Ooops...:|';
const ok = 200;
const notFound = 404;

Conditions :

if (age <= 2) {
return 0;
} else if (age > 2 && age < 13) {
return 0.5;
} else {
return 1;
}
// For-in loop
for (final user in group) {
print(user);
}
// For loop
for (int month = 1; month <= 12; month++) {
print(month);
}
// While loop
while (year < 2016) {
year += 1;
}

Switch classique :

switch(route) {
case '/login':
screen = LoginScreen();
break;
case '/':
screen = HomeScreen();
break;
case '/404':
default:
screen = NotFoundScreen();
}

Switch expression (Dart 3.0) :

final view = switch(result) {
Result.pending => ProgressView(),
Result.error => ErrorView(),
Result.data => UserList(),
};
final cityList = ['Paris', 'Londres', 'Rome'];
cityList.add('Amsterdam');
cityList.addAll(['Madrid', 'Barcelone']);
cityList.removeLast();
cityList.add('Rome');
final uppercaseCities = cityList
.map((city) => city.toUpperCase())
.toList();
print(uppercaseCities);

Collection d’éléments uniques :

final citySet = {'Paris', 'Londres', 'Rome'};
citySet.add('Rome'); // N'ajoute pas de doublon
print('Cities $citySet');
final city = {
'name': 'Paris',
'country': 'France',
'pictures': ['paris1.jpg', 'paris2.jpg'],
};
enum Status { pending, error, done }
Map<Status, Color> colorMap = {
Status.pending: Colors.grey,
Status.error: Colors.red,
Status.done: Colors.green,
};
void main() {
print(city['country']);
print(colorMap[Status.error]);
}

Depuis Dart 3.0 :

void main() {
// Records positionnels
final position = getPoint();
print('position X : ${position.$1}, Y : ${position.$2}, Z : ${position.$3}');
// Destructuring
final (x, y, z) = getPoint();
print('position X : $x, Y : $y, Z : $z');
// Named records
final coordinates = getPosition();
print('longitude ${coordinates.longitude} latitude ${coordinates.latitude}');
}
(double, double, double) getPoint() {
final x = computeX();
final y = computeY();
final z = computeZ();
return (x, y, z);
}
({double longitude, double latitude}) getPosition() {
final longitude = computeLongitude();
final latitude = computeLatitude();
return (latitude: latitude, longitude: longitude);
}

Les fonctions Dart doivent être typées, pour les paramètres et la valeur retournée. Les fonctions sans retour sont typées void.

void main() {
final values = [1, 2, 5, 6, 7];
final total = sum(values);
print('Somme : $total\nMoyenne ${average(values)}\n'
'Moyenne arrondie ${roundAverage(values)}');
}
int sum(List<int> values) {
final result = values.reduce((value, item) => value + item);
return result;
}
double average(List<int> values) => sum(values) / values.length;
int roundAverage(List<int> values) => sum(values) ~/ values.length;
double average(List<int> values) => sum(values) / values.length;
String hello([String? name]) {
return 'Hello ${name ?? 'Dart'}';
}
print(hello('Bob'));
String hello({String? name}) {
return 'Hello ${name ?? 'World'}';
}
print(hello(name: 'Bob'));

Les paramètres nommés apportent beaucoup de lisibilité, mais sont optionnels, sauf s’ils sont précédés d’un required.

String hello({required String name}) {
return 'Hello $name';
}
print(hello(name: 'Bob'));

Dart propose une POO relativement classique avec des classes pourvues de constructeurs. Il est possible de déclarer des constructeurs nommés, permettant de créer des “configurations” d’instanciation particulières.

void main() {
final counter = ValueNotifier(0);
counter.addListener(() => print('listener 1 : ${counter.value}'));
counter.addListener(() => print('listener 2 : ${counter.value}'));
counter.value = 1;
}
typedef Listener = void Function();
abstract class Notifier {
final _listeners = <Listener>{};
void addListener(Listener listener) {
_listeners.add(listener);
}
void removeListener(Listener listener) {
_listeners.remove(listener);
}
void removeAllListeners() {
_listeners.clear();
}
void notifyListeners() {
for (final listener in _listeners) {
listener.call();
}
}
}
class ValueNotifier<T> extends Notifier {
T _value;
T get value => _value;
set value(T newValue) {
_value = newValue;
notifyListeners();
}
ValueNotifier(T value) : _value = value;
}

En savoir plus

class Response {
final int statusCode;
final String body;
Response({required this.statusCode, required this.body});
Response.ok({required this.body}) : statusCode = 200;
Response.notFound({required this.body}) : statusCode = 404;
}

Un factory constructor peut renvoyer une instance existante :

class Singleton {
static final Singleton _singleton = Singleton._internal();
factory Singleton() {
return _singleton;
}
Singleton._internal();
}

Par défaut, toute class est implicitement une interface. Il est donc possible de l’implémenter ou de l’étendre.

class A {
void hello() {
print('hello');
}
}
final a = A();
a.hello();
class B extends A {}
final b = B();
b.hello();
class C implements A {
void hello() {
print('HELLO');
}
}
final c = C();
c.hello();
  • On ne peut pas les instancier
  • Toutes les méthodes n’ont pas besoin d’être implémentées
abstract class BaseService {
void describe();
void describeWithEmphasis() {
print('=========');
describe();
print('=========');
}
}

En savoir plus sur les classes abstraites

Les mixins permettent d’ajouter un même comportement à d’autres classes.

mixin Piloted {
int astronauts = 1;
void describeCrew() {
print('Number of astronauts: $astronauts');
}
}
class PilotedCraft extends Spacecraft with Piloted {
// ...
}

En savoir plus sur les mixins

Les enums de Dart ont longtemps été très basiques. Dart 2.15 a ajouté quelques utilitaires :

  • MyEnum.values.asNameMap()
  • MyEnum.option1.name
enum Orientation { landscape, portrait }
enum CardType { visa, mastercard }
void main() {
print(CardType.values);
print(CardType.values.asMap());
print(CardType.values.asNameMap());
print(CardType.visa);
print(CardType.visa.name);
print(CardType.visa.index);
print(CardType.values.byName('visa'));
}

Depuis Dart 2.17, les enums peuvent avoir des valeurs associées. Le constructeur doit être constant et les valeurs associées final.

enum Routes {
home('/'),
login('/login'),
details('/details');
final String path;
const Routes(this.path);
}
void main() {
final route = Routes.home;
switch(route) {
case Routes.login:
goToLogin(route.path);
case Routes.home:
goToHome(route.path);
case Routes.details:
goToDetails(route.path);
}
}

Les extensions de méthodes permettent d’ajouter des méthodes et propriétés à d’autres classes. À la différence des mixins, les extensions ont accès aux propriétés et méthodes de la classe qu’elles étendent.

extension SpaceCraftExtension on SpaceCraft {
String get description => '$name / $launchDate';
}

Permet de définir une catégorie de classes énumérables.

sealed class Message {
final String title;
Message(this.title);
}
class TextMessage extends Message {
TextMessage(super.title);
}
class VideoMessage extends Message {
VideoMessage(super.title);
}
Widget buildMessageView(Message message) {
return switch (message) {
TextMessage() => Text('Text: ${message.title}'),
VideoMessage() => VideoPlayer(message.title),
};
}

Les API asynchrones renvoient principalement des Future. Les Futures sont l’équivalent des Promise en JavaScript. Il s’agit d’une valeur indéterminée au moment de l’affectation, mais qui sera potentiellement résolue ultérieurement, soit avec une donnée, soit avec une erreur.

Deux syntaxes permettent de les utiliser :

  • .then()
  • async / await
void main() {
final data = _loadData();
print(data);
data.then(
(value) => print(value),
onError: (error) => print('ERROR : $error'),
);
}
Future<String> _loadData() {
//return Future.value('Hello');
return Future.delayed(const Duration(seconds: 1), () => 'Hello');
//return Future.error('_loadData Error');
}
void main() async {
final version = await loadVersion();
print(version);
}
Future<String> loadVersion() =>
Future.delayed(const Duration(seconds: 1), () => '1.0');

En savoir plus sur l’asynchronie avec Dart

if (astronauts == 0) {
throw StateError('No astronauts.');
}
void main(List<String> arguments) async {
try {
for (var path in arguments) {
var content = await File(path).readAsString();
print(content);
}
} on IOException catch (e, stacktrace) {
print('Could not read: $e\n$stacktrace');
} catch (e, stacktrace) {
print('Erreur : $e\n$stacktrace');
} finally {
print('Done !');
}
}

Les packages Dart peuvent être importés depuis différentes sources :

Depuis pub.dev :

dependencies:
basics: ^1.0.0

Depuis GitHub :

dependencies:
basics:
git: https://github.com/user/repo.git

Depuis un chemin local :

dependencies:
basics:
path: ../local/path

Depuis un repo privé :

dependencies:
basics:
hosted:
name: basics
url: https://my.host.com
version: ^1.0.0

Le gestionnaire de packages Dart est pub. Pour récupérer les dépendances :

  • Projet Dart : pub get
  • Projet Flutter : flutter packages get

Pour ajouter un package :

  • Projet Flutter : flutter packages add <package_name>
  • Projet Flutter : flutter packages upgrade
// Importing core libraries
import 'dart:math';
// Importing libraries from external packages
import 'package:test/test.dart';
// Importing files
import 'path/to/my_other_file.dart';

Cf. https://dart.dev/tools/pub/dependencies

Il est possible de personnaliser l’analyzer Dart via le fichier (facultatif) analysis_options.yaml.

Pedantic est un ensemble de bonnes pratiques renforcées via le linter intégré.

Consultez les règles de lint pour personnaliser votre configuration.

Projet pratique : Créer une application console Dart pour convertir JSON en CSV.

import 'dart:io'; // File
import 'dart:convert'; // jsonDecode
// dart bin/json_csv.dart /chemin/users.json
void main(List<String> arguments) async {
// 1. récupération du chemin
// 2. lecture du contenu du fichier cf File(path)
// 3. parsing json cf. jsonDecode
// 4. transformation données vers CSV
// 5. enregistrement fichier CSV
}

Le fichier de test :

[
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"age": 28
},
{
"id": 2,
"name": "Bob",
"email": "bob@example.com",
"age": 34
},
{
"id": 3,
"name": "Charlie",
"email": "charlie@example.com",
"age": 22
}
]