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.
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éfilablepadding: 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 :
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.
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
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émentsListView.builder(itemCount: 50, itemBuilder: ...): Pour une liste dynamique avec beaucoup d'élémentsitemCount: Nombre total d'éléments dans la listeitemBuilder: Fonction qui construit chaque élément (reçoitcontextetindex)
🧪 Exemple : ListView simple
Voici un exemple simple avec une ListView :
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 est un widget qui affiche une ligne dans une liste avec :
leading: Widget à gauche (icône, avatar, etc.)title: Titre principalsubtitle: Sous-titre (optionnel)trailing: Widget à droite (icône, bouton, etc.)
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 :
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 est idéal pour des listes avec beaucoup d'éléments car :
itemCount: Nombre total d'élémentsitemBuilder: 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.
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émentsGridView.builder(itemBuilder: ...): Pour une grille dynamique avec beaucoup d'élémentscrossAxisCount: Nombre de colonnes dans la grillecrossAxisSpacing: Espacement horizontal entre les colonnesmainAxisSpacing: Espacement vertical entre les lignes
🧪 Exemple : GridView simple
Voici un exemple simple avec une GridView :
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.
crossAxisCount: Nombre de colonnes dans la grillecrossAxisSpacing: Espacement horizontal entre les colonnesmainAxisSpacing: Espacement vertical entre les lignespadding: Espacement autour de la grille
🧪 Exemple : GridView avec builder
Pour créer une grille avec beaucoup d'éléments, utilisez GridView.builder :
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.
Ce delegate définit la structure de la grille :
crossAxisCount: Nombre de colonnescrossAxisSpacing: Espacement horizontalmainAxisSpacing: 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
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éfilementcontroller: _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 animationdispose(): 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" :
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.
ScrollController _scrollController: Crée un contrôleur de défilementcontroller: _scrollController: Attache le contrôleur à la ListView_scrollController.animateTo(0): Fait défiler vers le début (position 0) avec une animationdispose(): Libère le contrôleur quand le widget est détruit (important pour éviter les fuites mémoire)
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 :
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.
_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) convertit le nombre en chaîne de caractères avec 0 décimales. Par exemple, 123.456 devient "123".
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.
📝 Instructions :
- 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.)
- 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.)
- 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.