MDC-102 : Structure et mise en page Material Design

logo_components_color_2x_web_96dp.png

Material Components (MDC) aide les développeurs à implémenter Material Design. Conçu par une équipe d'ingénieurs et de spécialistes de l'expérience utilisateur chez Google, MDC propose des dizaines de composants d'interface utilisateur élégants et fonctionnels. Il est disponible pour Android, iOS, le Web et Flutter.material.io/develop.

Dans l'atelier de programmation MDC-101, vous avez utilisé deux éléments Material Components pour créer une page de connexion : des champs de texte et des boutons comportant des animations "tâche d'encre". Sur ces bases, enrichissons notre application en y ajoutant navigation, structure et données.

Objectifs de l'atelier

Dans cet atelier de programmation, vous allez créer un écran d'accueil pour Shrine, une application d'e-commerce pour la vente de vêtements et d'articles pour la maison. Cet écran contiendra :

  • Une barre d'application supérieure
  • Une liste de produits sous forme de grille

Android

iOS

Composants MDC utilisés dans cet atelier

  • Barre d'application supérieure
  • Grilles
  • Fiches

Comment évalueriez-vous votre niveau d'expérience en développement avec Flutter ?

Débutant Intermédiaire Expert

Avant de commencer

Pour commencer à développer des applications mobiles avec Flutter, vous devez :

  1. télécharger et installer le SDK Flutter ;
  2. mettre à jour votre PATH avec le SDK Flutter ;
  3. installer Android Studio avec les plug-ins Flutter et Dart, ou votre éditeur préféré ;
  4. installer un émulateur Android, un simulateur iOS (nécessite un Mac avec Xcode) ou utiliser un appareil physique.

Pour plus d'informations sur l'installation de Flutter, consultez la section Get Started: Install (Premiers pas : Installation). Pour configurer un éditeur, consultez la section Get Started: Set up an editor Premiers pas : Configurer un éditeur. Lorsque vous installez un émulateur Android, n'hésitez pas à utiliser les options par défaut, comme un téléphone Pixel 3 doté de la dernière image système. Il est conseillé, mais pas obligatoire, d'activer l'accélération de la VM. Une fois les quatre étapes ci-dessus effectuées, vous pouvez revenir à l'atelier de programmation. Pour cet atelier de programmation, vous avez seulement à installer Flutter pour une plate-forme (Android ou iOS).

Vérifier que la version de votre SDK Flutter est correcte

Avant de poursuivre cet atelier de programmation, assurez-vous que la version de votre SDK est correcte. Si le SDK Flutter a déjà été installé, utilisez flutter upgrade pour vous assurer qu'il s'agit de la dernière version.

 flutter upgrade

L'exécution de flutter upgrade lance automatiquement flutter doctor. S'il s'agit d'une nouvelle installation de Flutter et qu'aucune mise à niveau n'est nécessaire, exécutez flutter doctor manuellement. Le système vous indique si vous devez installer des dépendances pour finaliser la configuration. Vous pouvez ignorer les options qui ne vous sont pas utiles (par exemple, Xcode si vous n'avez pas l'intention d'effectuer de développement pour iOS).

 flutter doctor

Questions fréquentes

Vous avez déjà suivi l'atelier MDC-101 ?

Si vous avez fini l'atelier de programmation MDC-101, votre code devrait être prêt pour commencer cet atelier. Passez à l'étape suivante : Ajouter une barre d'application supérieure.

Vous partez de zéro ?

Télécharger l'application de départ de l'atelier de programmation

Télécharger l'application de départ

L'application de départ se trouve dans le répertoire material-components-flutter-codelabs-102-starter_and_101-complete/mdc_100_series.

… ou cloner l'atelier depuis GitHub

Pour cloner cet atelier de programmation à partir de GitHub, exécutez les commandes suivantes :

git clone https://github.com/material-components/material-components-flutter-codelabs.git
cd material-components-flutter-codelabs/mdc_100_series
git checkout 102-starter_and_101-complete

Configurer votre projet

Les instructions suivantes supposent que vous utilisez Android Studio (IntelliJ).

Ouvrir le projet

1. Ouvrez Android Studio.

2. Si l'écran de bienvenue s'affiche, cliquez sur Open an existing Android Studio project (Ouvrir un projet Android Studio existant).

3. Accédez au répertoire material-components-flutter-codelabs/mdc_100_series et cliquez sur "Open" (Ouvrir). Le projet devrait s'ouvrir. Vous pouvez ignorer toutes les erreurs qui s'affichent dans Dart Analysis jusqu'à ce que vous ayez créé le projet.

4. Si vous y êtes invité :

  • Installez les mises à jour de la plate-forme et des plug-ins ou FlutterRunConfigurationType.
  • Si le SDK Dart ou Flutter n'est pas configuré, définissez le chemin d'accès au SDK Flutter pour le plug-in Flutter.
  • Configurez les frameworks Android.
  • Cliquez sur "Get dependencies" (Obtenir les dépendances) ou sur "Run 'flutter package gets'" (Exécuter "flutter packages get").

Ensuite, redémarrez Android Studio.

Exécuter l'application de départ

Les instructions suivantes supposent que vous effectuez les tests sur un émulateur ou un appareil Android. Vous pouvez également les effectuer sur un simulateur ou un appareil iOS si Xcode est installé.

1. Sélectionnez l'appareil ou l'émulateur. Si l'émulateur Android ne s'exécute pas déjà, sélectionnez Tools > Android > AVD Manager (Outils > Android > AVD Manager) pour créer un appareil virtuel et lancer l'émulateur. Si un AVD existe déjà, vous pouvez lancer l'émulateur directement depuis le sélecteur d'appareil dans Android Studio, comme indiqué à l'étape suivante. Pour le simulateur iOS, s'il ne s'exécute pas déjà, lancez-le sur votre ordinateur de développement en sélectionnant Flutter Device Selection > Open iOS Simulator (Sélection d'appareils Flutter > Ouvrir le simulateur iOS).

2. Démarrez votre application Flutter :

  • Recherchez le menu déroulant "Flutter Device Selection" (Sélection des appareils Flutter) en haut de l'écran de votre éditeur, puis sélectionnez l'appareil (par exemple, iPhone SE ou Android SDK créé pour <version>).
  • Appuyez sur l'icône de lecture ().

Parfait ! La page de connexion de Shrine créée dans l'atelier de programmation MDC-101 devrait s'afficher dans le simulateur ou l'émulateur.

Android

iOS

Maintenant que l'écran de connexion est en place, ajoutons quelques produits dans l'application.

Pour le moment, si vous cliquez sur le bouton "Next", l'écran d'accueil s'affiche avec le message "You did it!" (Vous avez réussi.). Parfait. À présent, notre utilisateur ne peut effectuer aucune action et n'a aucun moyen de savoir où il se trouve dans l'application. Pour résoudre ce problème, il est temps d'ajouter la navigation.

Material Design propose des formats de navigation qui garantissent une grande facilité d'utilisation. L'un des composants les plus visibles est la barre d'application supérieure.

Pour offrir aux utilisateurs un accès rapide à d'autres actions, ajoutons une barre d'application supérieure.

Ajouter un widget AppBar

Dans home.dart, ajoutez un widget AppBar à un élément Scaffold :

  // TODO: Add app bar (102)
  appBar: AppBar(
    // TODO: Add buttons and title (102)
  ),

L'ajout du widget AppBar au champ appBar: de Scaffold nous permet de bénéficier d'une mise en page impeccable sans effort en positionnant le widget au sommet de la page et le corps en dessous.

Enregistrez le projet. Une fois l'application Shrine mise à jour, cliquez sur Next (Suivant) pour afficher l'écran d'accueil.

Android

iOS

L'élément est bien positionné, mais il lui manque un titre.

Ajouter un widget Text

Dans home.dart, ajoutez un titre à la barre d'application AppBar :

// TODO: Add app bar (102)
  appBar: AppBar(
    // TODO: Add buttons and title (102)
    title: Text('SHRINE'),
    // TODO: Add trailing buttons (102)

Enregistrez le projet.

Android

iOS

De nombreuses barres d'application comportent un bouton à côté du titre. Ajoutons une icône de menu dans notre application.

Ajouter un bouton IconButton en début de ligne

Toujours dans home.dart, définissez un élément IconButton pour le champ leading: de la barre d'application AppBar. (Placez-le avant le champ title: conformément à la disposition du début à la fin de la ligne) :

    // TODO: Add buttons and title (102)
    leading: IconButton(
      icon: Icon(
        Icons.menu,
        semanticLabel: 'menu',
      ),
      onPressed: () {
        print('Menu button');
      },
    ),

Enregistrez le projet.

Android

iOS

L'icône de menu (également appelée "hamburger"), s'affiche à l'emplacement souhaité.

Vous pouvez également ajouter des boutons en fin de ligne. Dans Flutter, ces boutons sont appelés "actions".

Ajouter des actions

Il reste assez de place pour deux autres éléments IconButton.

Ajoutez-les à l'instance AppBar après le titre :

// TODO: Add trailing buttons (102)
actions: <Widget>[
  IconButton(
    icon: Icon(
      Icons.search,
      semanticLabel: 'search',
    ),
    onPressed: () {
      print('Search button');
    },
  ),
  IconButton(
    icon: Icon(
      Icons.tune,
      semanticLabel: 'filter',
    ),
    onPressed: () {
      print('Filter button');
    },
  ),
],

Enregistrez le projet. Votre écran d'accueil devrait se présenter ainsi :

Android

iOS

L'application dispose désormais d'un bouton en début de ligne, d'un titre et de deux actions à droite. La barre d'application présente également un effet d'élévation dû à un léger ombrage indiquant qu'elle se trouve à un niveau différent de celui du contenu.

Maintenant que notre application est un peu plus structurée, organisons ses contenus en les plaçant dans des fiches.

Ajouter un widget GridView

Commençons par ajouter une fiche sous la barre d'application supérieure. À l'heure actuelle, le widget Card (Fiche) ne contient pas suffisamment d'informations pour se positionner de façon visible. Nous allons donc l'encapsuler dans un widget GridView.

Remplacez le centre dans le corps de l'élément Scaffold par un widget GridView :

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  // TODO: Build a grid of cards (102)
  children: <Widget>[Card()],
),

Analysons ce code. Le widget Griview appelle le constructeur count(), car le nombre d'éléments qu'il affiche peut être compté et n'est pas infini. Cependant, il requiert certaines informations pour définir sa mise en page.

La variable crossAxisCount: indique le nombre d'éléments par ligne. Nous voulons obtenir deux colonnes.

Le champ padding: définit un espace libre sur les quatre côtés du widget GridView. Bien entendu, cette marge n'est pas visible à la suite de l'élément ou sous celui-ci, car aucun enfant GridView n'a encore été placé à côté de cet élément pour le moment.

Le champ childAspectRatio: identifie la taille des éléments sous forme de proportions (largeur sur hauteur).

Par défaut, GridView crée des blocs de taille identique.

En additionnant toutes ces données, le widget GridView calcule la largeur de chaque enfant comme suit : ([width of the entire grid] - [left padding] - [right padding]) / number of columns. Voici le calcul avec nos valeurs : ([width of the entire grid] - 16 - 16) / 2.

La hauteur est calculée à partir de la largeur, en appliquant le rapport suivant entre largeur et hauteur : ([width of the entire grid] - 16 - 16) / 2 * 9 / 8. Nous avons interverti 8 et 9, car nous commençons par la largeur et calculons la hauteur, et non l'inverse.

Nous avons une fiche, mais elle est vide. Ajoutons des widgets enfants à cette fiche.

Mettre en page les contenus

Les fiches doivent comporter des zones pour une image, un titre et du texte secondaire.

Mettez à jour les enfants du widget GridView :

// TODO: Build a grid of cards (102)
children: <Widget>[
  Card(
    clipBehavior: Clip.antiAlias,
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        AspectRatio(
          aspectRatio: 18.0 / 11.0,
          child: Image.asset('assets/diamond.png'),
        ),
        Padding(
          padding: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text('Title'),
              SizedBox(height: 8.0),
              Text('Secondary Text'),
            ],
          ),
        ),
      ],
    ),
  )
],

Ce code ajoute un widget Column (Colonne) qui permet de disposer les widgets enfants verticalement.

Le paramètre crossAxisAlignment: field spécifie la valeur CrossAxisAlignment.start, ce qui signifie "aligner le texte sur le côté 'début de ligne'".

Le widget AspectRatio définit les proportions de l'image affichée, quel que soit le type d'image fourni.

L'élément Padding définit une marge pour le texte.

Les deux widgets Text sont placés l'un au-dessus de l'autre et séparés par huit points d'espace vide (SizedBox). Nous créons un autre élément Column pour leur donner des marges intérieures.

Enregistrez le projet.

Android

iOS

Dans cet aperçu, vous pouvez voir que la fiche est positionnée avec une marge, ses coins sont arrondis et elle projette une ombre (représentant son élévation). La forme entière est appelée "conteneur" dans le système Material Design (à ne pas confondre avec la classe de widget appelée Container).

Les fiches s'affichent généralement dans une collection avec d'autres fiches. Disposons-les sous forme de collection dans une grille.

Lorsque plusieurs fiches sont présentes sur un écran, elles sont regroupées dans une ou plusieurs collections. Les fiches d'une collection sont coplanaires : elles ont toute la même élévation au repos (c'est-à-dire lorsqu'elles ne sont pas sélectionnées ou déplacées, ce que nous ne ferons pas ici).

Créer une collection de fiches

Pour l'instant, notre fiche se positionne à l'intérieur du champ children: de la grille GridView. Cela entraîne une grande quantité de code imbriqué qui peut être difficile à lire. Transformons cette partie du code en une fonction permettant de générer autant de fiches vides que nécessaire et de renvoyer une liste de fiches.

Créez une fonction privée au-dessus de la fonction build() (n'oubliez pas que les fonctions commençant par un trait de soulignement sont des API privées) :

// TODO: Make a collection of cards (102)
List<Card> _buildGridCards(int count) {
  List<Card> cards = List.generate(
    count,
    (int index) => Card(
      clipBehavior: Clip.antiAlias,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          AspectRatio(
            aspectRatio: 18.0 / 11.0,
            child: Image.asset('assets/diamond.png'),
          ),
          Padding(
            padding: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Text('Title'),
                SizedBox(height: 8.0),
                Text('Secondary Text'),
              ],
            ),
          ),
        ],
      ),
    ),
  );

  return cards;
}

Affectez les fiches générées au champ children du widget GridView. N'oubliez pas de remplacer tous les éléments contenus dans le widget GridView par ce nouveau code :

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  children: _buildGridCards(10) // Replace
),

Enregistrez le projet.

Android

iOS

Les fiches existent, mais ne présentent aucun contenu. Le moment est venu d'ajouter quelques données produit.

Ajouter des données produit

L'application contient quelques produits avec des images, des noms et des prix. Ajoutons cela aux widgets déjà présents dans la fiche

Ensuite, dans home.dart, importez un nouveau package et quelques fichiers fournis pour un modèle de données :

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

import 'model/products_repository.dart';
import 'model/product.dart';

Enfin, modifiez _buildGridCards() pour récupérer les informations sur le produit et utiliser ces données dans les fiches :

// TODO: Make a collection of cards (102)

// Replace this entire method
List<Card> _buildGridCards(BuildContext context) {
  List<Product> products = ProductsRepository.loadProducts(Category.all);

  if (products == null || products.isEmpty) {
    return const <Card>[];
  }

  final ThemeData theme = Theme.of(context);
  final NumberFormat formatter = NumberFormat.simpleCurrency(
      locale: Localizations.localeOf(context).toString());

  return products.map((product) {
    return Card(
      clipBehavior: Clip.antiAlias,
      // TODO: Adjust card heights (103)
      child: Column(
        // TODO: Center items on the card (103)
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          AspectRatio(
            aspectRatio: 18 / 11,
            child: Image.asset(
              product.assetName,
              package: product.assetPackage,
             // TODO: Adjust the box size (102)
            ),
          ),
          Expanded(
            child: Padding(
              padding: EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 8.0),
              child: Column(
               // TODO: Align labels to the bottom and center (103)
               crossAxisAlignment: CrossAxisAlignment.start,
                // TODO: Change innermost Column (103)
                children: <Widget>[
                 // TODO: Handle overflowing labels (103)
                 Text(
                    product.name,
                    style: theme.textTheme.headline6,
                    maxLines: 1,
                  ),
                  SizedBox(height: 8.0),
                  Text(
                    formatter.format(product.price),
                    style: theme.textTheme.subtitle2,
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }).toList();
}

REMARQUE : Attendez avant de lancer la compilation et l'exécution. Il reste une modification à apporter.

Modifiez également la fonction build() pour transmettre la valeur BuildContext à _buildGridCards() avant de lancer la compilation :

// TODO: Add a grid view (102)
body: GridView.count(
  crossAxisCount: 2,
  padding: EdgeInsets.all(16.0),
  childAspectRatio: 8.0 / 9.0,
  children: _buildGridCards(context) // Changed code
),

Android

iOS

Vous remarquerez peut-être que nous n'ajoutons pas d'espace vertical entre les fiches. C'est parce qu'elles ont par défaut quatre points de marge intérieure à leur sommet et à leur base.

Enregistrez le projet.

Les données produit s'affichent, mais les images sont entourées d'un espace inutile. Par défaut, les images sont dessinées avec le champ BoxFit défini sur .scaleDown (dans ce cas). Remplacez cette valeur par .fitWidth pour qu'elle effectue un zoom avant et supprime l'espace inutile.

Ajoutez un champ fit: à l'image avec la valeur BoxFit.fitWidth :

  // TODO: Adjust the box size (102)
  fit: BoxFit.fitWidth,

Android

iOS

Nos produits s'affichent désormais parfaitement dans l'application.

Notre application propose un fonctionnement de base permettant à l'utilisateur de passer de l'écran de connexion à un écran d'accueil où les produits sont affichés. Quelques lignes de code nous ont suffi pour ajouter une barre d'application supérieure (avec un titre et trois boutons) et des fiches (pour présenter le contenu de notre application). L'écran d'accueil obtenu est simple et fonctionnel, et présente une structure de base et des contenus exploitables.

Étapes suivantes

Avec la barre d'application supérieure, la fiche, le champ de texte et le bouton, nous avons utilisé quatre composants essentiels de la bibliothèque MDC-Flutter. Pour découvrir encore plus de composants, consultez le catalogue de widgets Flutter.

Même si notre application fonctionne parfaitement, elle ne reflète pas une identité de marque ni un point de vue particulier. Dans l'atelier MDC-103 : Utilisation des thèmes de Material Design (couleur, formes, élévation et type), nous allons personnaliser le style de ces composants pour exprimer une marque moderne et dynamique.

Atelier de programmation suivant

La réalisation de cet atelier de programmation m'a demandé un temps et des efforts raisonnables

Tout à fait d'accord D'accord Ni d'accord, ni pas d'accord Pas d'accord Pas du tout d'accord

Je souhaite continuer à utiliser Material Components

Tout à fait d'accord D'accord Ni d'accord, ni pas d'accord Pas d'accord Pas du tout d'accord