Esta pantalla integra todos los widgets construidos y consulta el backend con FutureBuilder. Primero, el estado:
dart🔒
import 'package:flutter/material.dart';
import 'package:ecommer_app/widgets/top_bar.dart';
import 'package:ecommer_app/widgets/search_bar_with_filter.dart';
import 'package:ecommer_app/widgets/category_selector.dart';
import 'package:ecommer_app/widgets/product_card.dart';
import 'package:ecommer_app/models/product.dart';
import 'package:ecommer_app/services/product_service.dart';
class Menu extends StatefulWidget {
const Menu({super.key});
@override
State<Menu> createState() => _MenuState();
}
class _MenuState extends State<Menu> {
int _selectedIndex = 0;
late Future<List<Product>> _productsFuture;
@override
void initState() {
super.initState();
_cargarProductos();
}
Future<void> _cargarProductos() async {
setState(() {
_productsFuture = ProductService.getProductos();
});
}
Luego el build con el encabezado deslizable y el FutureBuilder que maneja los 4 estados posibles:
dart🔒
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF181513),
body: SafeArea(
child: RefreshIndicator(
color: const Color(0xFFC88D51),
backgroundColor: const Color(0xFF1F1B18),
onRefresh: _cargarProductos,
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
TopBar(),
SizedBox(height: 24),
SearchBarWithFilter(),
SizedBox(height: 24),
CategorySelector(),
SizedBox(height: 12),
],
),
),
),
FutureBuilder<List<Product>>(
future: _productsFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 40.0),
child: Center(child: CircularProgressIndicator(color: Color(0xFFC88D51))),
),
);
}
if (snapshot.hasError) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 40.0),
child: Center(
child: Text(
'Error al obtener productos de la BD:\n${snapshot.error}',
style: const TextStyle(color: Color(0xFFF5E6D3)),
textAlign: TextAlign.center,
),
),
),
);
}
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 40.0),
child: Center(
child: Text('No hay productos registrados en la base de datos', style: TextStyle(color: Color(0xFFF5E6D3))),
),
),
);
}
final products = snapshot.data!;
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 16.0,
crossAxisSpacing: 16.0,
childAspectRatio: 0.60,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
final product = products[index];
return ProductCard(product: product, onTap: () {}, onAddTap: () {});
},
childCount: products.length,
),
),
);
},
),
const SliverToBoxAdapter(child: SizedBox(height: 24)),
],
),
),
),
Y para cerrar, la barra de navegación inferior:
dart🔒
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
onTap: (index) => setState(() => _selectedIndex = index),
backgroundColor: const Color(0xFF1F1B18),
type: BottomNavigationBarType.fixed,
selectedItemColor: const Color(0xFFC88D51),
unselectedItemColor: Colors.white.withValues(alpha: 0.35),
showUnselectedLabels: false,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home_filled), label: 'Inicio'),
BottomNavigationBarItem(icon: Icon(Icons.favorite_rounded), label: 'Favoritos'),
BottomNavigationBarItem(icon: Icon(Icons.shopping_bag_rounded), label: 'Carrito'),
BottomNavigationBarItem(icon: Icon(Icons.person_rounded), label: 'Perfil'),
],
),
);
}
}
🧪 Prueba este componente
Con el backend corriendo, navega desde HomePages presionando "EXPLORAR MENÚ". Debes ver primero el círculo de carga y luego la grilla de 2 columnas con las tarjetas reales de la base de datos. Desliza hacia abajo para refrescar.