E-Commerce Example
Home & Discover Screen
The central product discovery view.
Home & Discover Screen
The Home screen is the central hub of our application, featuring complex layouts, reactive components, and specialized sidebars.
Home View Component (lib/features/home/home.view.dart)
Notice how we meticulously use Fluxy's atomic tokens like .tw(), .bg(), and .rounded() to craft responsive nested grids for product discoverability, side-nav drawers, and bottom sheets seamlessly.
import 'package:flutter/material.dart';
import 'package:fluxy/fluxy.dart';
import 'home.controller.dart';
import '../browse/browse.view.dart';
import '../wishlist/wishlist.view.dart';
import '../profile/profile.view.dart';
import '../checkout/checkout.view.dart';
import 'dart:ui';
class HomeView extends StatelessWidget {
const HomeView({super.key});
@override
Widget build(BuildContext context) {
final controller = Fluxy.find<HomeController>();
return Fx(() {
final index = controller.currentNavIndex.value;
return Fx.scaffold(
backgroundColor: controller.isDarkMode.value ? const Color(0xFF121212) : Colors.grey.shade50,
appBar: index == 0 ? AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
toolbarHeight: 80,
iconTheme: IconThemeData(color: controller.text),
title: Fx.col(
alignItems: CrossAxisAlignment.start,
children: [
Fx.text('Welcome back,').font.sm().color(controller.textMuted),
Fx.text('Discover').font.xl3().bold().color(controller.text),
],
),
actions: [
Fx(() => FxBadge(
label: '${controller.cart.value.length}',
color: Colors.redAccent,
offset: const Offset(4, -4),
child: Fx.box()
.w(44).h(44)
.bg(controller.surface)
.rounded(14)
.shadowSmall()
.pointer()
.center()
.child(Icon(Icons.shopping_bag_outlined, color: controller.text))
.onTap(() => _openCart(context, controller)),
)).pr(20).center(),
],
) : null,
body: Builder(builder: (context) {
if (index == 0) return _buildHomeView(context, controller);
if (index == 1) return BrowseView(controller: controller);
if (index == 2) return WishlistView(controller: controller, buildProductCard: _buildProductCard);
return ProfileView(controller: controller);
}),
drawer: _buildSidebar(context, controller),
bottomNavigationBar: Fx(() => FxBottomBar(
currentIndex: controller.currentNavIndex.value,
onTap: controller.changeNavIndex,
activeColor: Colors.blueAccent,
items: const [
FxBottomBarItem(icon: Icons.home, label: 'Home'),
FxBottomBarItem(icon: Icons.category, label: 'Browse'),
FxBottomBarItem(icon: Icons.favorite, label: 'Wishlist'),
FxBottomBarItem(icon: Icons.person, label: 'Profile'),
],
)),
);
});
}
Widget _buildSidebar(BuildContext context, HomeController controller) {
return Drawer(
backgroundColor: controller.surface,
surfaceTintColor: Colors.transparent,
child: Fx.safe(
Fx.col(
alignItems: CrossAxisAlignment.start,
children: [
// Header Section
Fx.col(
alignItems: CrossAxisAlignment.start,
children: [
Fx.box()
.tw('w-16 h-16 rounded-full flex items-center justify-center')
.bg(Colors.blueAccent.withOpacity(0.1))
.border(color: Colors.blueAccent.withOpacity(0.3), width: 2)
.child(const Icon(Icons.person, size: 32, color: Colors.blueAccent)),
Fx.gap(16),
Fx.text('Fluxy User').tw('text-2xl font-bold').color(controller.text),
Fx.text('user@fluxyshop.com').tw('text-sm').color(controller.textMuted),
],
).tw('px-8 pt-8 pb-4 w-full'),
Fx.gap(16),
// Menu Items
// Menu Items
_buildDrawerItem(context, 'Home', Icons.home, controller, navIndex: 0),
_buildDrawerItem(context, 'Categories', Icons.category_outlined, controller, navIndex: 1),
_buildDrawerItem(context, 'Wishlist', Icons.favorite_border, controller, navIndex: 2),
_buildDrawerItem(context, 'Profile', Icons.person_outline, controller, navIndex: 3),
Fx.box().tw('w-full my-6 opacity-20').h(1).bg(Colors.grey.shade400).mx(24),
Fx.text('Account').tw('text-xs font-bold opacity-50 uppercase tracking-widest px-8 pb-3').color(controller.text),
_buildDrawerItem(context, 'My Orders', Icons.shopping_bag_outlined, controller,
trailing: Fx(() {
if (controller.ordersCount.value == 0) return const SizedBox.shrink();
return Fx.box().bg(Colors.blueAccent).px(8).py(2).rounded(10)
.child(Fx.text('${controller.ordersCount.value}').tw('text-xs font-bold text-white'));
})
),
_buildDrawerItem(context, 'Settings', Icons.settings_outlined, controller),
_buildDrawerItem(context, 'Logout', Icons.logout, controller),
],
).scrollable().pb(40)
),
);
}
Widget _buildDrawerItem(BuildContext context, String title, IconData icon, HomeController controller, {int? navIndex, Widget? trailing}) {
return Fx(() {
final isActive = navIndex != null && controller.currentNavIndex.value == navIndex;
return Fx.row(
children: [
Icon(icon, color: isActive ? Colors.blueAccent : controller.textMuted, size: 24),
Fx.gap(16),
Fx.text(title).tw('text-lg font-bold').color(isActive ? Colors.blueAccent : controller.text).expanded(),
if (trailing != null) trailing,
],
)
.tw('px-6 py-4 mx-6 mb-2 rounded-2xl cursor-pointer')
.bg(isActive ? Colors.blueAccent.withOpacity(0.1) : Colors.transparent)
.onTap(() {
if (navIndex != null) controller.changeNavIndex(navIndex);
Navigator.pop(context);
});
});
}
Widget _buildProductCard(Product product, HomeController controller) {
return Fx.col(
alignItems: CrossAxisAlignment.start,
children: [
Fx.stack(
children: [
Fx.image(
product.imageUrl,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
radius: 16,
error: const Icon(Icons.error).center(),
).p(8),
Positioned(
top: 16,
right: 16,
child: Fx(() {
final isWishlisted = controller.wishlist.value.contains(product.id);
return Fx.box()
.p(8)
.bg(controller.isDarkMode.value ? Colors.grey.shade800 : Colors.white)
.circle()
.shadowSmall()
.pointer()
.child(Icon(
isWishlisted ? Icons.favorite : Icons.favorite_border,
color: isWishlisted ? Colors.redAccent : (controller.isDarkMode.value ? Colors.grey.shade400 : Colors.black45),
size: 18,
))
.onTap(() => controller.toggleWishlist(product));
}),
),
],
).expanded(),
Fx.col(
alignItems: CrossAxisAlignment.start,
children: [
Fx.text(product.category.toUpperCase()).font.xs().bold().color(Colors.grey.shade500),
Fx.gap(4),
Fx.text(product.name).font.md().bold().color(controller.text),
Fx.gap(12),
Fx.row(
justify: MainAxisAlignment.spaceBetween,
children: [
Fx.text('\$${product.price.toStringAsFixed(2)}')
.font.xl()
.bold()
.color(controller.text),
Fx.box()
.w(36).h(36)
.bg(controller.text)
.rounded(10)
.pointer()
.center()
.child(Icon(Icons.add, color: controller.surface, size: 20))
.onTap(() => controller.addToCart(product)),
],
).wFull(),
],
).px(16).pb(16).pt(8),
],
)
.bg(controller.surface)
.rounded(24)
.shadowSmall();
}
void _openCart(BuildContext context, HomeController controller) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) => _CartSheet(controller: controller),
);
}
Widget _buildHomeView(BuildContext context, HomeController controller) {
var crossAxisCount = MediaQuery.of(context).size.width > 800 ? 4 : 2;
return Fx(() {
final products = controller.filteredProducts.value;
return Fx.col(
children: [
Fx.col(
alignItems: CrossAxisAlignment.start,
children: [
Fx.input(
signal: controller.searchQuery,
placeholder: 'Search for anything...',
icon: Icons.search,
).bg(controller.surface).shadowSmall().rounded(16),
Fx.gap(24),
Fx.text('Categories').font.xl().bold().color(controller.text),
Fx.gap(12),
SizedBox(
height: 48,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: controller.categories.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) {
final category = controller.categories[index];
return Fx(() {
final isSelected = controller.selectedCategory.value == category;
return Fx.text(category)
.font.sm()
.color(isSelected ? (controller.isDarkMode.value ? Colors.black87 : Colors.white) : controller.textMuted)
.bold()
.center()
.px(24).py(12)
.bg(isSelected ? controller.text : controller.surface)
.rounded(16)
.shadowSmall()
.pointer()
.onTap(() => controller.selectedCategory.value = category);
});
},
),
),
],
).p(24),
if (products.isEmpty)
Fx.text('No products found.').font.lg().muted().center().py(40),
if (products.isNotEmpty)
FxGrid(
columns: crossAxisCount,
gap: 24,
childAspectRatio: 0.62,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: products.map((p) => _buildProductCard(p, controller)).toList(),
).px(24).pb(24),
],
).scrollable();
});
}
}
class _CartSheet extends StatelessWidget {
final HomeController controller;
const _CartSheet({required this.controller});
@override
Widget build(BuildContext context) {
return Fx.col(
children: [
Fx.box()
.tw('my-3 w-12 h-1.5 rounded-md')
.bg(controller.isDarkMode.value ? Colors.grey.shade800 : Colors.grey.shade200),
Fx.row(
justify: MainAxisAlignment.spaceBetween,
children: [
Fx.col(
alignItems: CrossAxisAlignment.start,
children: [
Fx.text('Your Cart').tw('text-3xl font-bold').color(controller.text),
Fx(() => Fx.text('${controller.cart.value.length} items').tw('text-base').color(controller.textMuted)),
],
),
Fx.box()
.tw('w-10 h-10 rounded-full flex items-center justify-center')
.bg(controller.isDarkMode.value ? Colors.grey.shade800 : Colors.grey.shade100)
.pointer()
.child(Icon(Icons.close, color: controller.textMuted, size: 20))
.onTap(() => Navigator.pop(context)),
],
).tw('px-6 py-4'),
Fx(() {
if (controller.cart.value.isEmpty) {
return Fx.text('Your cart is empty').font.lg().muted().center();
}
return Fx.list(
itemCount: controller.cart.value.length,
itemBuilder: (context, index) {
final item = controller.cart.value[index];
return Fx.row(
children: [
Fx.image(
item.imageUrl,
width: 60,
height: 60,
fit: BoxFit.cover,
radius: 10,
error: const Icon(Icons.error).center(),
),
Fx.gap(16),
Fx.col(
alignItems: CrossAxisAlignment.start,
children: [
Fx.text(item.name).tw('font-bold').color(controller.text),
Fx.gap(4),
Fx.text('\$${item.price.toStringAsFixed(2)}').tw('font-bold text-blue-500'),
],
).expanded(),
IconButton(
icon: const Icon(Icons.remove_circle_outline, color: Colors.redAccent),
onPressed: () => controller.removeFromCart(item),
),
],
)
.tw('p-3 mb-4 rounded-2xl')
.bg(controller.isDarkMode.value ? Colors.grey.shade800 : Colors.grey.shade50)
.border(color: controller.isDarkMode.value ? Colors.grey.shade700 : Colors.grey.shade200);
},
).px(24);
}).expanded(),
Fx.safe(
Fx.col(
size: MainAxisSize.min,
children: [
Fx.row(
justify: MainAxisAlignment.spaceBetween,
children: [
Fx.text('Total').tw('text-2xl font-bold').color(controller.text),
Fx(() => Fx.text('\$${controller.cartTotal.value.toStringAsFixed(2)}')
.tw('text-3xl font-bold text-blue-500')),
],
),
Fx.gap(20),
Fx.box()
.tw('w-full py-4.5 rounded-2xl shadow-sm flex items-center justify-center')
.bg(controller.text)
.pointer()
.child(Fx.text('Go to Checkout').tw('text-lg font-bold').color(controller.surface))
.onTap(() {
if (controller.cart.value.isEmpty) return;
Navigator.pop(context); // Close cart sheet
Navigator.push(context, MaterialPageRoute(builder: (_) => CheckoutView(controller: controller)));
}),
],
),
)
.tw('p-6 shadow-md')
.bg(controller.surface),
],
)
.bg(controller.surface);
}
}Product Detail View (lib/features/home/product_detail.view.dart)
import 'package:flutter/material.dart';
import 'package:fluxy/fluxy.dart';
import 'home.controller.dart';
class ProductDetailView extends StatelessWidget {
final Product product;
final HomeController controller;
const ProductDetailView({
super.key,
required this.product,
required this.controller,
});
@override
Widget build(BuildContext context) {
return Fx(() {
final isWishlisted = controller.wishlist.value.contains(product.id);
return Fx.scaffold(
backgroundColor: controller.surface,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(Icons.arrow_back_ios_new, color: controller.text, size: 20),
onPressed: () => Fluxy.back(),
),
actions: [
Fx.box()
.p(8)
.bg(controller.isDarkMode.value ? Colors.grey.shade800 : Colors.white)
.circle()
.shadowSmall()
.pointer()
.child(Icon(
isWishlisted ? Icons.favorite : Icons.favorite_border,
color: isWishlisted ? Colors.redAccent : (controller.isDarkMode.value ? Colors.grey.shade400 : Colors.black45),
size: 20,
))
.onTap(() => controller.toggleWishlist(product))
.pr(16),
],
),
body: Fx.col(
children: [
// Image Section with Hero
Hero(
tag: 'product_image_${product.id}',
child: Fx.image(
product.imageUrl,
width: double.infinity,
height: MediaQuery.of(context).size.height * 0.45,
fit: BoxFit.cover,
error: const Icon(Icons.error).center(),
),
),
// Details Section
Fx.col(
alignItems: CrossAxisAlignment.start,
children: [
Fx.row(
justify: MainAxisAlignment.spaceBetween,
children: [
Fx.text(product.category.toUpperCase())
.font.sm().bold().color(Colors.blueAccent).tw('tracking-widest'),
Fx.row(children: [
const Icon(Icons.star, color: Colors.amber, size: 20),
Fx.gap(4),
Fx.text(product.rating.toStringAsFixed(1)).font.md().bold().color(controller.text),
Fx.gap(4),
Fx.text('(${product.reviews} reviews)').font.sm().color(controller.textMuted),
]),
]
),
Fx.gap(16),
Fx.text(product.name).font.xl3().bold().color(controller.text),
Fx.gap(12),
Fx.text('\${product.price.toStringAsFixed(2)}')
.font.xl2().bold().color(controller.text),
Fx.gap(32),
Fx.text('Description').font.lg().bold().color(controller.text),
Fx.gap(12),
Fx.text(product.description)
.font.md().color(controller.textMuted).tw('leading-relaxed'),
Fx.gap(40),
],
).p(24).bg(controller.surface),
],
).scrollable(),
bottomNavigationBar: Fx.safe(
Fx.row(
children: [
Fx.col(
alignItems: CrossAxisAlignment.start,
size: MainAxisSize.min,
children: [
Fx.text('Total Price').font.xs().bold().color(controller.textMuted).tw('uppercase tracking-wider'),
Fx.gap(4),
Fx.text('\${product.price.toStringAsFixed(2)}').font.xl().bold().color(controller.text),
]
).expanded(),
Fx.gap(16),
Fx.box()
.h(56)
.w(200)
.bg(Colors.blueAccent)
.rounded(16)
.shadowSmall()
.center()
.pointer()
.child(
Fx.row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.shopping_bag_outlined, color: Colors.white, size: 20),
Fx.gap(12),
Fx.text('Add to Cart').font.lg().bold().color(Colors.white),
],
)
)
.onTap(() {
controller.addToCart(product);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('${product.name} added to cart!'),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
margin: const EdgeInsets.all(16),
)
);
}),
],
).p(24).bg(controller.surface).shadowSmall()
),
);
});
}
}📱 Visual Examples





