Row, Column et Container. Dans ce chapitre, nous allons découvrir des techniques avancées de positionnement : superposer des widgets avec Stack, positionner précisément avec Positioned, aligner avec Align, créer des layouts adaptatifs avec Wrap, et rendre votre interface responsive avec LayoutBuilder.
3.3Positionnement et mise en page avancée
3.3.1 – Superposer des widgets avec Stack
Stack est un widget qui permet de superposer plusieurs widgets les uns sur les autres, comme des couches. C'est très utile pour créer des effets visuels comme du texte sur une image, des badges, ou des overlays.
🎯 Pourquoi utiliser Stack ?
Stack est utile quand vous voulez :
- Superposer des widgets : Placer un widget au-dessus d'un autre
- Créer des overlays : Ajouter des éléments par-dessus un contenu
- Combiner visuellement : Mélanger différents types de widgets (texte, images, boutons)
Stack fonctionne comme une pile de feuilles : chaque widget est une feuille, et les widgets sont empilés les uns sur les autres. Le dernier widget dans la liste apparaît au-dessus des autres.
📚 Empilement de widgets (z-index)
Dans un Stack, les widgets sont empilés dans l'ordre où ils apparaissent dans la liste children. Le premier widget est en bas, le dernier est en haut.
Stack(
children: [
Container(color: Colors.red), // En bas
Container(color: Colors.blue), // Au milieu
Container(color: Colors.green), // En haut
],
)
Dans
Stack, l'ordre dans la liste children détermine quel widget est au-dessus. Le dernier élément de la liste est toujours visible en premier (au-dessus des autres).
📝 Le paramètre children
Comme Row et Column, Stack utilise le paramètre children pour définir les widgets à superposer :
Stack(
children: [
// Widgets à superposer
],
)
🧪 Exemple : texte sur image
Voici un exemple classique : afficher du texte par-dessus une image :
Stack.
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('Stack - Texte sur image'),
),
body: SafeArea(
child: Center(
child: Stack(
children: [
Image.network(
'http://mazoul.online/images/courses/flutter/chapitre_3/owl.jpg',
width: 300,
height: 300,
fit: BoxFit.cover,
),
Positioned(
bottom: 20,
left: 20,
child: Container(
padding: const EdgeInsets.all(8),
color: Colors.black54,
child: const Text(
'Texte sur image',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
),
),
);
}
}
Dans cet exemple, le texte "Texte sur image" est affiché par-dessus l'image grâce à Stack. Le Positioned permet de placer le texte en bas à gauche (nous verrons Positioned dans la prochaine section).
Colors.black54 est un noir avec 54% d'opacité. Cela crée un fond semi-transparent qui permet de mieux voir le texte blanc par-dessus l'image.
3.3.2 – Positionner précisément avec Positioned
Positioned est un widget qui permet de positionner précisément un enfant dans un Stack. Il vous donne un contrôle total sur la position du widget.
📍 Utilisation dans Stack
Positioned ne peut être utilisé que comme enfant direct d'un Stack. Il permet de placer un widget à une position spécifique dans le Stack.
Positioned fonctionne uniquement à l'intérieur d'un Stack. Si vous essayez de l'utiliser ailleurs, vous obtiendrez une erreur.
📐 Paramètres (top, bottom, left, right)
Positioned accepte plusieurs paramètres pour définir la position :
top: Distance depuis le hautbottom: Distance depuis le basleft: Distance depuis la gaucheright: Distance depuis la droite
Positioned(
top: 10,
left: 20,
child: Text('Positionné'),
)
Vous pouvez combiner plusieurs paramètres. Par exemple,
top: 10, left: 20 place le widget à 10 pixels du haut et 20 pixels de la gauche. Si vous utilisez top et bottom ensemble, le widget aura une hauteur fixe.
🧪 Exemple : badge sur icône
Voici un exemple classique : un badge de notification sur une icône :
Positioned pour placer un badge sur une icône.
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('Positioned - Badge'),
),
body: SafeArea(
child: Center(
child: Stack(
children: [
const Icon(
Icons.notifications,
size: 64,
color: Colors.grey,
),
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: const Text(
'3',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
),
),
);
}
}
Dans cet exemple, un badge rouge avec le nombre "3" est positionné en haut à droite de l'icône de notification grâce à Positioned(top: 0, right: 0).
BoxShape.circle crée un cercle parfait. Pour que cela fonctionne, le Container doit avoir une largeur et une hauteur égales (ici, le padding crée la taille).
3.3.3 – Aligner un widget avec Align
Align est un widget qui permet de positionner précisément un enfant dans un conteneur. Il est plus flexible que Center car il permet de choisir n'importe quelle position.
🎯 Positionnement précis dans un conteneur
Align permet de placer un widget à une position spécifique dans son parent, comme le coin supérieur droit, le centre, ou n'importe où entre les deux.
Center place toujours le widget au centre. Align permet de choisir n'importe quelle position (centre, coin supérieur droit, coin inférieur gauche, etc.).
📐 Le paramètre alignment
Le paramètre alignment définit où placer le widget. Voici les valeurs courantes :
Alignment.topLeft: Coin supérieur gaucheAlignment.topCenter: Haut, centréAlignment.topRight: Coin supérieur droitAlignment.center: Centre (commeCenter)Alignment.bottomLeft: Coin inférieur gaucheAlignment.bottomCenter: Bas, centréAlignment.bottomRight: Coin inférieur droit
Align(
alignment: Alignment.topRight,
child: Text('En haut à droite'),
)
🔄 Différence avec Center
Center est en fait un raccourci pour Align(alignment: Alignment.center). Align est plus flexible car il permet d'autres positions.
- Utilisez
Centersi vous voulez simplement centrer un widget - Utilisez
Alignsi vous voulez placer un widget à une position spécifique (coin, bord, etc.)
🧪 Exemple pratique
Voici un exemple qui montre différentes positions avec Align :
Align.
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('Align'),
),
body: SafeArea(
child: Container(
width: double.infinity,
height: 400,
color: Colors.grey[200],
child: Align(
alignment: Alignment.bottomRight,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
child: const Center(
child: Text(
'Coin',
style: TextStyle(color: Colors.white),
),
),
),
),
),
),
),
);
}
}
Dans cet exemple, un container bleu est positionné dans le coin inférieur droit d'un container gris grâce à Align(alignment: Alignment.bottomRight).
Essayez de changer
Alignment.bottomRight par d'autres valeurs comme Alignment.topLeft, Alignment.center, ou Alignment.topRight pour voir les différentes positions.
3.3.4 – Disposition adaptative avec Wrap
Wrap est un widget qui organise ses enfants en lignes ou en colonnes, et passe automatiquement à la ligne suivante (ou colonne suivante) quand il n'y a plus de place. C'est très utile pour créer des listes de tags, chips, ou boutons qui s'adaptent à la largeur disponible.
🔄 Pourquoi Wrap plutôt que Row ?
Row place tous les enfants sur une seule ligne. Si les enfants sont trop larges, ils débordent. Wrap résout ce problème en passant automatiquement à la ligne suivante quand il n'y a plus de place.
Wrap évite les erreurs de débordement (RenderFlex overflowed) en passant automatiquement à la ligne suivante. C'est parfait pour des listes de tags, chips, ou boutons dont le nombre varie.
📝 Retour à la ligne automatique
Wrap place les enfants horizontalement (par défaut) et passe à la ligne suivante automatiquement quand il n'y a plus de place :
Wrap(
children: [
// Widgets qui passent à la ligne automatiquement
],
)
📏 Paramètres spacing et runSpacing
Wrap a deux paramètres importants pour l'espacement :
spacing: Espacement horizontal entre les enfants (8 pixels par défaut)runSpacing: Espacement vertical entre les lignes (0 par défaut)
Wrap(
spacing: 8,
runSpacing: 8,
children: [
// Widgets
],
)
🧪 Exemple : liste de tags/chips
Voici un exemple classique : une liste de tags qui s'adapte automatiquement :
Wrap pour disposer des tags automatiquement à la ligne.
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('Wrap - Tags'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
Chip(
label: const Text('Flutter'),
backgroundColor: Colors.blue[100],
),
Chip(
label: const Text('Dart'),
backgroundColor: Colors.blue[100],
),
Chip(
label: const Text('Mobile'),
backgroundColor: Colors.green[100],
),
Chip(
label: const Text('UI/UX'),
backgroundColor: Colors.purple[100],
),
Chip(
label: const Text('Design'),
backgroundColor: Colors.orange[100],
),
Chip(
label: const Text('Responsive'),
backgroundColor: Colors.red[100],
),
],
),
),
),
),
);
}
}
Dans cet exemple, les chips (tags) sont organisés avec Wrap. Si l'écran est trop petit, les chips passent automatiquement à la ligne suivante. Les paramètres spacing: 8 et runSpacing: 8 ajoutent de l'espace entre les chips.
Chip est un widget Material Design qui affiche une petite étiquette avec un fond arrondi. C'est parfait pour afficher des tags, des catégories, ou des filtres.
3.3.5 – Adapter la mise en page avec LayoutBuilder
LayoutBuilder est un widget qui vous donne accès aux contraintes de taille disponibles. Il permet de créer des interfaces qui s'adaptent à la taille de l'écran ou du conteneur parent.
📐 Réagir à la taille disponible
LayoutBuilder fournit un callback qui reçoit les contraintes de taille. Vous pouvez utiliser ces contraintes pour décider comment afficher votre contenu.
Les contraintes (
constraints) indiquent la taille minimale et maximale disponible pour votre widget. Par exemple, maxWidth: 400 signifie que vous avez au maximum 400 pixels de largeur.
🔧 Le paramètre constraints
Le callback de LayoutBuilder reçoit un objet BoxConstraints qui contient :
maxWidth: Largeur maximale disponiblemaxHeight: Hauteur maximale disponibleminWidth: Largeur minimale disponibleminHeight: Hauteur minimale disponible
LayoutBuilder(
builder: (context, constraints) {
// Utiliser constraints.maxWidth, constraints.maxHeight, etc.
return Widget();
},
)
📱 Interface responsive
LayoutBuilder est parfait pour créer des interfaces qui s'adaptent à la taille de l'écran. Par exemple, afficher une Column sur mobile et une Row sur tablette.
🧪 Exemple : affichage différent selon la largeur
Voici un exemple qui change la disposition selon la largeur disponible :
LayoutBuilder permet d'afficher une Row sur un grand écran, ou une Column sur un petit é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('LayoutBuilder'),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
// Si la largeur est supérieure à 600 pixels, afficher en Row
if (constraints.maxWidth > 600) {
return Row(
children: [
Expanded(
child: Container(
color: Colors.blue[100],
child: const Center(
child: Text(
'Zone 1',
style: TextStyle(fontSize: 24),
),
),
),
),
Expanded(
child: Container(
color: Colors.green[100],
child: const Center(
child: Text(
'Zone 2',
style: TextStyle(fontSize: 24),
),
),
),
),
],
);
} else {
// Sinon, afficher en Column
return Column(
children: [
Expanded(
child: Container(
color: Colors.blue[100],
child: const Center(
child: Text(
'Zone 1',
style: TextStyle(fontSize: 24),
),
),
),
),
Expanded(
child: Container(
color: Colors.green[100],
child: const Center(
child: Text(
'Zone 2',
style: TextStyle(fontSize: 24),
),
),
),
),
],
);
}
},
),
),
),
);
}
}
Dans cet exemple, si la largeur disponible est supérieure à 600 pixels, les zones sont affichées côte à côte (Row). Sinon, elles sont empilées verticalement (Column). Redimensionnez la fenêtre pour voir l'effet !
Cette technique permet de créer des interfaces qui s'adaptent automatiquement à la taille de l'écran. Sur un téléphone (petit écran), le contenu est empilé verticalement. Sur une tablette ou un ordinateur (grand écran), le contenu peut être affiché côte à côte.
Voici des seuils de largeur couramment utilisés pour le responsive design :
600: Tablette en mode portrait900: Tablette en mode paysage / petit ordinateur1200: Ordinateur de bureau
Maintenant que vous connaissez les techniques de positionnement avancé (Stack, Positioned, Align, Wrap, LayoutBuilder), mettez vos connaissances en pratique avec l'exercice ci-dessous !
🎯 Exercice pratique
Objectif : Reproduire l'interface de profil ci-dessous en utilisant tous les concepts appris dans ce chapitre : Stack pour superposer l'image de profil sur le fond dégradé, Positioned pour positionner précisément l'avatar, Wrap pour les tags, et les widgets StatefulWidget avec setState() pour gérer les interactions (like, follow).
📝 Instructions :
- Identifiez les défis : Observez l'interface et notez les défis techniques que vous identifiez (par exemple : "Comment superposer l'avatar sur le fond dégradé ?", "Comment positionner précisément l'avatar ?", "Comment faire passer les tags à la ligne automatiquement ?", etc.)
- Notez vos solutions : Avant de regarder le code, essayez de noter comment vous résoudriez chaque défi avec les widgets appris (Stack, Positioned, Wrap, 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.