Fluxy
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

Discover ScreenDiscover DarkProduct DetailsProduct Details DarkSidebar MenuSidebar Menu Dark

On this page