Crear una experiencia de usuario excepcional es esencial para el éxito de su aplicación. No importa qué tan bueno sea el producto que venda, si su aplicación no ofrece una UX positiva, tendrá dificultades. En este tutorial, aprenderá cómo crear una galería de productos intuitiva con Flutter y brindar una mejor experiencia de compra.
Crearemos una aplicación de muestra que enumera los productos de una manera atractiva: al tocar un producto, animaremos una página de detalles del producto donde puede establecer su cantidad y agregarlo al carrito. En la parte superior también verá la cantidad de artículos agregados al carrito, y cuando haga clic en él, podrá ver una lista de todos los productos que se agregaron al mismo.
Al final de este tutorial, aprenderá cómo crear pantallas más grandes, permitir la búsqueda de productos, alternar la vista, administrar el estado del carrito y agregar movimiento de material mientras navega entre pantallas para una experiencia de usuario fluida.
Así es como se verá nuestra galería de productos de comercio electrónico de Flutter cuando esté completa:
Esto es lo que cubriremos en este tutorial:
Antes de comenzar, debe instalar Flutter SDK y crear un proyecto básico. Veamos cómo hacer esto.
Primero, obtén el SDK de Flutter desde este enlace. Una vez instalado, comprueba si todo está bien pulsando el siguiente comando en la terminal:
flutter doctor
(Nota: si algo está marcado con una marca roja, debe cuidarlo antes de continuar)
Ahora cree un directorio donde desee crear este proyecto e ingrese el siguiente comando. Esto creará un nuevo proyecto en el directorio especificado.
flutter create ecomm_app
Para abrir, modificar el contenido del archivo y ejecutar/depurar/probar el proyecto, necesita instalar el IDE. Puede elegir uno de los siguientes editores:
Abre el carpetadelproyecto/lib/main.dart y ejecute la aplicación de arranque desde el IDE presionando el botón de reproducción. También puede ejecutarlo desde la línea de comando ingresando el siguiente comando:
flutter run // Tip: If you are running multiple devices, you can run the // following: flutter run -d "<device_name>"
La aplicación de muestra consta de tres pantallas: la lista de productos, los detalles del producto y la página del carrito de compras.
Veamos cómo encajan estas pantallas en el flujo:
Primero, verás todos los productos. Toque cualquier elemento para abrir la página de detalles. Puede agregar artículos al carrito de compras e ir a la página del carrito de compras.
La primera pantalla muestra todos los productos con su nombre, imagen y precio. Aquí puede buscar productos y cambiar la vista entre vista de cuadrícula y vista de lista. Al tocar el artículo, se abre la página de detalles del producto.
Estos son algunos de los widgets esenciales que puede usar para crear la primera pantalla:
Aquí está el código exacto para crear este contenedor:
Container( //width: MediaQuery.of(context).size.width * 0.45, decoration: BoxDecoration( color: AppTheme.of(context).secondaryBackground, boxShadow: [ BoxShadow( blurRadius: 4, color: Color(0x3600000F), offset: Offset(0, 2), ) ], borderRadius: BorderRadius.circular(8), ), child: Padding( padding: EdgeInsetsDirectional.fromSTEB(0, 0, 0, 12), child: Column( mainAxisSize: MainAxisSize.max, children: [ Row( mainAxisSize: MainAxisSize.max, children: [ Expanded( child: ClipRRect( borderRadius: BorderRadius.only( bottomLeft: Radius.circular(0), bottomRight: Radius.circular(0), topLeft: Radius.circular(8), topRight: Radius.circular(8), ), child: Image.network( product.image, width: 100, height: 100, fit: BoxFit.cover, ), ), ), ], ), Padding( padding: EdgeInsetsDirectional.fromSTEB(0, 4, 0, 0), child: Row( mainAxisSize: MainAxisSize.max, children: [ Padding( padding: EdgeInsetsDirectional.fromSTEB(8, 4, 0, 0), child: Text( product.name, style: AppTheme.of(context).bodyText1, ), ), ], ), ), Padding( padding: EdgeInsetsDirectional.fromSTEB(0, 2, 0, 0), child: Row( mainAxisSize: MainAxisSize.max, children: [ Padding( padding: EdgeInsetsDirectional.fromSTEB(8, 4, 0, 0), child: Text( '\$${product.price}', style: AppTheme.of(context).bodyText2, ), ), ], ), ), ], ), ), );
La página de detalles del producto muestra información del producto. Le permite establecer la cantidad del producto y agregarlo al carrito. También puede abrir el carrito de compras desde esta página.
Estos son algunos de los widgets esenciales que puede usar para crear la pantalla de detalles del producto:
El widget de insignia no es el widget estándar; en cambio, se agrega desde una biblioteca llamada Insignias. Esto anima automáticamente la insignia cuando se actualiza el valor.
Aquí está el código que muestra la insignia sobre el artículo del carrito:
Badge( badgeContent: Text( '${cartItem.length}', style: AppTheme.of(context).bodyText1.override( fontFamily: 'Poppins', color: Colors.white, ), ), showBadge: true, shape: BadgeShape.circle, badgeColor: AppTheme.of(context).primaryColor, elevation: 4, padding: EdgeInsetsDirectional.fromSTEB(8, 8, 8, 8), position: BadgePosition.topEnd(), animationType: BadgeAnimationType.scale, toAnimate: true, child: IconButton( icon: Icon( Icons.shopping_cart_outlined, color: AppTheme.of(context).secondaryText, size: 30, ), onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => CheckoutWidget(), ), ); }, ), )
Esta página muestra la lista de todos los artículos agregados al carrito con la opción de eliminar cualquier artículo del carrito. Aquí puede ver todos los resúmenes de precios y una opción de pago.
Estos son algunos de los widgets importantes que puede usar para crear la pantalla del carrito de compras:
Aquí se explica cómo mostrar una lista de todos los artículos en un carrito:
ListView.builder( padding: EdgeInsets.zero, primary: false, shrinkWrap: true, scrollDirection: Axis.vertical, itemCount: cartItems.length, itemBuilder: (BuildContext context, int index) { return Padding( padding: EdgeInsetsDirectional.fromSTEB(16, 8, 16, 0), child: Container( width: double.infinity, height: 100, decoration: BoxDecoration( color: AppTheme.of(context).secondaryBackground, boxShadow: [ BoxShadow( blurRadius: 4, color: Color(0x320E151B), offset: Offset(0, 1), ) ], borderRadius: BorderRadius.circular(12), ), child: Padding( padding: EdgeInsetsDirectional.fromSTEB(16, 8, 8, 8), child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Hero( tag: 'ControllerImage', transitionOnUserGestures: true, child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.network( cartItems[index].image, width: 80, height: 80, fit: BoxFit.fitWidth, ), ), ), Padding( padding: EdgeInsetsDirectional.fromSTEB(12, 0, 0, 0), child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsetsDirectional.fromSTEB(0, 0, 0, 8), child: Text( cartItems[index].name, style: AppTheme.of(context).subtitle2.override( fontFamily: 'Poppins', color: AppTheme.of(context).primaryText, ), ), ), Text( '\$${cartItems[index].price}', style: AppTheme.of(context).bodyText2, ), Padding( padding: EdgeInsetsDirectional.fromSTEB(0, 8, 0, 0), child: Text( 'Quanity: ${cartItems[index].quantity}', style: AppTheme.of(context).bodyText2, ), ), ], ), ), IconButton( icon: Icon( Icons.delete_outline_rounded, color: Color(0xFFE86969), size: 20, ), onPressed: () { // Remove item }, ), ], ), ), ), ); });
Una vez que la interfaz de usuario esté lista, puede completar la lista de productos agregando varios productos. En un escenario real, completaría esta lista con elementos obtenidos de su backend, pero agregaremos productos localmente en una variable para simplificar.
Primero, cree una clase de producto que contenga campos como id, nombre, imagen, precio y cantidad.
class Product { final int id; final String name; final String image; final double price; int quantity; Product({required this.id, required this.name, required this.image, required this.price, this.quantity = 0}); }
Ahora cree una lista de varios productos usando la clase anterior. Así:
final List<Product> products = [ Product( id: 1, name: 'Champion', image: 'https://images.unsplash.com/photo-1606107557195-0e29a4b5b4aa?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=764&q=80', price: 55.5), Product( id: 2, name: 'Stark', image: 'https://images.unsplash.com/photo-1549298916-b41d501d3772?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1624&q=80', price: 65.5), ];
Así mismo, puedes añadir más productos si lo deseas. Una vez que esta lista esté lista, es hora de continuar y usarla para inflar el widget GridView, como se muestra a continuación:
GridView.builder( itemCount: products.length, itemBuilder: (context, index) => ProductTile( itemNo: index, product: products[index], ), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 1, ), )
La funcionalidad central de cualquier aplicación de comercio electrónico es la capacidad de agregar productos al carrito para comprarlos. Para crear esta funcionalidad, puede integrar la gestión de estado en su aplicación.
Para esta aplicación, usaremos la técnica de administración de estado llamada Bloque porque puede separar la lógica comercial de la interfaz de usuario y es más fácil escribir y reutilizar pruebas.
Aquí hemos explicado cómo usar el patrón de diseño de bloque en una publicación de blog.
La gestión del estado del bloque requiere la adición de tres clases esenciales: bloque, evento y estado. Para agregar o eliminar artículos del carrito, agregaremos las siguientes clases:
CarritoBloque
Aquí es donde reside la lógica empresarial (agregar y eliminar elementos).
class CartBloc extends Bloc<CartEvent, CartState> { CartBloc() : super(ProductAdded(cartItem: [])); final List<Product> _cartItems = []; List<Product> get items => _cartItems; bool isGridView = true; @override Stream<CartState> mapEventToState(CartEvent event) async* { if (event is AddProduct) { _cartItems.add(event.productIndex); yield ProductAdded(cartItem: _cartItems); } else if (event is RemoveProduct) { _cartItems.remove(event.productIndex); yield ProductRemoved(cartItem: _cartItems); } else if (event is ChangeGallaryView) { isGridView = event.isGridView; yield ChangeGallaryViewState(isGridView: isGridView); } } }
(Nota: _cartItems es una fuente única de información para administrar artículos del carrito)
CarritoEvento
Esto se utiliza para enviar artículos al bloque del carrito.
abstract class CartEvent extends Equatable { const CartEvent(); @override List<Object> get props => []; } class AddProduct extends CartEvent { final Product productIndex; const AddProduct(this.productIndex); @override List<Object> get props => [productIndex]; @override String toString() => 'AddProduct { index: $productIndex }'; } class RemoveProduct extends CartEvent { final Product productIndex; const RemoveProduct(this.productIndex); @override List<Object> get props => [productIndex]; @override String toString() => 'RemoveProduct { index: $productIndex }'; }
Estado del carrito
Esto se utiliza para enviar elementos a la interfaz de usuario.
abstract class CartState { final List<Product> cartItem; final bool isGridView; const CartState({this.cartItem = const [], this.isGridView = true}); @override List<Object> get props => []; } class CartLoadInProgress extends CartState { CartLoadInProgress({required super.cartItem}); } class ProductAdded extends CartState { final List<Product> cartItem; const ProductAdded({required this.cartItem}) : super(cartItem: cartItem); @override List<Object> get props => [cartItem]; @override String toString() => 'ProductAdded { todos: $cartItem }'; }
Desde la interfaz de usuario (un botón con el texto «Agregar al carrito»), puede insertar el siguiente código para agregar el artículo a la lista:
onPressed: () { Product p = widget.product; p.quantity = countControllerValue!.toInt(); BlocProvider.of<CartBloc>(context).add(AddProduct(p)); }
Para eliminar el producto del carrito, simplemente puede activar el evento para eliminar el artículo, así:
onPressed: () { BlocProvider.of<CartBloc>(context).add(RemoveProduct(cartItems[index])); }
Para recuperar artículos del carrito, puede envolver GridView o ListView en el generador de bloques, y la lista se actualiza cada vez que se agrega o elimina el artículo.
BlocBuilder<CartBloc, CartState>(builder: (_, cartState) { return ListView.builder(); }),
También puedes adaptar la versión web de tu aplicación. Esto significa que los usuarios no deberían sentir que están usando una aplicación móvil en un navegador; más bien debería dar la impresión de que es una aplicación web nativa.
Para esta aplicación, podemos mostrar más elementos cuando la aplicación se usa en una pantalla más grande. Es posible que se sorprenda al saber que esto se puede lograr con solo un pequeño cambio en el código.
Así es cómo:
return LayoutBuilder(builder: (context, constraints) { return GridView.builder( itemCount: products.length, itemBuilder: (context, index) => ProductTileAnimation( itemNo: index, product: products[index], ), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: constraints.maxWidth > 700 ? 4 : 2, //<-SEE HERE childAspectRatio: 1, ), ); });
Puede envolver el widget GridView en LayoutBuilder, que proporciona las restricciones utilizadas para determinar el ancho y el alto. Usando restricciones, podemos construir varias interfaces de usuario.
Para nuestro ejemplo, en el código anterior, cada vez que la resolución de la pantalla cambia a 700 o más de ancho, mostramos cuatro elementos en el eje transversal.
Así es como funciona:
A veces, es posible que desee permitir que los usuarios cambien la vista actual (es decir, GridView) y la muestren en ListView.
Para hacer esto, puede crear una variable booleana (probablemente dentro del bloque) y alternar su valor. Según esta variable, puede configurar dos widgets, GridView y ListView, y cambiar el icono.
Aquí se explica cómo cambiar el icono:
BlocBuilder<CartBloc, CartState>(builder: (_, cartState) { bool isGridView = cartState.isGridView; return IconButton( onPressed: () { BlocProvider.of<CartBloc>(context).add(ChangeGallaryView(!isGridView)); }, icon: !isGridView ? Icon(Icons.grid_on) : Icon(Icons.list)); })
Aquí está el código para elegir la lista a mostrar:
BlocBuilder<CartBloc, CartState>(builder: (_, cartState) { bool isGridView = cartState.isGridView; if (isGridView) { return LayoutBuilder(builder: (context, constraints) { return GridView.builder( itemCount: products.length, itemBuilder: (context, index) => ProductTile( itemNo: index, product: products[index], ), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, childAspectRatio: 1, ), ); }); } else { return ListView.builder( itemCount: products.length, itemBuilder: (BuildContext context, int index) { return ProductTile( itemNo: index, product: products[index], ); }); } });
Para permitir que los usuarios busquen en una lista, puede aprovechar el evento onChanged de TextFormField. Esto le da los últimos caracteres ingresados en TextFormField. Puede usarlo para filtrar la lista principal y luego proporcionar el resultado de la búsqueda en una nueva lista, como se muestra a continuación:
TextFormField( controller: textController, obscureText: false, onChanged: (_) => EasyDebounce.debounce( 'tFMemberController', Duration(milliseconds: 0), () { isSearchStarted = textController!.text.isNotEmpty && textController!.text.trim().length > 0; if (isSearchStarted) { print('${textController!.text.trim()}'); searchedProducts = products .where((item) => item.name.toLowerCase().contains(textController!.text.trim().toLowerCase())) .toList(); } setState(() {}); }, ), )
La variable isSearchStarted se utiliza para indicar si mostrar o no el resultado de la búsqueda.
ProductList( products: isSearchStarted ? searchedProducts : products, )
Es posible que desee agregar animaciones para mejorar la experiencia del usuario. En lugar de tener una transición de navegación predeterminada, puede agregar una animación que abre suavemente la página de detalles del producto cuando se toca.
Puede usar el conjunto de ajustes preestablecidos de animación del sistema de movimiento Material agregando la Biblioteca de animación a su aplicación.
Para hacer esto, envuelva su widget en OpenContainer y especifique la página que desea animar en el parámetro openBuilder.
Aquí está el código:
ContainerTransitionType _transitionType = ContainerTransitionType.fade; OpenContainer<bool>( transitionType: _transitionType, openBuilder: (BuildContext _, VoidCallback openContainer) { return ProductDetailWidget( product: product, ); }, closedShape: const RoundedRectangleBorder(), closedElevation: 0.0, closedBuilder: (BuildContext _, VoidCallback openContainer) { return Container( // Product tile ); }, )
Esto es lo que parece:
El código fuente completo de esta aplicación de comercio electrónico Flutter se puede encontrar en GitHub aquí.
Crear una experiencia de usuario intuitiva es esencial para una aplicación de comercio electrónico. Este tutorial le mostró cómo desarrollar grandes pantallas y exhibir productos de una manera atractiva.
También aprendimos a usar técnicas de administración de estado como Block para administrar elementos del carrito de compras y mejorar la aplicación agregando funciones como alternar, buscar y animaciones.
¿Has notado aplicaciones desconocidas o un drenaje inesperado de la batería? Estos podrían ser indicios…
Saber cómo Restablecer un iPhone a su Estado de Fábrica es clave para solucionar problemas…
Motorola ha confirmado el lanzamiento de Moto G84 5G y Moto G54 5G en India,…
Recuerde WizardCoder, ¿el codificador de IA que cubrimos recientemente aquí en Windows Report? Nos jactamos…
Los investigadores han descubierto numerosos fallos de seguridad en el complemento WordPress Jupiter X Core…
Para solucionar problemas del sistema de PC con Windows, necesitará una herramienta dedicada Fortect es…