Aller au contenu principal

Listes et ScrollViews

ListView : Affichage de listes scrollables

ListView basique

Le ListView affiche une liste scrollable de widgets. Idéal pour un petit nombre d'éléments.

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('ListView basique')),
body: ListView(
children: const [
ListTile(
leading: Icon(Icons.person),
title: Text('Alice'),
subtitle: Text('alice@example.com'),
),
ListTile(
leading: Icon(Icons.person),
title: Text('Bob'),
subtitle: Text('bob@example.com'),
),
ListTile(
leading: Icon(Icons.person),
title: Text('Charlie'),
subtitle: Text('charlie@example.com'),
),
],
),
),
);
}
}

ListView.builder : Construction dynamique

ListView.builder est plus performant pour les listes dynamiques car il ne construit que les éléments visibles.

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('ListView.builder')),
body: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text('Élément $index'),
subtitle: Text('Description de l\'élément $index'),
trailing: const Icon(Icons.arrow_forward),
onTap: () {
debugPrint('Élément $index cliqué');
},
);
},
),
),
);
}
}

ListView.separated : Avec séparateurs

Ajoute automatiquement des séparateurs entre les éléments.

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('ListView.separated')),
body: ListView.separated(
itemCount: 20,
separatorBuilder: (context, index) => const Divider(),
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
),
),
);
}
}

ListView avec données complexes

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class Person {
final String name;
final String email;
final String avatar;

const Person(this.name, this.email, this.avatar);
}

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


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Liste avec données')),
body: PersonList(),
),
);
}
}

class PersonList extends StatelessWidget {
PersonList({super.key});

final List<Person> people = const [
Person('Alice', 'alice@example.com', 'A'),
Person('Bob', 'bob@example.com', 'B'),
Person('Charlie', 'charlie@example.com', 'C'),
];


Widget build(BuildContext context) {
return ListView.builder(
itemCount: people.length,
itemBuilder: (context, index) {
final person = people[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
leading: Text(person.avatar, style: const TextStyle(fontSize: 32)),
title: Text(person.name),
subtitle: Text(person.email),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
// Logique de suppression
},
),
),
);
},
);
}
}

GridView : Affichage en grille

GridView.count : Grille avec nombre de colonnes fixe

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('GridView.count')),
body: GridView.count(
crossAxisCount: 3,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
padding: const EdgeInsets.all(10),
children: List.generate(20, (index) {
return Container(
color: Colors.primaries[index % Colors.primaries.length],
child: Center(
child: Text(
'Item $index',
style: const TextStyle(color: Colors.white),
),
),
);
}),
),
),
);
}
}

GridView.builder : Grille dynamique

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('GridView.builder')),
body: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.0,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
padding: const EdgeInsets.all(10),
itemCount: 50,
itemBuilder: (context, index) {
return Card(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.image, size: 50, color: Colors.blue),
const SizedBox(height: 8),
Text('Image $index'),
],
),
);
},
),
),
);
}
}

GridView.extent : Taille maximale des éléments

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('GridView.extent')),
body: GridView.extent(
maxCrossAxisExtent: 150,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
padding: const EdgeInsets.all(10),
children: List.generate(20, (index) {
return Container(
color: Colors.teal,
child: Center(child: Text('$index')),
);
}),
),
),
);
}
}

Widgets de liste avancés

Dismissible : Glisser pour supprimer

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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


Widget build(BuildContext context) {
return const MaterialApp(
home: DismissibleExample(),
);
}
}

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


State<DismissibleExample> createState() => _DismissibleExampleState();
}

class _DismissibleExampleState extends State<DismissibleExample> {
final List<String> items = List.generate(20, (index) => 'Item ${index + 1}');


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Dismissible')),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return Dismissible(
key: Key(item),
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
setState(() {
items.removeAt(index);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$item supprimé')),
);
},
child: ListTile(
title: Text(item),
),
);
},
),
);
}
}

ReorderableListView : Réorganiser les éléments

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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


Widget build(BuildContext context) {
return const MaterialApp(
home: ReorderableExample(),
);
}
}

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


State<ReorderableExample> createState() => _ReorderableExampleState();
}

class _ReorderableExampleState extends State<ReorderableExample> {
final List<String> items = List.generate(10, (index) => 'Item ${index + 1}');


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('ReorderableListView')),
body: ReorderableListView(
onReorder: (oldIndex, newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final item = items.removeAt(oldIndex);
items.insert(newIndex, item);
});
},
children: items.map((item) {
return ListTile(
key: Key(item),
title: Text(item),
leading: const Icon(Icons.drag_handle),
);
}).toList(),
),
);
}
}

SingleChildScrollView : Scroll simple

Pour du contenu scrollable qui n'est pas une liste.

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('SingleChildScrollView')),
body: SingleChildScrollView(
child: Column(
children: const [
SizedBox(height: 200, child: ColoredBox(color: Colors.red)),
SizedBox(height: 200, child: ColoredBox(color: Colors.blue)),
SizedBox(height: 200, child: ColoredBox(color: Colors.green)),
SizedBox(height: 200, child: ColoredBox(color: Colors.yellow)),
],
),
),
),
);
}
}

Scroll horizontal

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Scroll horizontal')),
body: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(10, (index) {
return Container(
width: 150,
height: 150,
margin: const EdgeInsets.all(8),
color: Colors.primaries[index % Colors.primaries.length],
child: Center(child: Text('$index')),
);
}),
),
),
),
);
}
}

ListTile et Card

ListTile personnalisé

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('ListTile personnalisé')),
body: ListView(
children: [
ListTile(
leading: const CircleAvatar(
backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
),
title: const Text('John Doe'),
subtitle: const Text('En ligne il y a 5 minutes'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.phone),
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.message),
onPressed: () {},
),
],
),
onTap: () {
debugPrint('ListTile cliqué');
},
),
],
),
),
);
}
}

Card avec contenu complexe

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Card complexe')),
body: ListView(
children: [
Card(
elevation: 4,
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const CircleAvatar(
child: Icon(Icons.person),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'Titre de la carte',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text('Sous-titre'),
],
),
],
),
const SizedBox(height: 16),
const Text(
'Contenu de la carte avec plus de détails...',
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {},
child: const Text('ANNULER'),
),
TextButton(
onPressed: () {},
child: const Text('OK'),
),
],
),
],
),
),
),
],
),
),
);
}
}

ExpansionTile : Liste extensible

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('ExpansionTile')),
body: ListView(
children: const [
ExpansionTile(
leading: Icon(Icons.folder),
title: Text('Dossier'),
subtitle: Text('3 éléments'),
children: [
ListTile(
leading: Icon(Icons.file_present),
title: Text('Document 1'),
),
ListTile(
leading: Icon(Icons.file_present),
title: Text('Document 2'),
),
ListTile(
leading: Icon(Icons.file_present),
title: Text('Document 3'),
),
],
),
],
),
),
);
}
}

Optimisation des performances

Utiliser const

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Optimisation: const')),
body: ListView.builder(
itemCount: 30,
itemBuilder: (context, index) {
return const ListTile(
leading: Icon(Icons.star), // const pour optimisation
title: Text('Item'),
);
},
),
),
);
}
}

Limiter la hauteur des items

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

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


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Optimisation: itemExtent')),
body: ListView.builder(
itemCount: 50,
itemExtent: 80, // Hauteur fixe pour meilleures performances
itemBuilder: (context, index) {
return ListTile(title: Text('Item $index'));
},
),
),
);
}
}

Bonnes pratiques

  1. ListView.builder pour listes dynamiques (+ de 10 éléments)
  2. ListView simple pour petites listes statiques
  3. Dismissible pour actions de suppression intuitives
  4. Key unique pour chaque élément dans listes dynamiques
  5. const autant que possible pour optimiser les performances
  6. itemExtent ou prototypeItem pour hauteurs fixes