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

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-103, vous avez personnalisé la couleur, l'élévation, la typographie et la forme des composants de 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 décrivent 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 se compose d'une couche arrière avec son contenu, d'une couche avant avec son contenu, de règles de mouvement et d'options d'affichage. Chacun de ces composants peut être personnalisé pour répondre aux besoins, cas d'utilisation et contenus de chaque application. Ces éléments sont, pour la plupart, des vues, commandes et fonctions traditionnelles du SDK de votre plate-forme.

Bien que les consignes de Material Design citent plusieurs composants, tous ne conviennent pas pour du code réutilisable et ne sont donc pas disponibles dans MDC. Vous pouvez créer ces expériences vous-même et personnaliser le style de votre application en utilisant un code traditionnel.

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

Composant 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

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 Premiers pas : Installation. Pour configurer un éditeur, consultez la section 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

Continuer à partir de 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 ?

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

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

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 built for <version>).
  • Appuyez sur l'icône de lecture ().

Bravo ! La page de connexion de Shrine créée dans les précédents ateliers de programmation devrait s'afficher dans le simulateur ou l'émulateur.

Android

iOS

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 un fichier à /lib appelé backdrop.dart :

import 'package:flutter/material.dart';
import 'package:meta/meta.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,
  })  : assert(currentCategory != null),
        assert(frontLayer != null),
        assert(backLayer != null),
        assert(frontTitle != null),
        assert(backTitle != null);

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

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

Le package meta est importé afin de marquer les propriétés @required. Cette pratique est recommandée lorsque le constructeur comporte des propriétés qui n'ont pas de valeurs par défaut qui ne peuvent pas être null, et qui ne doivent donc pas être oubliées. Notez que nous avons également des assertions après le constructeur qui vérifient que les valeurs transmises dans ces champs ne sont effectivement pas null.

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(
      brightness: Brightness.light,
      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 fonction build() de ShrineApp. Remplacez home: par un fond dont la frontLayer est une page d'accueil :

      // TODO: Change home: to a Backdrop with a HomePage frontLayer (104)
      home: 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'),
      ),

Si vous appuyez sur le bouton de lecture, vous devriez voir la page d'accueil et la barre d'application s'afficher :

Android

iOS

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 :

ad988a22875b5e82.png

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

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,
    this.child,
  }) : super(key: key);

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return Material(
      elevation: 16.0,
      shape: 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

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.

L'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)
  AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: 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: 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: Icon(Icons.menu),
        onPressed: _toggleBackdropLayerVisibility,
      ),

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

Android

iOS

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 {
  OneProductCardColumn({this.product});

  final Product product;

  @override
  Widget build(BuildContext context) {
    // TODO: Replace Column with a ListView (104)
    return ListView(
      physics: const ClampingScrollPhysics(),
      reverse: true,
      children: <Widget>[
        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

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: EdgeInsetsDirectional.only(start: 28.0),
            child: top != null
                ? ProductCard(
                    imageAspectRatio: imageAspectRatio,
                    product: top,
                  )
                : SizedBox(
                    height: heightOfCards,
                  ),
          ),
          SizedBox(height: spacerHeight),
          Padding(
            padding: 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

Il n'y a plus de dépassement.

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 'package:meta/meta.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,
  })  : assert(currentCategory != null),
        assert(onCategoryTap != null);

  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>[
            SizedBox(height: 16.0),
            Text(
              categoryString,
              style: theme.textTheme.bodyText1,
              textAlign: TextAlign.center,
            ),
            SizedBox(height: 14.0),
            Container(
              width: 70.0,
              height: 2.0,
              color: kShrinePink400,
            ),
          ],
        )
      : Padding(
        padding: 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: 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. Sélectionnez ShrineApp..
  2. Appuyez sur Alt (option) + Entrée.
  3. Sélectionnez "Convert to StatefulWidget" (Convertir en widget avec état).
  4. Définissez la classe ShrineAppState sur "privé" (_ShrineAppState). Pour cela, à partir du menu principal de l'IDE, sélectionnez Refactor > Rename (Refactoriser > Renommer). Vous pouvez aussi, à partir du code, sélectionner le nom de la classe, ShrineAppState, puis faire un clic droit et sélectionner Refactor > Rename (Refactoriser > Renommer). 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 :

// TODO: Convert ShrineApp to stateful widget (104)
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 'colors.dart';
import 'home.dart';
import 'login.dart';
import 'category_menu_page.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.

      home: 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: Text('SHRINE'),
        backTitle: Text('MENU'),
      ),

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

Android

iOS

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

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

import 'package:flutter/material.dart';

import 'model/products_repository.dart';
import 'model/product.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});

  @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

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() 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);
    }
  }

Actualisez la page à chaud, puis 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
    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.

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

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

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

  const _BackdropTitle({
    Key key,
    Listenable listenable,
    this.onPress,
    @required this.frontTitle,
    @required this.backTitle,
  })  : assert(frontTitle != null),
        assert(backTitle != null),
        super(key: key, listenable: listenable);

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

    return DefaultTextStyle(
      style: Theme.of(context).primaryTextTheme.headline6,
      softWrap: false,
      overflow: TextOverflow.ellipsis,
      child: Row(children: <Widget>[
        // branded icon
        SizedBox(
          width: 72.0,
          child: IconButton(
            padding: EdgeInsets.only(right: 8.0),
            onPressed: this.onPress,
            icon: Stack(children: <Widget>[
              Opacity(
                opacity: animation.value,
                child: ImageIcon(AssetImage('assets/slanted_menu.png')),
              ),
              FractionalTranslation(
                translation: Tween<Offset>(
                  begin: Offset.zero,
                  end: Offset(1.0, 0.0),
                ).evaluate(animation),
                child: 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: Interval(0.5, 1.0),
              ).value,
              child: FractionalTranslation(
                translation: Tween<Offset>(
                  begin: Offset.zero,
                  end: Offset(0.5, 0.0),
                ).evaluate(animation),
                child: backTitle,
              ),
            ),
            Opacity(
              opacity: CurvedAnimation(
                parent: animation,
                curve: Interval(0.5, 1.0),
              ).value,
              child: FractionalTranslation(
                translation: Tween<Offset>(
                  begin: 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, à 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 s'effectue sans modifier les autres paramètres, comme 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: Icon(
            Icons.search,
            semanticLabel: 'login', // New code
          ),
          onPressed: () {
            // TODO: Add open login (104)
            Navigator.push(
              context,
              MaterialPageRoute(builder: (BuildContext context) => LoginPage()),
            );
          },
        ),
        IconButton(
          icon: 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.

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 d'autres composants de MDC-Flutter, consultez le catalogue des widgets Flutter.

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.

Pour savoir comment connecter une application à Firebase pour un backend opérationnel, consultez l'atelier de programmation Firebase pour Flutter.

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