CHAPITRE 3.3

Positionnement et mise en page avancée

Maîtrisez le positionnement précis et les layouts adaptatifs pour créer des interfaces professionnelles
Jusqu'à présent, vous avez organisé vos widgets avec 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)
Analogie :
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
  ],
)
Ordre d'empilement :
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 :

Exemple Stack texte sur image Flutter
Le texte est superposé sur l'image grâce au widget 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 :
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.

Important :
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 haut
  • bottom : Distance depuis le bas
  • left : Distance depuis la gauche
  • right : Distance depuis la droite
Positioned(
  top: 10,
  left: 20,
  child: Text('Positionné'),
)
Combinaison de paramètres :
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 :

Exemple badge Positionned sur icône Flutter
L'utilisation de 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 :
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.

Différence avec Center :
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 gauche
  • Alignment.topCenter : Haut, centré
  • Alignment.topRight : Coin supérieur droit
  • Alignment.center : Centre (comme Center)
  • Alignment.bottomLeft : Coin inférieur gauche
  • Alignment.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.

Quand utiliser Align vs Center ?
  • Utilisez Center si vous voulez simplement centrer un widget
  • Utilisez Align si vous voulez placer un widget à une position spécifique (coin, bord, etc.)

🧪 Exemple pratique

Voici un exemple qui montre différentes positions avec Align :

Exemple Align Flutter
Container bleu positionné dans différents coins grâce au widget 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).

Expérimenter :
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.

Avantage de Wrap :
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 :

Exemple Wrap Flutter
L'utilisation de 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 :
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.

Contraintes :
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 disponible
  • maxHeight : Hauteur maximale disponible
  • minWidth : Largeur minimale disponible
  • minHeight : 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 :

Exemple LayoutBuilder responsive Flutter
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 !

Responsive design :
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.
Seuils courants :
Voici des seuils de largeur couramment utilisés pour le responsive design :
  • 600 : Tablette en mode portrait
  • 900 : Tablette en mode paysage / petit ordinateur
  • 1200 : Ordinateur de bureau
Exercice pratique :
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).

Interface de profil à reproduire
Interface à reproduire : Créez cette interface de profil 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 superposer l'avatar sur le fond dégradé ?", "Comment positionner précisément l'avatar ?", "Comment faire passer les tags à la ligne automatiquement ?", etc.)
  2. 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.)
  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.