MDC-104 Flutter: Componentes avanzados de Material

1. Introducción

logo_components_color_2x_web_96dp.png

Los componentes de Material (MDC) ayudan a los desarrolladores a implementar Material Design. MDC, creado por un equipo de ingenieros y diseñadores de UX en Google, cuenta con decenas de componentes de IU atractivos y funcionales, y está disponible para Android, iOS, la Web y Flutter.material.io/develop.

En el codelab MDC-103, personalizaste el color, la elevación, la tipografía y la forma de los componentes de Material (MDC) para crear el estilo de tu app.

Los componentes del sistema de Material Design realizan un conjunto de tareas predefinidas y tienen determinadas características, como botones. Sin embargo, un botón representa más que una forma de realizar acciones solicitadas por el usuario; también es una expresión visual de forma, tamaño y color que le permite al usuario entender su interactividad y que sucederá algo cuando lo toque o haga clic en él.

En los lineamientos de Material Design se describen los componentes desde el punto de vista de un diseñador. Además, se detallan una gran variedad de funciones básicas que están disponibles en diferentes plataformas, como los elementos anatómicos que conforman cada componente. Por ejemplo, un fondo contiene una capa posterior y su contenido, la capa frontal y su contenido, reglas de movimiento y opciones de visualización. Todos estos componentes se pueden personalizar según las necesidades de cada app, caso de uso y contenido.

Qué compilarás

En este codelab, cambiarás la IU de la app de Shrine a una presentación de dos niveles, que se denomina "fondo". El fondo incluye un menú en el que se detallan categorías que se pueden seleccionar para filtrar los productos que se muestran en la cuadrícula asimétrica. En este codelab, utilizarás los siguientes componentes de Flutter:

  • Forma
  • Movimiento
  • Widgets de Flutter (que ya usaste en los codelabs anteriores)

Android

iOS

app de comercio electrónico (con tema café y rosa) con una barra superior de la app y una cuadrícula asimétrica llena de productos por la que es posible desplazarse horizontalmente

app de comercio electrónico (con tema café y rosa) con una barra superior de la app y una cuadrícula asimétrica llena de productos por la que es posible desplazarse horizontalmente

menú en el que se enumeran 4 categorías

menú en el que se enumeran 4 categorías

Componentes y subsistemas de MDC-Flutter de este codelab

  • Forma

¿Cómo calificarías tu nivel de experiencia con el desarrollo de Flutter?

Principiante Intermedio Avanzado

2. Configura tu entorno de desarrollo de Flutter

Para completar este lab, necesitas dos programas de software: el SDK de Flutter y un editor.

Puedes ejecutar el codelab con cualquiera de estos dispositivos o modalidades:

  • Un dispositivo físico Android o iOS conectado a tu computadora y configurado en el Modo de desarrollador
  • El simulador de iOS (requiere instalar las herramientas de Xcode)
  • Android Emulator (requiere configuración en Android Studio)
  • Un navegador (se requiere Chrome para la depuración)
  • Como una aplicación para computadoras que ejecuten Windows, Linux o macOS (debes desarrollarla en la plataforma donde tengas pensado realizar la implementación; por lo tanto, si quieres desarrollar una app de escritorio para Windows, debes desarrollarla en ese SO a fin de obtener acceso a la cadena de compilación correcta; encuentra detalles sobre los requisitos específicos del sistema operativo en docs.flutter.dev/desktop).

3. Descarga la app de partida del codelab

¿Vienes de MDC-103?

Si completaste MDC-103, tu código debería estar listo para este codelab. Ve al paso Agrega el menú de fondo.

¿Empiezas de cero?

La app de inicio se encuentra en el directorio material-components-flutter-codelabs-104-starter_and_103-complete/mdc_100_series.

… o clónalo desde GitHub

Para clonar este codelab desde GitHub, ejecuta los siguientes comandos:

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

Abre el proyecto y ejecuta la app

  1. Abre el proyecto en el editor que prefieras.
  2. Sigue las instrucciones para “ejecutar la app” en Get Started: Test drive en el editor que elegiste.

Listo. Deberías ver la página de acceso de Shrine de los codelabs anteriores en tu dispositivo.

Android

iOS

Página de acceso a Shrine

Página de acceso a Shrine

4. Agrega el menú de fondo

Aparecerá un fondo detrás del resto de los componentes y del contenido. Está compuesto de dos capas: una posterior (que muestra acciones y filtros) y una frontal (que muestra contenido). Puedes usar un fondo para mostrar información y acciones interactivas, como filtros de contenido o navegación.

Quita la barra principal de la aplicación

El widget HomePage será el contenido de nuestra capa frontal. En este momento, tiene una barra de la aplicación. Moverás la barra de la aplicación a la capa posterior y HomePage solo incluirá la AsymmetricView.

En home.dart, cambia la función build() para que solo muestre una AsymmetricView.

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

Agrega el widget Backdrop

Crea un widget llamado Backdrop que incluya frontLayer y backLayer.

La backLayer contiene un menú que te permite seleccionar una categoría para filtrar la lista (currentCategory). Dado que deseamos que la selección del menú persista, harás que Backdrop sea un widget con estado.

Agrega a /lib un nuevo archivo llamado backdrop.dart, de la siguiente manera:

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)

Ten en cuenta que marcamos algunas propiedades required. Esta es una práctica recomendada para las propiedades en el constructor que no tienen un valor predeterminado y no pueden ser null; por lo tanto, no debes olvidarlas.

En la definición de clase de Backdrop, agrega la clase _BackdropState, como se indica a continuación:

// 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 función build() muestra un elemento Scaffold con una barra de la aplicación de la misma manera que lo hacía HomePage. Sin embargo, el cuerpo de Scaffold es Stack (una pila). Los elementos secundarios de una pila se pueden superponer. El tamaño y la ubicación de cada elemento secundario se especifican en relación con el elemento superior de la pila.

Ahora, agrega una instancia de Backdrop a ShrineApp.

En app.dart, importa backdrop.dart y model/product.dart, como se muestra a continuación:

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';

En app.dart,, muestra un Backdrop que tenga HomePage como su frontLayer para modificar la ruta /, de la siguiente manera:

// 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'),
),

Guarda el proyecto, se debería ver la página principal y la barra de la app:

Android

iOS

Página de productos de Shrine con fondo rosa

Página de productos de Shrine con fondo rosa

La backLayer muestra el área rosa en una capa nueva detrás de la página principal de la frontLayer.

Para verificar que la pila tenga un contenedor detrás de una HomePage, puedes usar Flutter Inspector. Debería tener un aspecto similar al siguiente:

4783ed30f1cc010.png

Ahora puedes ajustar el diseño y el contenido de las dos capas.

5. Agrega una forma

En este paso, crearás un estilo para la capa frontal con un corte en la esquina superior izquierda.

En Material Design, este tipo de personalización se conoce como una forma. Las superficies de Material pueden tener formas arbitrarias. Estas formas agregan énfasis y estilo a las superficies y se pueden usar para expresar la marca. Las formas rectangulares comunes se pueden personalizar para que tengan esquinas y bordes curvos o angulados, y la cantidad de lados que quieras. Además, pueden ser simétricas o irregulares.

Agrega una forma a la capa frontal

El logotipo angulado de Shrine fue la inspiración para la historia de formas de la app. La historia de formas es el uso común de formas que se aplican en toda una app. Por ejemplo, la forma del logotipo se repite en los elementos con forma de la página de acceso. En este paso, crearás un estilo para la capa frontal con un corte angular en la esquina superior izquierda.

En backdrop.dart, agrega una clase _FrontLayer nueva de la siguiente manera:

// 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,
          ),
        ],
      ),
    );
  }
}

Luego, en la función _buildStack() de _BackdropState, une la capa frontal con una _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),
      ],
    );
  }

Vuelve a cargar la página.

Android

iOS

Página de productos de Shrine con una forma personalizada

Página de productos de Shrine con una forma personalizada

La superficie principal de Shrine tiene una forma personalizada. Debido a la elevación de la superficie, los usuarios pueden ver que hay algo detrás de la capa blanca frontal. A continuación, agregarás movimiento para que los usuarios puedan ver la capa posterior del fondo.

6. Agrega movimiento

El movimiento permite darle vida a tu app. Puede ser amplio y dramático, sutil y mínimo, o bien un efecto intermedio. Sin embargo, debes tener en cuenta que el tipo de movimiento que uses debe ser adecuado para la situación. El movimiento que se implementa en las acciones regulares y repetidas debería ser mínimo y sutil para que las acciones no distraigan al usuario o tarden demasiado cada vez que se ejecutan. No obstante, algunas situaciones son propicias para usar animaciones más llamativas, como la primera vez que el usuario abre una app. Además, algunas animaciones pueden enseñarle al usuario cómo usar la app.

Agrega movimiento de revelación al botón de menú

En la parte superior de backdrop.dart, fuera del alcance de cualquier clase o función, agrega una constante que represente la velocidad que quieres darle a la animación.

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

Agrega un widget AnimationController a _BackdropState, crea una instancia de él en la función initState() y bórralo en la función dispose() del estado.

  // 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)

El AnimationController coordina animaciones y te proporciona una API para reproducir, revertir y detener la animación. Ahora necesitas funciones que le den movimiento.

Agrega funciones que determinen y cambien la visibilidad de la capa frontal.

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

Une la backLayer con un widget ExcludeSemantics, que excluirá los elementos del menú de la backLayer del árbol de semántica cuando la capa posterior no sea visible.

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

Cambia la función _buildStack() para que tome un BuildContext y BoxConstraints. Además, incluye una PositionedTransition que tome animación RelativeRectTween de la siguiente manera:

  // 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,
          ),
        ),
      ],
    );
  }

Por último, en lugar de llamar a la función _buildStack para el cuerpo de Scaffold, muestra un widget LayoutBuilder que use _buildStack como compilador.

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

Se retrasó la compilación de la pila de la capa frontal/posterior hasta el tiempo de diseño con LayoutBuilder, de modo que puedas incorporar la altura general real del fondo. LayoutBuilder es un widget especial cuya devolución de llamada del compilador genera restricciones de tamaño.

En la función build(), convierte el ícono de menú inicial de la barra de la aplicación en un IconButton y úsalo para activar o desactivar la visibilidad de la capa frontal cuando se presiona el botón.

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

Vuelve a cargar la página y presiona el botón del menú en el simulador.

Android

iOS

Menú de Shrine vacío con dos errores

Menú de Shrine vacío con dos errores

La capa frontal tiene una animación (se desliza) hacia abajo. Sin embargo, si observas la parte inferior, verás un error rojo y un error de desbordamiento. Esto se debe a que se aprieta la AsymmetricView y se vuelve más pequeña con esta animación, que, a su vez, deja menos espacio para los elementos Column. En algún momento, las columnas no se pueden implementar con el poco espacio que tienen, y se genera un error. Si reemplazas Columns por ListViews, el tamaño de las columnas debería permanecer como la animación.

Une columnas de productos en una ListView

En supplemental/product_columns.dart, reemplaza Column en OneProductCardColumn por una 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,
        ),
      ],
    );
  }
}

El elemento Column incluye MainAxisAlignment.end. Para iniciar el diseño desde la parte inferior, marca la opción reverse: true. Se revertirá el orden de los elementos secundarios para compensar el cambio.

Vuelve a cargar la página y presiona el botón de menú.

Android

iOS

Menú de Shrine vacío con un error

Menú de Shrine vacío con un error

Ya no aparece la advertencia gris de desbordamiento en OneProductCardColumn. Ahora, debes corregir la otra.

En supplemental/product_columns.dart, cambia la manera en que se calcula imageAspectRatio y reemplaza la columna en TwoProductCardColumn por una 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,
            ),
          ),
        ],
      );

También se agregó un poco de seguridad a imageAspectRatio.

Vuelve a cargar la página. Luego, presiona el botón de menú.

Android

iOS

Menú de Shrine vacío

Menú de Shrine vacío

Ya no hay desbordamientos.

7. Agrega un menú en la capa posterior

Un menú es una lista de elementos de texto que se pueden presionar y que notifica a los objetos de escucha cuando se tocan los elementos de texto. En este paso, agregarás un menú de filtrado de categorías.

Agrega el menú

Agrega el menú a la capa frontal y los botones interactivos a la posterior.

Crea un archivo nuevo llamado lib/category_menu_page.dart de la siguiente manera:

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

Es un GestureDetector que ajusta una Column cuyos elementos secundarios son los nombres de las categorías. Para indicar la categoría seleccionada, se usa un subrayado.

En app.dart, convierte el widget de ShrineApp para que pase del modo con estado a uno sin estado.

  1. Destaca ShrineApp..
  2. En función del IDE, muestra las siguientes acciones de código:
  3. Android Studio: Presiona ⌥Intro (en macOS) o alt + Intro
  4. VS Code: Presiona ⌘. (en macOS) o Ctrl+.
  5. Selecciona "Convert to StatefulWidget".
  6. Convierte la clase ShrineAppState en privada (_ShrineAppState). Haz clic con el botón derecho en ShrineAppState y
  7. Android Studio: selecciona Refactor > Rename.
  8. VS Code: selecciona el símbolo para cambiar el nombre.
  9. Ingresa _ShrineAppState para convertir la clase en privada.

En app.dart, agrega una variable a _ShrineAppState para la categoría seleccionada y una devolución de llamada cuando se presiona.

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

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

Luego, cambia la capa posterior a una CategoryMenuPage.

En app.dart, importa la CategoryMenuPage de la siguiente manera:

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';

En la función build(), cambia el campo de backLayer a CategoryMenuPage y el campo de currentCategory para tomar la variable de la instancia.

'/': (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'),
            ),

Vuelve a cargar la página y presiona el botón de menú.

Android

iOS

Menú de Shrine con 4 categorías

Menú de Shrine con 4 categorías

Si presionas una opción del menú, aún no sucederá nada. Sin embargo, podemos solucionarlo.

En home.dart, agrega una variable para Category y pásala a 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),
    );
  }
}

En app.dart, pasa la _currentCategory para frontLayer.

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

Vuelve a cargar la página. Presiona el botón de menú en el simulador y selecciona una categoría.

Android

iOS

Página de productos filtrados de Shrine

Página de productos filtrados de Shrine

Presiona el ícono de menú para ver los productos. ¡Se filtraron!

Cierra la capa frontal después de una selección del menú

En backdrop.dart, agrega una anulación para la función didUpdateWidget() (ejecutada cada vez que cambia la configuración del widget) en _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);
    }
  }

Guarda tu proyecto para activar una recarga en caliente. Presiona el ícono de menú y selecciona una categoría. Se debería cerrar automáticamente el menú, y deberías ver la categoría de elementos elegida. Ahora, también agregarás esa funcionalidad a la capa frontal.

Activa o desactiva la capa frontal

En backdrop.dart, agrega una devolución de llamada a la capa de fondo para la acción de presionar.

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;

Luego, agrega un GestureDetector al elemento secundario de _FrontLayer: los elementos secundarios de Column.

      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,
          ),
        ],
      ),

Luego, implementa la nueva propiedad onTap en el _BackdropState, en la función _buildStack().

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

Vuelve a cargar y presiona la parte superior de la capa frontal. Se debería abrir y cerrar la capa cada vez que presionas la parte superior de la capa frontal.

8. Agrega un ícono de marca

La iconografía de la marca también se extiende a los íconos conocidos. Personalizarás el ícono de revelación y lo combinarás con el título para crear una apariencia de marca única.

Cambia el ícono del botón de menú

Android

iOS

Página de productos de Shrine con un ícono de marca

Página de productos de Shrine con un ícono de marca

En backdrop.dart, crea una clase _BackdropTitle nueva.

// 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,
              ),
            ),
          ],
        )
      ]),
    );
  }
}

_BackdropTitle es un widget personalizado que reemplazará el widget Text simple para el parámetro title del widget AppBar. Tiene un ícono de menú animado y transiciones animadas entre el título frontal y el posterior. El ícono animado del menú usará un elemento nuevo. Se debe agregar la referencia al nuevo archivo slanted_menu.png a pubspec.yaml.

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

Quita la propiedad leading del compilador AppBar. Esta acción es necesaria para que se renderice el ícono personalizado de la marca en el lugar original del widget leading. El listenable de la animación y el controlador de onPress para el ícono de la marca se pasan al _BackdropTitle. También se pasan los elementos frontTitle y backTitle para que se puedan renderizar en el título del fondo. El parámetro title de la AppBar se debería ver de esta forma:

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

Se crea el ícono de marca en el _BackdropTitle. Contiene una Stack de íconos animados: un menú inclinado y un diamante, que se une a un IconButton para que se pueda presionar. Luego, se une el IconButton a un SizedBox a fin de dejar espacio para el movimiento horizontal del ícono.

La arquitectura de Flutter, en la que "todo es un widget", permite que se pueda modificar el diseño de la AppBar predeterminada sin necesidad de crear un nuevo widget AppBar personalizado. El parámetro title, que es originalmente un widget Text, se puede reemplazar por un _BackdropTitle más complejo. Dado que el _BackdropTitle también incluye el Ícono personalizado, toma el lugar de la propiedad leading, que ahora se puede omitir. Esta sustitución simple de widgets se lleva a cabo sin tener que cambiar ninguno de los otros parámetros, como los íconos de acciones (que siguen funcionando por su cuenta).

Agrega un acceso directo para volver a la pantalla de acceso

En backdrop.dart,agrega un acceso directo para volver a la pantalla de acceso en los dos íconos finales de la barra de la aplicación. Cambia las etiquetas semánticas de los íconos para que reflejen el propósito nuevo.

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

Obtendrás un error si tratas de volver a cargar la pantalla. Importa login.dart para corregir el error.

import 'login.dart';

Vuelve a cargar la app y presiona los botones de búsqueda o de ajuste a fin de volver a la pantalla de acceso.

9. ¡Felicitaciones!

En el transcurso de estos cuatro codelabs, viste cómo usar los componentes de Material para compilar experiencias del usuario elegantes y únicas que expresen la personalidad y el estilo de la marca.

Próximos pasos

Este codelab, MDC-104, completa la secuencia de codelabs actual. Puedes visitar el catálogo de widgets de Componentes de Material para explorar aún más componentes.

Para ampliar el objetivo, trata de reemplazar el ícono de marca con un AnimatedIcon que tenga una animación entre dos íconos cuando el fondo está visible.

En función de tus intereses, hay muchos otros codelabs de Flutter para que los pruebes. Tenemos otro codelab específico de Material que podría interesarte. Se trata de Cómo compilar transiciones atractivas con el sistema de movimiento de Material para Flutter.

Pude completar este codelab con una cantidad de tiempo y esfuerzo razonables.

Totalmente de acuerdo De acuerdo Neutral En desacuerdo Totalmente en desacuerdo

Me gustaría seguir usando los componentes de Material en el futuro.

Totalmente de acuerdo De acuerdo Neutral En desacuerdo Totalmente en desacuerdo