MDC-104 Flutter: Componentes avanzados de Material (Flutter)

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. En su mayoría, estas partes son funciones, controles y vistas tradicionales del SDK de tu plataforma.

Si bien en los lineamientos de Material Design se mencionan muchos componentes, no todos son candidatos apropiados para el código reutilizable y, por lo tanto, no los encontrarás en MDC. Puedes crear estas experiencias por tu cuenta a fin de obtener un estilo personalizado para tu app, usando código tradicional.

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

Componente de MDC-Flutter de este codelab

  • Forma

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

Principiante Intermedio Avanzado

Antes de comenzar

A fin de comenzar a desarrollar apps para dispositivos móviles con Flutter, sigue estos pasos:

  1. Descarga e instala el SDK de Flutter.
  2. Actualiza tu PATH con el SDK de Flutter.
  3. Instala Android Studio con los complementos de Flutter y Dart, o tu editor favorito.
  4. Instala Android Emulator o un simulador de iOS (requiere una Mac con Xcode), o usa un dispositivo físico.

Para obtener más información sobre la instalación de Flutter, consulta Cómo comenzar: Instalación. Para configurar un editor, consulta Cómo comenzar: Configura un editor. Cuando instales Android Emulator, podrás usar las opciones predeterminadas, como un teléfono Pixel 3 con la imagen del sistema más reciente. Se recomienda, pero no es necesario, habilitar la aceleración de VM. Una vez que hayas completado los 4 pasos anteriores, podrás volver al codelab. Para completar este codelab, solo debes instalar Flutter en una plataforma (Android o iOS).

Asegúrate de que el SDK de Flutter esté en el estado correcto

Antes de continuar con este codelab, asegúrate de que el SDK esté en el estado correcto. Si el SDK de Flutter se instaló anteriormente, usa flutter upgrade para asegurarte de que el SDK esté en el estado más reciente.

 flutter upgrade

Si ejecutas flutter upgrade, se ejecutará automáticamente flutter doctor.. Si es una instalación nueva de Flutter y no necesitas actualizar nada, ejecuta flutter doctor de forma manual. Informará si hay dependencias que necesitas instalar para completar la configuración. Puedes ignorar las marcas de verificación que no sean relevantes para ti (por ejemplo, Xcode si no deseas desarrollar para iOS).

 flutter doctor

Preguntas frecuentes

¿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?

Descargar app de inicio

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

Configura tu proyecto

En las siguientes instrucciones, se da por sentado que usas Android Studio (IntelliJ).

Cómo abrir el proyecto

1. Abre Android Studio.

2. Si ves la pantalla de bienvenida, haz clic en Open an existing Android Studio project.

3. Navega al directorio material-components-flutter-codelabs/mdc_100_series y haz clic en "Open". Se debería abrir el proyecto. Puedes ignorar cualquier error que veas en Dart Analysis hasta que hayas compilado el proyecto una vez.

4. Si se te solicita, haz lo siguiente:

  • Instala cualquier actualización de plataforma y complemento o FlutterRunConfigurationType.
  • Si el SDK de Dart o Flutter no está configurado, establece la ruta del SDK de Flutter para el complemento de Flutter.
  • Configura los frameworks de Android.
  • Haz clic en "Get dependencies" o "Run 'flutter packages get'".

Luego, reinicia Android Studio.

Cómo ejecutar la app de inicio

En las siguientes instrucciones, se da por sentado que realizas la prueba en Android Emulator o un dispositivo Android, pero también puedes realizarla en un dispositivo o simulador de iOS si tienes instalado Xcode.

1. Selecciona el dispositivo o emulador. Si Android Emulator aún no se está ejecutando, selecciona Tools -> Android -> AVD Manager para crear un dispositivo virtual e iniciar el emulador. Si ya existe un AVD, puedes iniciar el emulador directamente desde el selector de dispositivos de Android Studio, como se muestra en el siguiente paso. (Para el simulador de iOS, si aún no se está ejecutando, selecciona Flutter Device Selection -> Open iOS Simulator para iniciarlo en la máquina de desarrollo).

2. Inicia tu app de Flutter:

  • Busca el menú desplegable "Flutter Device Selection" en la parte superior de la pantalla del editor y selecciona el dispositivo (por ejemplo, iPhone SE o Android SDK built for <versión>).
  • Presiona el ícono Reproducir ().

Listo. Deberías ver la página de acceso a Shrine de los codelabs anteriores en el simulador o el emulador.

Android

iOS

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.

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)

Se importa el paquete meta para marcar las propiedades que son @required. Esta es una práctica recomendada cuando tienes propiedades en el constructor que no tienen un valor predeterminado y no pueden ser null; por lo tanto, no debes olvidarlas. Ten en cuenta que también hay aserciones después del constructor que verifican que los valores pasados en esos campos no sean null.

En la definición de clase de Backdrop, agrega la clase _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 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,, modifica la función build() de ShrineApp. Cambia home: a un fondo que tenga una HomePage como su frontLayer.

      // 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 presionas el botón de reproducción, se debería ver la página principal y la barra de la aplicación:

Android

iOS

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:

ad988a22875b5e82.png

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

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

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

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.

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

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: 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: Icon(Icons.menu),
        onPressed: _toggleBackdropLayerVisibility,
      ),

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

Android

iOS

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

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

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

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

Ya no hay desbordamientos.

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

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. Presiona Alt (Option) + Intro.
  3. Selecciona "Convert to StatefulWidget".
  4. Convierte la clase ShrineAppState en privada (_ShrineAppState). Para hacer esto desde el menú principal del IDE, selecciona Refactor > Rename. De manera alternativa, puedes destacar el nombre de la clase (ShrineAppState) desde el código, hacer clic con el botón derecho en ella y seleccionar Refactor > Rename. 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.

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

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

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

Android

iOS

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

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

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

Vuelve a cargar en caliente, presiona el ícono del 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
    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.

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

En backdrop.dart, crea una clase _BackdropTitle nueva.

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

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

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.

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. Si visitas el catálogo de widgets de Flutter, podrás explorar aún más componentes de MDC-Flutter.

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.

A fin de aprender a conectar una app a Firebase para obtener un backend que funcione, consulta el codelab Firebase 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