CHAPITRE 3.4

Listes et défilement

Créez des listes performantes et des interfaces défilables pour afficher du contenu dynamique
Dans ce chapitre, nous allons découvrir comment créer des listes et gérer le défilement dans Flutter. Vous apprendrez à utiliser SingleChildScrollView pour des contenus courts, ListView pour des listes performantes, GridView pour des grilles, et ScrollController pour contrôler le défilement programmatiquement.

3.4Listes et défilement

3.4.1 – SingleChildScrollView

SingleChildScrollView est un widget qui rend son contenu défilable. Il est idéal pour des contenus courts ou de taille variable qui peuvent dépasser la taille de l'écran.

📖 Qu'est-ce que SingleChildScrollView ?

SingleChildScrollView permet de rendre n'importe quel widget défilable. Il accepte un seul widget enfant et crée une zone défilable autour de celui-ci.

Quand utiliser SingleChildScrollView :
Utilisez SingleChildScrollView pour des contenus courts ou de taille variable (formulaires, pages avec peu d'éléments, contenu dynamique). Pour de très longues listes, préférez ListView qui est plus performant.

📝 Syntaxe

Voici la syntaxe de base de SingleChildScrollView :

SingleChildScrollView(
  padding: EdgeInsets.all(16),
  child: Column(
    children: [
      // Votre contenu ici
    ],
  ),
)

Analysons cette syntaxe :

  • SingleChildScrollView( : Le widget qui rend le contenu défilable
  • padding: EdgeInsets.all(16) : Espacement autour du contenu (optionnel)
  • child: Column(...) : Le widget enfant à rendre défilable (généralement une Column ou Row)

🧪 Exemple : Column défilable

Voici un exemple simple avec une Column défilable :

Exemple SingleChildScrollView Flutter
SingleChildScrollView rend une Column entière défilable verticalement.
import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('SingleChildScrollView'),
        ),
        body: SafeArea(
          child: SingleChildScrollView(
            padding: const EdgeInsets.all(16),
            child: Column(
              children: [
                Container(
                  height: 400,
                  color: Colors.red,
                  child: const Center(
                    child: Text(
                      'Section 1',
                      style: TextStyle(color: Colors.white, fontSize: 24),
                    ),
                  ),
                ),
                const SizedBox(height: 20),
                Container(
                  height: 400,
                  color: Colors.blue,
                  child: const Center(
                    child: Text(
                      'Section 2',
                      style: TextStyle(color: Colors.white, fontSize: 24),
                    ),
                  ),
                ),
                const SizedBox(height: 20),
                Container(
                  height: 400,
                  color: Colors.green,
                  child: const Center(
                    child: Text(
                      'Section 3',
                      style: TextStyle(color: Colors.white, fontSize: 24),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Dans cet exemple, même si les trois sections ne tiennent pas à l'écran, l'utilisateur peut faire défiler pour voir tout le contenu grâce à SingleChildScrollView.

Paramètre padding :
Le paramètre padding de SingleChildScrollView ajoute de l'espace autour du contenu défilable. C'est pratique pour éviter que le contenu touche les bords de l'écran.

3.4.2 – ListView

ListView est un widget spécialement conçu pour afficher des listes d'éléments. Il est plus performant que SingleChildScrollView pour de longues listes car il ne construit que les éléments visibles à l'écran.

📋 Pourquoi ListView plutôt que SingleChildScrollView ?

ListView est optimisé pour les listes car il :

  • Construit uniquement les éléments visibles (lazy loading)
  • Recycle les widgets pour améliorer les performances
  • Gère automatiquement le défilement
  • Est plus performant pour de grandes listes
Lazy loading :
Le "lazy loading" signifie que ListView ne construit que les éléments qui sont actuellement visibles à l'écran. Quand vous faites défiler, il construit les nouveaux éléments et peut recycler les anciens. C'est beaucoup plus efficace que de construire tous les éléments en même temps.

📝 Syntaxe

Voici les syntaxes de base de ListView :

// ListView simple avec une liste d'enfants
ListView(
  padding: EdgeInsets.all(16),
  children: [
    Widget1(),
    Widget2(),
    Widget3(),
  ],
)

// ListView.builder pour des listes dynamiques
ListView.builder(
  itemCount: 50,
  itemBuilder: (context, index) {
    return Widget();
  },
)

Analysons ces syntaxes :

  • ListView(children: [...]) : Pour une liste avec un nombre fixe d'éléments
  • ListView.builder(itemCount: 50, itemBuilder: ...) : Pour une liste dynamique avec beaucoup d'éléments
  • itemCount : Nombre total d'éléments dans la liste
  • itemBuilder : Fonction qui construit chaque élément (reçoit context et index)

🧪 Exemple : ListView simple

Voici un exemple simple avec une ListView :

Exemple ListView Flutter
L'utilisation de ListView pour afficher une liste d'utilisateurs.
import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('ListView'),
        ),
        body: SafeArea(
          child: ListView(
            padding: const EdgeInsets.all(16),
            children: [
              ListTile(
                leading: const Icon(Icons.person),
                title: const Text('Utilisateur 1'),
                subtitle: const Text('Description de l\'utilisateur'),
                trailing: const Icon(Icons.arrow_forward_ios),
              ),
              const Divider(),
              ListTile(
                leading: const Icon(Icons.person),
                title: const Text('Utilisateur 2'),
                subtitle: const Text('Description de l\'utilisateur'),
                trailing: const Icon(Icons.arrow_forward_ios),
              ),
              const Divider(),
              ListTile(
                leading: const Icon(Icons.person),
                title: const Text('Utilisateur 3'),
                subtitle: const Text('Description de l\'utilisateur'),
                trailing: const Icon(Icons.arrow_forward_ios),
              ),
              const Divider(),
              ListTile(
                leading: const Icon(Icons.person),
                title: const Text('Utilisateur 4'),
                subtitle: const Text('Description de l\'utilisateur'),
                trailing: const Icon(Icons.arrow_forward_ios),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Dans cet exemple, une liste d'utilisateurs est affichée avec ListView. Chaque élément utilise ListTile, un widget Material Design spécialement conçu pour les listes.

ListTile :
ListTile est un widget qui affiche une ligne dans une liste avec :
  • leading : Widget à gauche (icône, avatar, etc.)
  • title : Titre principal
  • subtitle : Sous-titre (optionnel)
  • trailing : Widget à droite (icône, bouton, etc.)
Exemple ListTile Flutter
ListTile affichant une icône, un titre, un sous-titre et une icône de navigation à droite.

🧪 Exemple : ListView avec builder (liste dynamique)

Pour créer une liste avec beaucoup d'éléments, utilisez ListView.builder :

Exemple ListView.builder Flutter
L'utilisation de ListView.builder pour générer dynamiquement les éléments de la liste seulement au moment où ils apparaissent à l'écran.
import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('ListView.builder'),
        ),
        body: SafeArea(
          child: ListView.builder(
            padding: const EdgeInsets.all(16),
            itemCount: 50,
            itemBuilder: (context, index) {
              return Card(
                margin: const EdgeInsets.only(bottom: 12),
                child: ListTile(
                  leading: CircleAvatar(
                    backgroundColor: Colors.blue,
                    child: Text('${index + 1}',
                    style: TextStyle(color : Colors.white, fontSize : 16),)
                  ),
                  title: Text('Élément ${index + 1}'),
                  subtitle: Text('Description de l\'élément ${index + 1}'),
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

Dans cet exemple, ListView.builder crée 50 éléments. Seuls les éléments visibles sont construits, ce qui rend la liste très performante même avec beaucoup d'éléments.

ListView.builder :
ListView.builder est idéal pour des listes avec beaucoup d'éléments car :
  • itemCount : Nombre total d'éléments
  • itemBuilder : Fonction qui construit chaque élément à la demande
  • Seuls les éléments visibles sont construits

3.4.3 – GridView

GridView est un widget qui affiche ses enfants dans une grille (grid). C'est parfait pour afficher des images, des cartes, ou tout contenu organisé en colonnes et lignes.

📐 Qu'est-ce qu'une GridView ?

GridView organise ses enfants en colonnes et lignes, créant une grille. Chaque élément occupe une cellule de la grille.

Quand utiliser GridView :
Utilisez GridView pour :
  • Galerie d'images
  • Grille de produits
  • Tableau de bord avec icônes
  • Tout contenu organisé en grille

📝 Syntaxe

Voici les syntaxes de base de GridView :

// GridView.count avec une liste d'enfants
GridView.count(
  crossAxisCount: 2,
  padding: EdgeInsets.all(16),
  crossAxisSpacing: 16,
  mainAxisSpacing: 16,
  children: [
    Widget1(),
    Widget2(),
    Widget3(),
  ],
)

// GridView.builder pour des grilles dynamiques
GridView.builder(
  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    crossAxisSpacing: 16,
    mainAxisSpacing: 16,
  ),
  itemCount: 20,
  itemBuilder: (context, index) {
    return Widget();
  },
)

Analysons ces syntaxes :

  • GridView.count(children: [...]) : Pour une grille avec un nombre fixe d'éléments
  • GridView.builder(itemBuilder: ...) : Pour une grille dynamique avec beaucoup d'éléments
  • crossAxisCount : Nombre de colonnes dans la grille
  • crossAxisSpacing : Espacement horizontal entre les colonnes
  • mainAxisSpacing : Espacement vertical entre les lignes

🧪 Exemple : GridView simple

Voici un exemple simple avec une GridView :

Exemple GridView Flutter
L'utilisation de GridView.count pour créer une grille avec 2 colonnes.
import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('GridView'),
        ),
        body: SafeArea(
          child: GridView.count(
            crossAxisCount: 2,
            padding: const EdgeInsets.all(16),
            crossAxisSpacing: 16,
            mainAxisSpacing: 16,
            children: [
              Container(
                color: Colors.red,
                child: const Center(
                  child: Text(
                    '1',
                    style: TextStyle(color: Colors.white, fontSize: 32),
                  ),
                ),
              ),
              Container(
                color: Colors.blue,
                child: const Center(
                  child: Text(
                    '2',
                    style: TextStyle(color: Colors.white, fontSize: 32),
                  ),
                ),
              ),
              Container(
                color: Colors.green,
                child: const Center(
                  child: Text(
                    '3',
                    style: TextStyle(color: Colors.white, fontSize: 32),
                  ),
                ),
              ),
              Container(
                color: Colors.orange,
                child: const Center(
                  child: Text(
                    '4',
                    style: TextStyle(color: Colors.white, fontSize: 32),
                  ),
                ),
              ),
              Container(
                color: Colors.purple,
                child: const Center(
                  child: Text(
                    '5',
                    style: TextStyle(color: Colors.white, fontSize: 32),
                  ),
                ),
              ),
              Container(
                color: Colors.pink,
                child: const Center(
                  child: Text(
                    '6',
                    style: TextStyle(color: Colors.white, fontSize: 32),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Dans cet exemple, GridView.count crée une grille avec 2 colonnes (crossAxisCount: 2). Les éléments sont organisés en grille et peuvent être défilés verticalement.

Paramètres de GridView.count :
  • crossAxisCount : Nombre de colonnes dans la grille
  • crossAxisSpacing : Espacement horizontal entre les colonnes
  • mainAxisSpacing : Espacement vertical entre les lignes
  • padding : Espacement autour de la grille

🧪 Exemple : GridView avec builder

Pour créer une grille avec beaucoup d'éléments, utilisez GridView.builder :

Exemple GridView.builder Flutter
L'utilisation de GridView.builder pour générer dynamiquement les éléments de la grille seulement au moment où ils apparaissent à l'écran.
import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('GridView.builder'),
        ),
        body: SafeArea(
          child: GridView.builder(
            padding: const EdgeInsets.all(16),
            gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
              crossAxisCount: 2,
              crossAxisSpacing: 16,
              mainAxisSpacing: 16,
            ),
            itemCount: 20,
            itemBuilder: (context, index) {
              return Container(
                decoration: BoxDecoration(
                  color: Colors.blue[100],
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Center(
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Icon(
                        Icons.image,
                        size: 48,
                        color: Colors.blue[700],
                      ),
                      const SizedBox(height: 8),
                      Text(
                        'Image ${index + 1}',
                        style: TextStyle(
                          fontSize: 16,
                          fontWeight: FontWeight.bold,
                          color: Colors.blue[700],
                        ),
                      ),
                    ],
                  ),
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

Dans cet exemple, GridView.builder crée une grille de 20 éléments. Comme ListView.builder, seuls les éléments visibles sont construits pour optimiser les performances.

SliverGridDelegateWithFixedCrossAxisCount :
Ce delegate définit la structure de la grille :
  • crossAxisCount : Nombre de colonnes
  • crossAxisSpacing : Espacement horizontal
  • mainAxisSpacing : Espacement vertical

3.4.4 – ScrollController

ScrollController permet de contrôler le défilement d'un widget défilable (comme ListView, GridView, ou SingleChildScrollView) de manière programmatique.

🎮 Qu'est-ce qu'un ScrollController ?

ScrollController est un objet qui vous donne le contrôle sur le défilement. Vous pouvez :

  • Faire défiler vers une position spécifique
  • Animer le défilement
  • Écouter les événements de défilement
  • Obtenir la position actuelle du défilement
Quand utiliser ScrollController :
Utilisez ScrollController quand vous voulez :
  • Faire défiler automatiquement vers le haut ou le bas
  • Faire défiler vers un élément spécifique
  • Réagir aux changements de position de défilement
  • Ajouter un bouton "Retour en haut"

📝 Syntaxe

Voici la syntaxe de base pour utiliser ScrollController :

// 1. Créer le contrôleur dans un StatefulWidget
final ScrollController _scrollController = ScrollController();

// 2. Attacher le contrôleur au widget défilable
ListView(
  controller: _scrollController,
  children: [...],
)

// 3. Utiliser le contrôleur pour faire défiler
_scrollController.animateTo(
  0,  // Position (0 = début)
  duration: Duration(milliseconds: 500),
  curve: Curves.easeInOut,
);

// 4. Libérer le contrôleur dans dispose()
@override
void dispose() {
  _scrollController.dispose();
  super.dispose();
}

Analysons cette syntaxe :

  • ScrollController _scrollController : Crée un contrôleur de défilement
  • controller: _scrollController : Attache le contrôleur au widget défilable (ListView, GridView, etc.)
  • animateTo(0) : Fait défiler vers la position 0 (début) avec une animation
  • dispose() : Libère le contrôleur (important pour éviter les fuites mémoire)

🧪 Exemple : ScrollController avec bouton "Retour en haut"

Voici un exemple qui utilise ScrollController pour ajouter un bouton "Retour en haut" :

Exemple ScrollController Flutter
L'utilisation de ScrollController pour ajouter un bouton "Retour en haut" à la liste.
import 'package:flutter/material.dart';

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

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

  @override
  State<MaPage> createState() => _MaPageState();
}

class _MaPageState extends State<MaPage> {
  final ScrollController _scrollController = ScrollController();

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

  void _scrollToTop() {
    _scrollController.animateTo(
      0,
      duration: const Duration(milliseconds: 500),
      curve: Curves.easeInOut,
    );
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('ScrollController'),
        ),
        body: SafeArea(
          child: Stack(
            children: [
              ListView.builder(
                controller: _scrollController,
                padding: const EdgeInsets.all(16),
                itemCount: 50,
                itemBuilder: (context, index) {
                  return Card(
                    margin: const EdgeInsets.only(bottom: 12),
                    child: ListTile(
                      title: Text('Élément ${index + 1}'),
                      subtitle: Text('Description de l\'élément ${index + 1}'),
                    ),
                  );
                },
              ),
              Positioned(
                bottom: 20,
                right: 20,
                child: FloatingActionButton(
                  onPressed: _scrollToTop,
                  child: const Icon(Icons.arrow_upward),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Dans cet exemple, un ScrollController est attaché à la ListView. Quand l'utilisateur clique sur le bouton flottant, la liste défile automatiquement vers le haut avec une animation.

Explication du code :
  • ScrollController _scrollController : Crée un contrôleur de défilement
  • controller: _scrollController : Attache le contrôleur à la ListView
  • _scrollController.animateTo(0) : Fait défiler vers le début (position 0) avec une animation
  • dispose() : Libère le contrôleur quand le widget est détruit (important pour éviter les fuites mémoire)
Important :
N'oubliez jamais d'appeler dispose() sur votre ScrollController dans la méthode dispose() du widget. Cela libère les ressources et évite les fuites mémoire.

🧪 Exemple : ScrollController avec position

Voici un exemple qui affiche la position actuelle du défilement :

Exemple ScrollController Flutter
L'utilisation de ScrollController pour afficher la position actuelle du défilement en temps réel.
import 'package:flutter/material.dart';

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

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

  @override
  State<MaPage> createState() => _MaPageState();
}

class _MaPageState extends State<MaPage> {
  final ScrollController _scrollController = ScrollController();
  double _scrollPosition = 0;

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      setState(() {
        _scrollPosition = _scrollController.offset;
      });
    });
  }

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Position de défilement'),
        ),
        body: SafeArea(
          child: Column(
            children: [
              Container(
                padding: const EdgeInsets.all(16),
                color: Colors.blue[100],
                child: Text(
                  'Position de défilement: ${_scrollPosition.toStringAsFixed(0)} pixels',
                  style: const TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
              Expanded(
                child: ListView.builder(
                  controller: _scrollController,
                  padding: const EdgeInsets.all(16),
                  itemCount: 50,
                  itemBuilder: (context, index) {
                    return Card(
                      margin: const EdgeInsets.only(bottom: 12),
                      child: ListTile(
                        title: Text('Élément ${index + 1}'),
                        subtitle: Text('Description de l\'élément ${index + 1}'),
                      ),
                    );
                  },
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Dans cet exemple, la position de défilement est affichée en temps réel. Quand vous faites défiler la liste, le nombre de pixels défilés s'affiche en haut.

addListener :
_scrollController.addListener() ajoute un écouteur qui est appelé chaque fois que la position de défilement change. Dans cet exemple, nous mettons à jour l'état pour afficher la nouvelle position.
toStringAsFixed(0) :
toStringAsFixed(0) convertit le nombre en chaîne de caractères avec 0 décimales. Par exemple, 123.456 devient "123".
Exercice pratique :
Maintenant que vous connaissez les widgets de liste et de défilement (SingleChildScrollView, ListView, GridView, ScrollController), mettez vos connaissances en pratique avec l'exercice ci-dessous !

🎯 Exercice pratique

Objectif : Reproduire l'interface de liste de contacts ci-dessous en utilisant tous les concepts appris dans ce chapitre : ListView pour afficher la liste de contacts, StatefulWidget avec setState() pour gérer la sélection d'un contact, et les widgets ListTile pour chaque élément de la liste.

Interface de liste de contacts à reproduire
Interface à reproduire : Créez cette interface de liste de contacts en utilisant les widgets et techniques appris dans ce chapitre.

📝 Instructions :

  1. Identifiez les défis : Observez l'interface et notez les défis techniques que vous identifiez (par exemple : "Comment afficher une liste de contacts ?", "Comment gérer la sélection d'un contact ?", "Comment afficher le contact sélectionné en haut ?", etc.)
  2. Notez vos solutions : Avant de regarder le code, essayez de noter comment vous résoudriez chaque défi avec les widgets appris (ListView, ListTile, StatefulWidget, setState(), etc.)
  3. Comparez avec les solutions : Cliquez sur "Afficher le code" ci-dessous pour voir les solutions proposées et comparer avec vos notes. Analysez comment chaque défi a été résolu dans le code.