MDC-104 Flutter : Composants avancés de Material Design

1. Introduction

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 travaillant pour 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-103, vous avez personnalisé la couleur, l'élévation, la typographie et la forme des composants Material Components (MDC) pour appliquer un style à votre application.

Un composant du système Material Design effectue un ensemble de tâches prédéfinies et présente certaines caractéristiques, comme un bouton. Toutefois, un bouton est bien plus qu'un moyen pour l'utilisateur d'effectuer une action. C'est également une expression visuelle ayant une certaine forme, taille et couleur qui indique à l'utilisateur que le bouton est interactif et qu'une action se produira lorsqu'il appuiera ou cliquera dessus.

Les consignes de Material Design présentent les composants du point de vue du graphiste. Elles décrivent une large gamme de fonctions de base disponibles pour différentes plates-formes, ainsi que les éléments anatomiques qui constituent chaque composant. Par exemple, un fond (backdrop) se compose d'une couche arrière avec du contenu, d'une couche avant avec du contenu, de règles de mouvement et d'options d'affichage. Chacun de ces composants peut être personnalisé pour couvrir les besoins, cas d'utilisation et contenus de chaque application.

Objectifs de l'atelier

Dans cet atelier de programmation, vous allez remplacer l'interface utilisateur de l'application Shrine par une présentation à deux niveaux appelée "fond". Le fond comporte un menu répertoriant les catégories sélectionnables pour filtrer les produits affichés dans la grille asymétrique. Dans cet atelier de programmation, vous allez utiliser les composants de Flutter suivants :

  • Forme
  • Animation
  • Widgets Flutter (que vous avez utilisés dans les ateliers de programmation précédents)

Android

iOS

application d'e-commerce basée sur un thème rose et marron, avec une barre d'application supérieure et une grille de produits asymétrique qui défile horizontalement

application d'e-commerce basée sur un thème rose et marron, avec une barre d'application supérieure et une grille de produits asymétrique qui défile horizontalement

menu listant quatre catégories

menu listant quatre catégories

Composants et sous-systèmes de MDC-Flutter dans cet atelier de programmation

  • Forme

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

Débutant Intermédiaire Expert

2. Configurer l'environnement de développement Flutter

Pour cet atelier, vous avez besoin de deux logiciels : le SDK Flutter et un éditeur.

Vous pouvez exécuter l'atelier de programmation sur l'un des appareils suivants :

  • Un appareil Android ou iOS physique connecté à votre ordinateur et réglé en mode développeur.
  • Le simulateur iOS (outils Xcode à installer).
  • L'émulateur Android (qui doit être configuré dans Android Studio).
  • Un navigateur (Chrome est requis pour le débogage).
  • En tant qu'application de bureau Windows, Linux ou macOS. Vous devez développer votre application sur la plate-forme où vous comptez la déployer. Par exemple, si vous voulez développer une application de bureau Windows, vous devez le faire sous Windows pour accéder à la chaîne de compilation appropriée. Prenez également connaissance des exigences spécifiques aux systèmes d'exploitation, lesquelles sont détaillées sur docs.flutter.dev/desktop.

3. Télécharger l'application de démarrage de l'atelier de programmation

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

Si vous avez fini l'atelier de programmation MDC-103, votre code devrait être prêt pour commencer cet atelier. Passez à l'étape Ajouter le menu de fond.

Vous partez de zéro ?

L'application de départ se trouve dans le répertoire material-components-flutter-codelabs-104-starter_and_103-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 104-starter_and_103-complete

Ouvrir le projet et exécuter l'application

  1. Ouvrez le projet dans l'éditeur de votre choix.
  2. Suivez les instructions concernant l'éditeur que vous avez choisi. Elles sont accessibles au paragraphe "Run the app" (Exécuter l'application) sur la page Get Started > Test drive (Premiers pas > Faire un essai).

Opération réussie. La page de connexion de Shrine créée dans les précédents ateliers de programmation devrait s'afficher sur votre appareil.

Android

iOS

Page de connexion de Shrine

Page de connexion de Shrine

4. Ajouter le menu de fond

Un fond s'affiche derrière tous les autres contenus et composants. Il comporte deux couches : une couche arrière (qui affiche les actions et les filtres) et une couche avant (qui affiche le contenu). Vous pouvez utiliser un fond pour afficher des informations et des actions interactives, comme des filtres de navigation ou de contenu.

Supprimer la barre d'application de la page d'accueil

Le widget HomePage sera le contenu de notre couche avant. Pour le moment, il contient une barre d'application. Nous allons déplacer la barre d'application vers la couche arrière afin que HomePage n'inclue que AsymmetricView.

Dans home.dart, modifiez la fonction build() pour ne renvoyer qu'une vue AsymmetricView :

// TODO: Return an AsymmetricView (104)
return AsymmetricView(products: ProductsRepository.loadProducts(Category.all));

Ajouter le widget Backdrop

Créez un widget appelé Backdrop qui inclut la frontLayer et la backLayer.

La backLayer contient un menu qui vous permet de sélectionner une catégorie pour filtrer la liste (currentCategory). Pour que le menu reste, nous allons définir Backdrop comme un widget avec état.

Ajoutez à /lib un fichier appelé backdrop.dart :

import 'package:flutter/material.dart';

import 'model/product.dart';

// TODO: Add velocity constant (104)

class Backdrop extends StatefulWidget {
  final Category currentCategory;
  final Widget frontLayer;
  final Widget backLayer;
  final Widget frontTitle;
  final Widget backTitle;

  const Backdrop({
    required this.currentCategory,
    required this.frontLayer,
    required this.backLayer,
    required this.frontTitle,
    required this.backTitle,
    Key? key,
  }) : super(key: key);

  @override
  _BackdropState createState() => _BackdropState();
}

// TODO: Add _FrontLayer class (104)
// TODO: Add _BackdropTitle class (104)
// TODO: Add _BackdropState class (104)

Vous remarquerez que certaines propriétés sont marquées comme required. Cette pratique est recommandée lorsque le constructeur comporte des propriétés qui n'ont pas de valeur par défaut et ne peuvent pas avoir la valeur null, et qui par conséquent ne doivent pas être oubliées.

Sous la définition de la classe Backdrop, ajoutez la classe _BackdropState :

// TODO: Add _BackdropState class (104)
class _BackdropState extends State<Backdrop>
    with SingleTickerProviderStateMixin {
  final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');

  // TODO: Add AnimationController widget (104)

  // TODO: Add BuildContext and BoxConstraints parameters to _buildStack (104)
  Widget _buildStack() {
    return Stack(
    key: _backdropKey,
      children: <Widget>[
        // TODO: Wrap backLayer in an ExcludeSemantics widget (104)
        widget.backLayer,
        widget.frontLayer,
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    var appBar = AppBar(
      elevation: 0.0,
      titleSpacing: 0.0,
      // TODO: Replace leading menu icon with IconButton (104)
      // TODO: Remove leading property (104)
      // TODO: Create title with _BackdropTitle parameter (104)
      leading: Icon(Icons.menu),
      title: Text('SHRINE'),
      actions: <Widget>[
        // TODO: Add shortcut to login screen from trailing icons (104)
        IconButton(
          icon: Icon(
            Icons.search,
            semanticLabel: 'search',
          ),
          onPressed: () {
          // TODO: Add open login (104)
          },
        ),
        IconButton(
          icon: Icon(
            Icons.tune,
            semanticLabel: 'filter',
          ),
          onPressed: () {
          // TODO: Add open login (104)
          },
        ),
      ],
    );
    return Scaffold(
      appBar: appBar,
      // TODO: Return a LayoutBuilder widget (104)
      body: _buildStack(),
    );
  }
}

La fonction build() renvoie un Scaffold avec une barre d'application semblable à celle de la page d'accueil. Mais le corps du Scaffold est une pile. Les enfants d'une pile peuvent se chevaucher. La taille et l'emplacement de chaque enfant sont donnés par rapport au parent de la pile.

Ajoutez à présent une instance Backdrop à ShrineApp.

Dans app.dart, importez backdrop.dart et model/product.dart :

import 'backdrop.dart'; // New code
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'model/product.dart'; // New code
import 'supplemental/cut_corners_border.dart';

Dans app.dart,, modifiez la route / en renvoyant un Backdrop dont la frontLayer est HomePage :

// TODO: Change to a Backdrop with a HomePage frontLayer (104)
'/': (BuildContext context) => Backdrop(
     // TODO: Make currentCategory field take _currentCategory (104)
     currentCategory: Category.all,
     // TODO: Pass _currentCategory for frontLayer (104)
     frontLayer: HomePage(),
     // TODO: Change backLayer field value to CategoryMenuPage (104)
     backLayer: Container(color: kShrinePink100),
     frontTitle: Text('SHRINE'),
     backTitle: Text('MENU'),
),

Enregistrez votre projet. Vous devriez voir la page d'accueil et la barre d'application s'afficher :

Android

iOS

Page des produits de Shrine avec un arrière-plan rose

Page des produits de Shrine avec un arrière-plan rose

La backLayer affiche la zone rose dans une nouvelle couche derrière la page d'accueil de la frontLayer.

Vous pouvez utiliser l'outil d'inspection de Flutter pour vérifier que la pile possède bien d'un conteneur derrière une page d'accueil Cela devrait ressembler à ceci :

4783ed30f1cc010.png

Vous pouvez à présent ajuster le design et le contenu des deux couches.

5. Ajouter une forme

Dans cette étape, vous allez appliquer un style à la couche avant pour ajouter une découpe dans l'angle supérieur gauche.

Material Design désigne ce type de personnalisation sous le terme de "forme". Les surfaces de Material Design peuvent avoir des formes arbitraires. Les formes mettent en avant les surfaces et leur donnent du style, et peuvent servir pour exprimer le branding. Les formes rectangulaires traditionnelles peuvent être personnalisées par des angles et des bordures arrondis ou pointus, et par autant de côtés que désiré. Elles peuvent être symétriques ou irrégulières.

Ajouter une forme à la couche avant

Le logo angulaire de Shrine a inspiré l'histoire des formes de l'application Shrine. L'histoire des formes désigne l'utilisation courante des formes appliquées dans toute une application. Par exemple, la forme du logo est reprise dans les éléments de la page de connexion auxquels la forme est appliquée. Dans cette étape, vous allez appliquer un style à la couche avant avec une découpe en biseau dans l'angle supérieur gauche.

Dans backdrop.dart, ajoutez une nouvelle classe _FrontLayer :

// TODO: Add _FrontLayer class (104)
class _FrontLayer extends StatelessWidget {
  // TODO: Add on-tap callback (104)
  const _FrontLayer({
    Key? key,
    required this.child,
  }) : super(key: key);

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Material(
      elevation: 16.0,
      shape: const BeveledRectangleBorder(
        borderRadius: BorderRadius.only(topLeft: Radius.circular(46.0)),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          // TODO: Add a GestureDetector (104)
          Expanded(
            child: child,
          ),
        ],
      ),
    );
  }
}

Ensuite, dans la fonction _buildStack() de _BackdropState, encapsulez la couche avant dans une _FrontLayer :

  Widget _buildStack() {
    // TODO: Create a RelativeRectTween Animation (104)

    return Stack(
    key: _backdropKey,
      children: <Widget>[
        // TODO: Wrap backLayer in an ExcludeSemantics widget (104)
        widget.backLayer,
        // TODO: Add a PositionedTransition (104)
        // TODO: Wrap front layer in _FrontLayer (104)
          _FrontLayer(child: widget.frontLayer),
      ],
    );
  }

Actualisez la page.

Android

iOS

Page des produits de Shrine avec la forme personnalisée

Page des produits de Shrine avec la forme personnalisée

Nous avons donné à la surface principale de Shrine une forme personnalisée. Grâce à l'élévation de la surface, les utilisateurs peuvent voir que quelque chose se trouve derrière la couche avant blanche. Ajoutons une animation afin que les utilisateurs puissent voir la couche arrière du fond.

6. Ajouter un effet de mouvement

Une animation permet de donner vie à votre application. Elle peut être imposante et spectaculaire, subtile et minimaliste, ou quelque part entre ces deux extrêmes. Gardez toutefois à l'esprit que le type d'animation que vous utilisez doit être adapté à la situation. Les animations appliquées à des actions standard récurrentes doivent être petites et subtiles, de sorte que ces actions ne détournent pas l'attention de l'utilisateur ni ne prennent régulièrement trop de temps. Toutefois, certaines situations se prêtent bien à des animations captivantes, par exemple la première fois que l'utilisateur ouvre l'application. D'autres animations peuvent aussi servir à montrer à l'utilisateur comment utiliser votre application.

Ajouter une animation d'affichage au bouton de menu

Au début de backdrop.dart, en dehors du champ d'une classe ou d'une fonction, ajoutez une constante pour représenter la vitesse de l'animation :

// TODO: Add velocity constant (104)
const double _kFlingVelocity = 2.0;

Ajoutez un widget AnimationController à _BackdropState, instanciez-le dans la fonction initState() et placez-le dans la fonction dispose() de l'état :

  // TODO: Add AnimationController widget (104)
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 300),
      value: 1.0,
      vsync: this,
    );
  }

  // TODO: Add override for didUpdateWidget (104)

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  // TODO: Add functions to get and change front layer visibility (104)

AnimationController coordonne les animations et met l'API à votre disposition pour lire, inverser et arrêter l'animation. Maintenant, nous avons besoin de fonctions pour exécuter l'animation.

Ajoutez des fonctions qui déterminent et modifient la visibilité de la couche avant :

  // TODO: Add functions to get and change front layer visibility (104)
  bool get _frontLayerVisible {
    final AnimationStatus status = _controller.status;
    return status == AnimationStatus.completed ||
        status == AnimationStatus.forward;
  }

  void _toggleBackdropLayerVisibility() {
    _controller.fling(
        velocity: _frontLayerVisible ? -_kFlingVelocity : _kFlingVelocity);
  }

Encapsulez la backLayer dans un widget ExcludeSemantics. Ce widget exclut les éléments de menu de backLayer de l'arborescence sémantique lorsque la couche arrière n'est pas visible.

    return Stack(
      key: _backdropKey,
      children: <Widget>[
        // TODO: Wrap backLayer in an ExcludeSemantics widget (104)
        ExcludeSemantics(
          child: widget.backLayer,
          excluding: _frontLayerVisible,
        ),
      ...

Modifiez la fonction _buildStack() pour qu'elle accepte un BuildContext et des BoxConstraints. Incluez également une PositionedTransition qui accepte une animation RelativeRectTween :

  // TODO: Add BuildContext and BoxConstraints parameters to _buildStack (104)
  Widget _buildStack(BuildContext context, BoxConstraints constraints) {
    const double layerTitleHeight = 48.0;
    final Size layerSize = constraints.biggest;
    final double layerTop = layerSize.height - layerTitleHeight;

    // TODO: Create a RelativeRectTween Animation (104)
    Animation<RelativeRect> layerAnimation = RelativeRectTween(
      begin: RelativeRect.fromLTRB(
          0.0, layerTop, 0.0, layerTop - layerSize.height),
      end: const RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0),
    ).animate(_controller.view);

    return Stack(
      key: _backdropKey,
      children: <Widget>[
        // TODO: Wrap backLayer in an ExcludeSemantics widget (104)
        ExcludeSemantics(
          child: widget.backLayer,
          excluding: _frontLayerVisible,
        ),
        // TODO: Add a PositionedTransition (104)
        PositionedTransition(
          rect: layerAnimation,
          child: _FrontLayer(
            // TODO: Implement onTap property on _BackdropState (104)
            child: widget.frontLayer,
          ),
        ),
      ],
    );
  }

Enfin, au lieu d'appeler la fonction _buildStack pour le corps de l'échafaudage, renvoyez un widget LayoutBuilder qui utilise _buildStack comme compilateur :

    return Scaffold(
      appBar: appBar,
      // TODO: Return a LayoutBuilder widget (104)
      body: LayoutBuilder(builder: _buildStack),
    );

Nous avons retardé la création de la pile de couches avant/arrière jusqu'à ce que le temps d'affichage soit écoulé en utilisant LayoutBuilder afin de pouvoir intégrer la hauteur réelle globale du fond. LayoutBuilder est un widget spécial dont le rappel de compilateur fournit des contraintes de taille.

Dans la fonction build(), transformez l'icône de menu principale dans la barre d'application en IconButton, puis utilisez celui-ci pour activer ou désactiver la visibilité de la couche avant lorsque l'utilisateur appuie sur le bouton.

      // TODO: Replace leading menu icon with IconButton (104)
      leading: IconButton(
        icon: const Icon(Icons.menu),
        onPressed: _toggleBackdropLayerVisibility,
      ),

Actualisez la page, puis appuyez sur le bouton de menu dans le simulateur.

Android

iOS

Menu de Shrine vide avec deux erreurs

Menu de Shrine vide avec deux erreurs

La couche avant s'anime (glisse). Si vous regardez en bas de l'écran, vous voyez une erreur rouge et une erreur de dépassement. Cela est dû au fait qu'AsymmetricView est compressée et réduite par cette animation, ce qui laisse moins de place aux colonnes. Au final, les colonnes ne peuvent pas s'afficher dans l'espace prévu, ce qui génère une erreur. Si nous remplaçons les colonnes par ListViews, la taille des colonnes ne devrait pas changer pendant l'animation.

Encapsuler les colonnes de produits dans une ListView

Dans supplemental/product_columns.dart, remplacez la colonne dans OneProductCardColumn par une ListView :

class OneProductCardColumn extends StatelessWidget {
  const OneProductCardColumn({required this.product, Key? key}) : super(key: key);

  final Product product;

  @override
  Widget build(BuildContext context) {
    // TODO: Replace Column with a ListView (104)
    return ListView(
      physics: const ClampingScrollPhysics(),
      reverse: true,
      children: <Widget>[
        const SizedBox(
          height: 40.0,
        ),
        ProductCard(
          product: product,
        ),
      ],
    );
  }
}

La colonne inclut MainAxisAlignment.end. Pour commencer la mise en page depuis le bas de la page, spécifiez reverse: true. L'ordre des enfants est inversé pour compenser le changement.

Actualisez la page, puis appuyez sur le bouton de menu.

Android

iOS

Menu de Shrine vide avec une erreur

Menu de Shrine vide avec une erreur

L'avertissement "overflow" (dépassement) gris sur OneProductCardColumn a disparu ! Maintenant, corrigeons l'autre erreur.

Dans supplemental/product_columns.dart, modifiez la méthode de calcul de imageAspectRatio et remplacez la colonne dans TwoProductCardColumn par une ListView :

      // TODO: Change imageAspectRatio calculation (104)
      double imageAspectRatio = heightOfImages >= 0.0
          ? constraints.biggest.width / heightOfImages
          : 49.0 / 33.0;
      // TODO: Replace Column with a ListView (104)
      return ListView(
        physics: const ClampingScrollPhysics(),
        children: <Widget>[
          Padding(
            padding: const EdgeInsetsDirectional.only(start: 28.0),
            child: top != null
                ? ProductCard(
                    imageAspectRatio: imageAspectRatio,
                    product: top!,
                  )
                : SizedBox(
                    height: heightOfCards,
                  ),
          ),
          const SizedBox(height: spacerHeight),
          Padding(
            padding: const EdgeInsetsDirectional.only(end: 28.0),
            child: ProductCard(
              imageAspectRatio: imageAspectRatio,
              product: bottom,
            ),
          ),
        ],
      );

Nous avons également ajouté certaines protections à imageAspectRatio.

Actualisez la page. Ensuite, appuyez sur le bouton de menu.

Android

iOS

Menu de Shrine vide

Menu de Shrine vide

Il n'y a plus de dépassement.

7. Ajouter un menu à la couche arrière

Un menu est une liste d'éléments textuels cliquables qui informent les écouteurs lorsque des éléments textuels sont sélectionnés. Dans cette étape, vous allez ajouter un menu de filtrage par catégorie.

Ajouter le menu

Ajoutez le menu à la couche avant et les boutons interactifs à la couche arrière.

Créez un fichier appelé lib/category_menu_page.dart :

import 'package:flutter/material.dart';

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

class CategoryMenuPage extends StatelessWidget {
  final Category currentCategory;
  final ValueChanged<Category> onCategoryTap;
  final List<Category> _categories = Category.values;

  const CategoryMenuPage({
    Key? key,
    required this.currentCategory,
    required this.onCategoryTap,
  }) : super(key: key);

  Widget _buildCategory(Category category, BuildContext context) {
    final categoryString =
        category.toString().replaceAll('Category.', '').toUpperCase();
    final ThemeData theme = Theme.of(context);

    return GestureDetector(
      onTap: () => onCategoryTap(category),
      child: category == currentCategory
        ? Column(
            children: <Widget>[
              const SizedBox(height: 16.0),
              Text(
                categoryString,
                style: theme.textTheme.bodyText1,
                textAlign: TextAlign.center,
              ),
              const SizedBox(height: 14.0),
              Container(
                width: 70.0,
                height: 2.0,
                color: kShrinePink400,
              ),
            ],
          )
      : Padding(
        padding: const EdgeInsets.symmetric(vertical: 16.0),
        child: Text(
          categoryString,
          style: theme.textTheme.bodyText1!.copyWith(
              color: kShrineBrown900.withAlpha(153)
            ),
          textAlign: TextAlign.center,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        padding: const EdgeInsets.only(top: 40.0),
        color: kShrinePink100,
        child: ListView(
          children: _categories
            .map((Category c) => _buildCategory(c, context))
            .toList()),
      ),
    );
  }
}

Il s'agit d'un GestDetector qui encapsule une colonne dont les enfants sont les noms des catégories. Un trait de soulignement est utilisé pour indiquer la catégorie sélectionnée.

Dans app.dart, convertissez le widget ShrineApp de widget sans état à widget avec état.

  1. Mettez ShrineApp. en surbrillance.
  2. Affichez les actions de code correspondant à votre IDE :
  3. Android Studio : appuyez sur ⌥Entrée (macOS) ou Alt+Entrée
  4. VS Code : appuyez sur ⌘. (macOS) ou Ctrl+.
  5. Sélectionnez "Convert to StatefulWidget" (Convertir en widget avec état).
  6. Passez la classe ShrineAppState en mode "privé" (_ShrineAppState). Effectuez un clic droit sur ShrineAppState, puis :
  7. Android Studio : sélectionnez Refactor > Rename (Refactoriser > Renommer)
  8. VS Code : sélectionnez Renommer le symbole
  9. Saisissez _ShrineAppState pour rendre la classe privée.

Dans app.dart, ajoutez une variable à _ShrineAppState pour la catégorie sélectionnée et un rappel lorsque l'utilisateur appuie dessus :

class _ShrineAppState extends State<ShrineApp> {
  Category _currentCategory = Category.all;

  void _onCategoryTap(Category category) {
    setState(() {
      _currentCategory = category;
    });
  }

Définissez ensuite la couche arrière comme CategoryMenuPage.

Dans app.dart, importez CategoryMenuPage :

import 'backdrop.dart';
import 'category_menu_page.dart';
import 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'model/product.dart';
import 'supplemental/cut_corners_border.dart';

Dans la fonction build(), définissez le champ "backLayer" sur "CategoryMenuPage" et modifiez le champ "currentCategory" pour qu'il accepte la variable d'instance.

'/': (BuildContext context) => Backdrop(
              // TODO: Make currentCategory field take _currentCategory (104)
              currentCategory: _currentCategory,
              // TODO: Pass _currentCategory for frontLayer (104)
              frontLayer: HomePage(),
              // TODO: Change backLayer field value to CategoryMenuPage (104)
              backLayer: CategoryMenuPage(
                currentCategory: _currentCategory,
                onCategoryTap: _onCategoryTap,
              ),
              frontTitle: const Text('SHRINE'),
              backTitle: const Text('MENU'),
            ),

Actualisez la page, puis appuyez sur le bouton de menu.

Android

iOS

Menu de Shrine avec quatre catégories

Menu de Shrine avec quatre catégories

Si vous appuyez sur une option de menu, rien ne se passe… pour l'instant. Résolvons ce problème.

Dans home.dart, ajoutez une variable pour la catégorie et transmettez-la à AsymmetricView.

import 'package:flutter/material.dart';

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

class HomePage extends StatelessWidget {
  // TODO: Add a variable for Category (104)
  final Category category;

  const HomePage({this.category = Category.all, Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // TODO: Pass Category variable to AsymmetricView (104)
    return AsymmetricView(
      products: ProductsRepository.loadProducts(category),
    );
  }
}

Dans app.dart, transmettez _currentCategory pour frontLayer :

// TODO: Pass _currentCategory for frontLayer (104)
frontLayer: HomePage(category: _currentCategory),

Actualisez la page. Appuyez sur le bouton de menu du simulateur, puis sélectionnez une catégorie.

Android

iOS

Page de produits filtrée de Shrine

Page de produits filtrée de Shrine

Appuyez sur l'icône de menu pour afficher les produits. Ils sont filtrés !

Fermer la couche avant après avoir sélectionné un menu

Dans backdrop.dart, ajoutez un forçage pour la fonction didUpdateWidget() (appelée dès que la configuration du widget change) dans _BackdropState :

  // TODO: Add override for didUpdateWidget() (104)
  @override
  void didUpdateWidget(Backdrop old) {
    super.didUpdateWidget(old);

    if (widget.currentCategory != old.currentCategory) {
      _toggleBackdropLayerVisibility();
    } else if (!_frontLayerVisible) {
      _controller.fling(velocity: _kFlingVelocity);
    }
  }

Enregistrez votre projet pour déclencher un hot reload. Appuyez sur l'icône de menu et sélectionnez une catégorie. Le menu devrait se fermer automatiquement et vous devriez voir la catégorie des éléments sélectionnés. Vous allez à présent ajouter cette fonctionnalité également à la couche avant.

Activer/Désactiver la couche avant

Dans backdrop.dart, ajoutez à la couche de fond un rappel qui se déclenche lors d'un appui de l'utilisateur :

class _FrontLayer extends StatelessWidget {
  // TODO: Add on-tap callback (104)
  const _FrontLayer({
    Key? key,
    this.onTap, // New code
    required this.child,
  }) : super(key: key);

  final VoidCallback? onTap; // New code
  final Widget child;

Ajoutez ensuite un GestureDetector à l'enfant de _FrontLayer : enfants de la colonne :

      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          // TODO: Add a GestureDetector (104)
          GestureDetector(
            behavior: HitTestBehavior.opaque,
            onTap: onTap,
            child: Container(
              height: 40.0,
              alignment: AlignmentDirectional.centerStart,
            ),
          ),
          Expanded(
            child: child,
          ),
        ],
      ),

Ensuite, implémentez la nouvelle propriété onTap sur _BackdropState dans la fonction _buildStack() :

          PositionedTransition(
            rect: layerAnimation,
            child: _FrontLayer(
              // TODO: Implement onTap property on _BackdropState (104)
              onTap: _toggleBackdropLayerVisibility,
              child: widget.frontLayer,
            ),
          ),

Actualisez la page, puis appuyez sur la partie supérieure de la couche avant. Celle-ci doit s'ouvrir et se fermer chaque fois que vous appuyez sur sa partie supérieure.

8. Ajouter une icône de marque

L'iconographie de la marque s'étend aussi aux icônes familières. Nous allons à présent personnaliser l'icône d'affichage et la fusionner avec notre titre pour donner un style unique fidèle à la marque.

Modifier l'icône du bouton de menu

Android

iOS

Page des produits de Shrine avec l'icône de marque

Page des produits de Shrine avec l'icône de marque

Dans backdrop.dart, créez une classe _BackdropTitle.

// TODO: Add _BackdropTitle class (104)
class _BackdropTitle extends AnimatedWidget {
  final void Function() onPress;
  final Widget frontTitle;
  final Widget backTitle;

  const _BackdropTitle({
    Key? key,
    required Animation<double> listenable,
    required this.onPress,
    required this.frontTitle,
    required this.backTitle,
  }) : _listenable = listenable,
       super(key: key, listenable: listenable);

  final Animation<double> _listenable;

  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = _listenable;

    return DefaultTextStyle(
      style: Theme.of(context).textTheme.headline6!,
      softWrap: false,
      overflow: TextOverflow.ellipsis,
      child: Row(children: <Widget>[
        // branded icon
        SizedBox(
          width: 72.0,
          child: IconButton(
            padding: const EdgeInsets.only(right: 8.0),
            onPressed: this.onPress,
            icon: Stack(children: <Widget>[
              Opacity(
                opacity: animation.value,
                child: const ImageIcon(AssetImage('assets/slanted_menu.png')),
              ),
              FractionalTranslation(
                translation: Tween<Offset>(
                  begin: Offset.zero,
                  end: const Offset(1.0, 0.0),
                ).evaluate(animation),
                child: const ImageIcon(AssetImage('assets/diamond.png')),
              )]),
          ),
        ),
        // Here, we do a custom cross fade between backTitle and frontTitle.
        // This makes a smooth animation between the two texts.
        Stack(
          children: <Widget>[
            Opacity(
              opacity: CurvedAnimation(
                parent: ReverseAnimation(animation),
                curve: const Interval(0.5, 1.0),
              ).value,
              child: FractionalTranslation(
                translation: Tween<Offset>(
                  begin: Offset.zero,
                  end: const Offset(0.5, 0.0),
                ).evaluate(animation),
                child: backTitle,
              ),
            ),
            Opacity(
              opacity: CurvedAnimation(
                parent: animation,
                curve: const Interval(0.5, 1.0),
              ).value,
              child: FractionalTranslation(
                translation: Tween<Offset>(
                  begin: const Offset(-0.25, 0.0),
                  end: Offset.zero,
                ).evaluate(animation),
                child: frontTitle,
              ),
            ),
          ],
        )
      ]),
    );
  }
}

Le widget _BackdropTitle est un widget personnalisé qui remplace le widget Text simple du paramètre title du widget AppBar. Il possède une icône de menu animée, ainsi que des transitions animées entre le titre en avant-plan et le titre en arrière-plan. L'icône de menu animée utilisera un nouvel élément. La référence du nouveau slanted_menu.png doit être ajoutée à pubspec.yaml.

assets:
    - assets/diamond.png
    # TODO: Add slanted menu asset (104)
    - assets/slanted_menu.png
    - packages/shrine_images/0-0.jpg

Supprimez la propriété leading du compilateur AppBar. Il est nécessaire de la supprimer pour que l'icône personnalisée de la marque s'affiche à l'emplacement du widget leading d'origine. L'animation listenable et le gestionnaire onPress de l'icône personnalisée sont transférés à _BackdropTitle. frontTitle et backTitle sont également transmis afin qu'ils puissent s'afficher dans le titre en fond. Le paramètre title de AppBar devrait ressembler à ceci :

// TODO: Create title with _BackdropTitle parameter (104)
title: _BackdropTitle(
  listenable: _controller.view,
  onPress: _toggleBackdropLayerVisibility,
  frontTitle: widget.frontTitle,
  backTitle: widget.backTitle,
),

L'icône de marque est créée dans _BackdropTitle.. Elle contient une Stack d'icônes animées : un menu incliné et un diamant, à l'intérieur d'un IconButton afin que l'utilisateur puisse appuyer dessus. Le IconButton est ensuite encapsulé dans un SizedBox afin de faire de la place pour l'animation horizontale de l'icône.

L'architecture de Flutter où tout est un widget permet de modifier la mise en page de AppBar par défaut sans avoir à créer un tout nouveau widget AppBar personnalisé. Le paramètre title, qui est à l'origine un widget Text, peut être remplacé par un _BackdropTitle plus complexe. Comme _BackdropTitle inclut également l'icône personnalisée, il remplace la propriété leading, qui peut désormais être omise. Cette substitution de widgets simple peut être effectuée sans modification des autres paramètres (tels que les icônes d'action) qui continuent de fonctionner indépendamment.

Ajouter un raccourci de retour à l'écran de connexion

Dans backdrop.dart,, ajoutez un raccourci pour revenir à l'écran de connexion à partir des deux icônes finales dans la barre d'application : modifiez les libellés sémantiques des icônes pour refléter leur nouveau rôle.

        // TODO: Add shortcut to login screen from trailing icons (104)
        IconButton(
          icon: const Icon(
            Icons.search,
            semanticLabel: 'login', // New code
          ),
          onPressed: () {
            // TODO: Add open login (104)
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (BuildContext context) => LoginPage()),
            );
          },
        ),
        IconButton(
          icon: const Icon(
            Icons.tune,
            semanticLabel: 'login', // New code
          ),
          onPressed: () {
            // TODO: Add open login (104)
            Navigator.push(
              context,
              MaterialPageRoute(
                builder: (BuildContext context) => LoginPage()),
            );
          },
        ),

Un message d'erreur s'affiche si vous essayez d'actualiser la page. Importez login.dart pour corriger l'erreur :

import 'login.dart';

Actualisez l'application et appuyez sur les boutons de recherche ou de réglage pour revenir à l'écran de connexion.

9. Félicitations !

Au cours de ces quatre ateliers de programmation, vous avez vu comment utiliser Material Components pour créer des expériences utilisateur uniques et élégantes qui expriment la personnalité et le style de votre marque.

Étapes suivantes

Cet atelier de programmation, MDC-104, termine cette série d'ateliers. Pour découvrir encore plus de composants de MDC-Flutter, consultez le catalogue des widgets Material Components.

Pour aller plus loin, essayez de remplacer l'icône de marque par une AnimatedIcon qui s'anime entre deux icônes lorsque le fond devient visible.

De nombreux autres ateliers de programmation Flutter sont disponibles. N'hésitez pas à essayer ceux qui vous intéressent. Nous avons également un autre atelier de programmation spécifique à Material Design qui pourrait vous intéresser : Créer de jolies transitions avec le système de mouvement de Material Design pour Android.

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