↑
CHAPITRE 4.3

Passage de données entre pages

Transmettez et récupérez des données lors de la navigation
Dans la plupart des applications, vous devez passer des données d'un écran à un autre. Par exemple, afficher les détails d'un produit sélectionné, ou récupérer un résultat après avoir rempli un formulaire. Dans ce chapitre, nous allons découvrir différentes méthodes pour transmettre des données entre les écrans.

4.3Passage de données entre pages

4.3.1 – Passage de paramètres lors de la navigation

La méthode la plus simple pour passer des données à un écran est de les transmettre directement au constructeur du widget lors de la navigation.

📤 Passer des données

Quand vous créez un nouvel écran avec Navigator.push(), vous pouvez passer des données en les transmettant au constructeur du widget.

Comment ça marche :
Vous créez le widget avec des paramètres dans son constructeur, puis vous passez ces paramètres lors de la navigation. C'est comme passer des arguments à une fonction.

📝 Syntaxe

Voici comment passer des données lors de la navigation :

// Navigation avec données
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => EcranDetails(
      nom: 'Produit A',
      prix: 29.99,
    ),
  ),
);

// Widget qui reçoit les données
class EcranDetails extends StatelessWidget {
  final String nom;
  final double prix;

  const EcranDetails({
    super.key,
    required this.nom,
    required this.prix,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(...);
  }
}

🧪 Exemple : Passage de données simple

Voici un exemple complet où on passe le nom d'un produit à l'écran de détails :

Exemple Passage de données simple Flutter
L'utilisation de Navigator.push pour passer des données à l'écran de détails.
import 'package:flutter/material.dart';

void main() {
  runApp(const MonApp());
}

class MonApp extends StatelessWidget {
  const MonApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: const EcranListe(),
    );
  }
}

// Écran avec une liste de produits
class EcranListe extends StatelessWidget {
  const EcranListe({super.key});

  @override
  Widget build(BuildContext context) {
    final produits = [
      {'nom': 'Produit A', 'prix': 29.99},
      {'nom': 'Produit B', 'prix': 39.99},
      {'nom': 'Produit C', 'prix': 49.99},
    ];

    return Scaffold(
      appBar: AppBar(
        title: const Text('Liste des produits'),
      ),
      body: ListView.builder(
        itemCount: produits.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(produits[index]['nom'] as String),
            subtitle: Text('${produits[index]['prix']} €'),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => EcranDetails(
                    nom: produits[index]['nom'] as String,
                    prix: produits[index]['prix'] as double,
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

// Écran de détails qui reçoit les données
class EcranDetails extends StatelessWidget {
  final String nom;
  final double prix;

  const EcranDetails({
    super.key,
    required this.nom,
    required this.prix,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(nom),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Nom : $nom',
              style: const TextStyle(fontSize: 18),
            ),
            const SizedBox(height: 16),
            Text(
              'Prix : $prix €',
              style: const TextStyle(fontSize: 18),
            ),
          ],
        ),
      ),
    );
  }
}

Dans cet exemple :

  • L'Ă©cran liste affiche une liste de produits
  • Quand vous cliquez sur un produit, Navigator.push() est appelĂ© avec les donnĂ©es du produit
  • L'Ă©cran de dĂ©tails reçoit nom et prix dans son constructeur
  • Les donnĂ©es sont affichĂ©es dans l'Ă©cran de dĂ©tails
required :
Le mot-clé required indique que le paramètre est obligatoire. Si vous oubliez de le passer, Dart vous avertira d'une erreur.
Astuce :
Vous pouvez passer plusieurs paramètres de différents types (String, int, double, bool, objets personnalisés, etc.) au constructeur du widget.

4.3.2 – Retour de données vers l'écran précédent

Parfois, vous voulez que l'écran suivant retourne une valeur à l'écran précédent. Par exemple, après avoir rempli un formulaire, vous voulez retourner le résultat à l'écran qui a ouvert le formulaire.

🔄 Retourner des données

Pour retourner des données, vous utilisez Navigator.pop(context, valeur) au lieu de Navigator.pop(context). L'écran précédent peut alors récupérer cette valeur en utilisant await avec Navigator.push().

📝 Syntaxe

Voici comment retourner des données :

// Dans l'écran qui ouvre le formulaire
final resultat = await Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => EcranFormulaire(),
  ),
);

// Dans l'écran qui retourne les données
Navigator.pop(context, 'Valeur retournée');

🧪 Exemple : Formulaire qui retourne un résultat

Voici un exemple où un formulaire retourne le texte saisi à l'écran précédent :

Exemple Retourner des données Flutter
L'utilisation de Navigator.push et Navigator.pop pour retourner des données à l'écran précédent.
import 'package:flutter/material.dart';

void main() {
  runApp(const MonApp());
}

class MonApp extends StatelessWidget {
  const MonApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: const EcranAccueil(),
    );
  }
}

// Écran d'accueil
class EcranAccueil extends StatefulWidget {
  const EcranAccueil({super.key});

  @override
  State<EcranAccueil> createState() => _EcranAccueilState();
}

class _EcranAccueilState extends State<EcranAccueil> {
  String? texteRecu;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Accueil'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () async {
                final resultat = await Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => const EcranSaisie(),
                  ),
                );

                if (resultat != null) {
                  setState(() {
                    texteRecu = resultat as String;
                  });
                }
              },
              child: const Text('Ouvrir le formulaire'),
            ),
            if (texteRecu != null) ...[
              const SizedBox(height: 20),
              Text(
                'Texte reçu : $texteRecu',
                style: const TextStyle(fontSize: 18),
              ),
            ],
          ],
        ),
      ),
    );
  }
}

// Écran de saisie qui retourne le texte
class EcranSaisie extends StatefulWidget {
  const EcranSaisie({super.key});

  @override
  State<EcranSaisie> createState() => _EcranSaisieState();
}

class _EcranSaisieState extends State<EcranSaisie> {
  final _controller = TextEditingController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Saisie'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
              controller: _controller,
              decoration: const InputDecoration(
                labelText: 'Saisissez quelque chose',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                Navigator.pop(context, _controller.text);
              },
              child: const Text('Valider et retourner'),
            ),
          ],
        ),
      ),
    );
  }
}

Dans cet exemple :

  • L'Ă©cran d'accueil ouvre l'Ă©cran de saisie avec await Navigator.push()
  • L'Ă©cran de saisie retourne le texte saisi avec Navigator.pop(context, _controller.text)
  • L'Ă©cran d'accueil reçoit le rĂ©sultat dans la variable resultat
  • Le texte reçu est affichĂ© sur l'Ă©cran d'accueil
await :
await attend que Navigator.push() se termine (c'est-à-dire que l'écran soit fermé) avant de continuer. La valeur retournée par Navigator.pop() est assignée à la variable resultat.
Important :
Si l'utilisateur ferme l'écran sans appeler Navigator.pop() avec une valeur (par exemple avec le bouton retour), resultat sera null. C'est pourquoi on vérifie if (resultat != null) avant d'utiliser la valeur.
as String :
Navigator.pop() retourne un Object? (peut être n'importe quel type). On utilise as String pour indiquer à Dart que cette valeur est une chaîne de caractères.

4.3.3 – Gestion des types et null safety

Dart utilise le null safety, ce qui signifie que vous devez gérer explicitement les valeurs qui peuvent être null. C'est particulièrement important lors du passage de données entre écrans.

🛡️ Null safety

En Dart, une variable peut être nullable (peut être null) ou non-nullable (ne peut jamais être null). Vous devez déclarer explicitement si une variable peut être null en ajoutant ? après le type.

📝 Syntaxe

Voici les différentes façons de gérer les types et le null safety :

// Variable non-nullable (obligatoire)
final String nom;

// Variable nullable (optionnelle)
final String? nomOptionnel;

// Retourner une valeur nullable
Navigator.pop(context, valeur); // peut ĂŞtre null

// Récupérer une valeur nullable
final resultat = await Navigator.push(...); // peut ĂŞtre null
if (resultat != null) {
  // Utiliser resultat en sécurité
}

🧪 Exemple : Gestion complète des types

Voici un exemple qui montre comment gérer différents types de données et le null safety :

Exemple Gestion des types Flutter
L'utilisation de Navigator.push et Navigator.pop pour retourner un Map contenant des données.
import 'package:flutter/material.dart';

void main() {
  runApp(const MonApp());
}

class MonApp extends StatelessWidget {
  const MonApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: const EcranAccueil(),
    );
  }
}

// Écran d'accueil
class EcranAccueil extends StatefulWidget {
  const EcranAccueil({super.key});

  @override
  State<EcranAccueil> createState() => _EcranAccueilState();
}

class _EcranAccueilState extends State<EcranAccueil> {
  String? messageRecu;
  int? nombreRecu;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Accueil'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () async {
                final resultat = await Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => const EcranFormulaire(),
                  ),
                );

                // Vérifier le type et la nullité
                if (resultat != null && resultat is Map) {
                  setState(() {
                    messageRecu = resultat['message'] as String?;
                    nombreRecu = resultat['nombre'] as int?;
                  });
                }
              },
              child: const Text('Ouvrir le formulaire'),
            ),
            if (messageRecu != null) ...[
              const SizedBox(height: 20),
              Text(
                'Message : $messageRecu',
                style: const TextStyle(fontSize: 18),
              ),
            ],
            if (nombreRecu != null) ...[
              const SizedBox(height: 10),
              Text(
                'Nombre : $nombreRecu',
                style: const TextStyle(fontSize: 18),
              ),
            ],
          ],
        ),
      ),
    );
  }
}

// Écran de formulaire
class EcranFormulaire extends StatefulWidget {
  const EcranFormulaire({super.key});

  @override
  State<EcranFormulaire> createState() => _EcranFormulaireState();
}

class _EcranFormulaireState extends State<EcranFormulaire> {
  final _messageController = TextEditingController();
  final _nombreController = TextEditingController();

  @override
  void dispose() {
    _messageController.dispose();
    _nombreController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Formulaire'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
              controller: _messageController,
              decoration: const InputDecoration(
                labelText: 'Message',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _nombreController,
              decoration: const InputDecoration(
                labelText: 'Nombre',
                border: OutlineInputBorder(),
              ),
              keyboardType: TextInputType.number,
            ),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                ElevatedButton(
                  onPressed: () {
                    // Retourner null (annulation)
                    Navigator.pop(context, null);
                  },
                  child: const Text('Annuler'),
                ),
                ElevatedButton(
                  onPressed: () {
                    // Retourner un Map avec les données
                    final nombre = int.tryParse(_nombreController.text);
                    Navigator.pop(context, {
                      'message': _messageController.text.isEmpty 
                          ? null 
                          : _messageController.text,
                      'nombre': nombre,
                    });
                  },
                  child: const Text('Valider'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Dans cet exemple :

  • Les variables messageRecu et nombreRecu sont dĂ©clarĂ©es comme nullable avec String? et int?
  • Le formulaire retourne un Map contenant les donnĂ©es, ou null si l'utilisateur annule
  • On vĂ©rifie que resultat n'est pas null et qu'il est de type Map avant de l'utiliser
  • On utilise int.tryParse() pour convertir le texte en nombre de manière sĂ©curisĂ©e (retourne null si la conversion Ă©choue)
is :
L'opérateur is vérifie le type d'une variable. resultat is Map vérifie si resultat est de type Map.
int.tryParse() :
int.tryParse() essaie de convertir une chaîne en nombre entier. Si la conversion réussit, elle retourne le nombre. Si elle échoue, elle retourne null au lieu de lever une erreur.
Bonnes pratiques :
  • Toujours vĂ©rifier null avant d'utiliser une valeur nullable
  • Utiliser is pour vĂ©rifier le type avant de faire un cast
  • Utiliser tryParse() pour les conversions qui peuvent Ă©chouer
  • DĂ©clarer les paramètres comme required s'ils sont obligatoires