solid_signals 1.0.1 copy "solid_signals: ^1.0.1" to clipboard
solid_signals: ^1.0.1 copied to clipboard

A high-performance, ultra-lightweight, and fine-grained reactive state management solution for Flutter.

example/main.dart

import 'package:flutter/material.dart';
import 'package:reactive_signals/reactive_flutter.dart';

// ==========================================
// 1. Core Models and State
// ==========================================

class Product {
  final String id;
  final String name;
  final double price;
  final String description;

  const Product({
    required this.id,
    required this.name,
    required this.price,
    required this.description,
  });
}

// Global catalog of products
const mockProducts = [
  Product(id: '1', name: 'Elite Noise-Cancelling Headphones', price: 299.99, description: 'Experience pure audio bliss with active hybrid noise cancellation.'),
  Product(id: '2', name: 'Ultra-Sharp Mechanical Keyboard', price: 149.99, description: 'Tactile blue switches with vibrant per-key RGB backlighting.'),
  Product(id: '3', name: 'Ergonomic Vertical Mouse', price: 79.99, description: 'Designed to reduce muscle strain and improve wrist alignment.'),
];

// Signals & Computeds
final cart = Signal<List<Product>>([]);
final cartTotal = Computed(() => cart.value.fold<double>(0.0, (sum, item) => sum + item.price));

// Active selected product ID for details screen
final selectedProductId = Signal<String?>(null);

// Signal Family: Fetches detailed reviews / extra details for a product asynchronously
final productDetailFamily = AsyncSignalFamily<String, String>(
  (productId) => AsyncSignal.fromFuture(
    () async {
      print("FAMILY API: Fetching mock reviews for product $productId...");
      await Future.delayed(const Duration(seconds: 1)); // Simulating api latency
      final product = mockProducts.firstWhere((p) => p.id == productId);
      return "Highly rated! Customers say: \"Amazing build quality and excellent value. The ${product.name} exceeded my expectations!\"";
    },
    autoDispose: true, // Dispose and clear cache when modal sheet closes!
  ),
);

// ==========================================
// 2. Central Monitoring via SignalObserver
// ==========================================

class ConsoleSignalObserver extends SignalObserver {
  @override
  void onSignalCreated(Signal signal) {
    print("[Observer] Signal Created: ${signal.runtimeType} (initial: ${signal.value})");
  }

  @override
  void onSignalChanged(Signal signal, Object? oldValue, Object? newValue) {
    print("[Observer] Signal Changed: ${signal.runtimeType} | $oldValue -> $newValue");
  }

  @override
  void onSignalDisposed(Signal signal) {
    print("[Observer] Signal Disposed: ${signal.runtimeType} (final: ${signal.value})");
  }
}

// ==========================================
// 3. Application Entrypoint
// ==========================================

void main() {
  // Register the observer
  signalObserver = ConsoleSignalObserver();

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Signals Enterprise Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        brightness: Brightness.dark,
        scaffoldBackgroundColor: const Color(0xFF0F0E17),
        cardColor: const Color(0xFF1E1C2A),
        colorScheme: const ColorScheme.dark(
          primary: Color(0xFF7F5AF0),
          secondary: Color(0xFF2CB67D),
          surface: Color(0xFF1E1C2A),
        ),
      ),
      home: const CatalogScreen(),
    );
  }
}

// ==========================================
// 4. Catalog Screen Widget
// ==========================================

class CatalogScreen extends StatelessWidget {
  const CatalogScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Solid Signals Store', style: TextStyle(fontWeight: FontWeight.bold)),
        centerTitle: true,
        actions: [
          _buildCartButton(context),
        ],
      ),
      // SignalListener tracks cart updates to trigger side effects (Snackbars) without rebuild
      body: SignalListener<int>(
        select: () => cart.value.length,
        listener: (itemCount) {
          if (itemCount > 0) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text('Added to cart! Total items: $itemCount'),
                backgroundColor: const Color(0xFF2CB67D),
                duration: const Duration(seconds: 1),
                behavior: SnackBarBehavior.floating,
              ),
            );
          }
        },
        child: ListView(
          padding: const EdgeInsets.all(20.0),
          children: [
            _buildPromoBanner(context),
            const SizedBox(height: 24),
            const Text('Available Products', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
            const SizedBox(height: 12),
            ...mockProducts.map((p) => _buildProductCard(context, p)),
          ],
        ),
      ),
    );
  }

  Widget _buildCartButton(BuildContext context) {
    // Rebuild surgically only the badge using Observe
    return Observe(
      builder: (context) {
        return Stack(
          alignment: Alignment.center,
          children: [
            IconButton(
              icon: const Icon(Icons.shopping_cart),
              onPressed: () => _showCartDialog(context),
            ),
            if (cart.value.isNotEmpty)
              Positioned(
                right: 8,
                top: 8,
                child: Container(
                  padding: const EdgeInsets.all(4),
                  decoration: const BoxDecoration(color: Color(0xFFEF4565), shape: BoxShape.circle),
                  constraints: const BoxConstraints(minWidth: 16, minHeight: 16),
                  child: Text(
                    '${cart.value.length}',
                    style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
                    textAlign: TextAlign.center,
                  ),
                ),
              ),
          ],
        );
      },
    );
  }

  Widget _buildPromoBanner(BuildContext context) {
    // Demonstration of SignalScope: Overriding selectedProductId in this subtree
    final promotionalProduct = mockProducts[0]; // Elite Headphones
    final localSelectedProduct = Signal<String?>(null);

    return SignalScope(
      overrides: {
        selectedProductId: localSelectedProduct,
      },
      child: Builder(
        builder: (context) {
          return Container(
            padding: const EdgeInsets.all(16),
            decoration: BoxDecoration(
              gradient: const LinearGradient(colors: [Color(0xFF7F5AF0), Color(0xFF2CB67D)]),
              borderRadius: BorderRadius.circular(12),
            ),
            child: Row(
              children: [
                const Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text('WEEKEND SPECIAL PROMO', style: TextStyle(fontSize: 11, fontWeight: FontWeight.w900, color: Colors.white70)),
                      SizedBox(height: 4),
                      Text('Get 20% off on premium audio products!', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white)),
                    ],
                  ),
                ),
                ElevatedButton(
                  onPressed: () {
                    // This sets the scoped override signal instead of the global selectedProductId
                    localSelectedProduct.value = promotionalProduct.id;
                    _showProductDetails(context, promotionalProduct);
                  },
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.white,
                    foregroundColor: Colors.black,
                  ),
                  child: const Text('View Deal'),
                ),
              ],
            ),
          );
        },
      ),
    );
  }

  Widget _buildProductCard(BuildContext context, Product product) {
    return Card(
      margin: const EdgeInsets.only(bottom: 16),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(product.name, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
            const SizedBox(height: 4),
            Text('\$${product.price.toStringAsFixed(2)}', style: const TextStyle(color: Color(0xFF2CB67D), fontWeight: FontWeight.bold)),
            const SizedBox(height: 8),
            Text(product.description, style: const TextStyle(color: Colors.grey, fontSize: 13)),
            const SizedBox(height: 16),
            Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                TextButton(
                  onPressed: () {
                    selectedProductId.value = product.id; // Sets global selectedProductId
                    _showProductDetails(context, product);
                  },
                  child: const Text('Learn More'),
                ),
                const SizedBox(width: 8),
                ElevatedButton.icon(
                  onPressed: () => cart.value = [...cart.value, product],
                  icon: const Icon(Icons.add_shopping_cart, size: 16),
                  label: const Text('Add to Cart'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  void _showProductDetails(BuildContext context, Product product) {
    // Retrieve the active selected ID from scope.
    // In the promo banner subtree, it resolves to localSelectedProduct, otherwise to the global selectedProductId.
    final activeIdSignal = SignalScope.get(context, selectedProductId);

    showModalBottomSheet(
      context: context,
      backgroundColor: const Color(0xFF1E1C2A),
      shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
      builder: (context) {
        // Resolve the async signal from the family based on the active ID
        final detailsSignal = productDetailFamily(activeIdSignal.value!);

        return Padding(
          padding: const EdgeInsets.all(24.0),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Text(product.name, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
              const SizedBox(height: 8),
              Text('\$${product.price.toStringAsFixed(2)}', style: const TextStyle(color: Color(0xFF2CB67D), fontSize: 16, fontWeight: FontWeight.bold)),
              const SizedBox(height: 16),
              const Text('Product Overview', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey)),
              const SizedBox(height: 4),
              Text(product.description),
              const SizedBox(height: 20),
              const Text('Featured Review (Loaded Lazily)', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey)),
              const SizedBox(height: 8),
              // Render loading / data / error for the AsyncSignalFamily
              Observe(
                builder: (context) {
                  return detailsSignal.when(
                    data: (review) => Container(
                      padding: const EdgeInsets.all(12),
                      decoration: BoxDecoration(color: const Color(0x0DFFFFFF), borderRadius: BorderRadius.circular(8)),
                      child: Text(
                        review,
                        style: const TextStyle(fontStyle: FontStyle.italic, fontSize: 13),
                      ),
                    ),
                    loading: () => const Center(
                      child: Padding(
                        padding: EdgeInsets.all(16.0),
                        child: CircularProgressIndicator(),
                      ),
                    ),
                    error: (err, st) => Text('Failed to load review: $err', style: const TextStyle(color: Colors.red)),
                  );
                },
              ),
              const SizedBox(height: 24),
              ElevatedButton(
                onPressed: () => Navigator.pop(context),
                child: const Text('Close'),
              ),
            ],
          ),
        );
      },
    ).then((_) {
      // Clear selection when sheet is closed
      activeIdSignal.value = null;
    });
  }

  void _showCartDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) {
        return AlertDialog(
          backgroundColor: const Color(0xFF1E1C2A),
          title: const Text('Shopping Cart'),
          content: SizedBox(
            width: double.maxFinite,
            child: Observe(
              builder: (context) {
                if (cart.value.isEmpty) {
                  return const Padding(
                    padding: EdgeInsets.symmetric(vertical: 24.0),
                    child: Text('Your cart is empty.', style: TextStyle(color: Colors.grey)),
                  );
                }
                return Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    ...cart.value.map((item) => ListTile(
                          title: Text(item.name),
                          trailing: Text('\$${item.price.toStringAsFixed(2)}'),
                        )),
                    const Divider(),
                    ListTile(
                      title: const Text('Total', style: TextStyle(fontWeight: FontWeight.bold)),
                      trailing: Text(
                        '\$${cartTotal.value.toStringAsFixed(2)}',
                        style: const TextStyle(fontWeight: FontWeight.bold, color: Color(0xFF2CB67D)),
                      ),
                    ),
                  ],
                );
              },
            ),
          ),
          actions: [
            TextButton(
              onPressed: () {
                cart.value = [];
                Navigator.pop(context);
              },
              child: const Text('Clear Cart', style: TextStyle(color: Color(0xFFEF4565))),
            ),
            ElevatedButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('Checkout'),
            ),
          ],
        );
      },
    );
  }
}
0
likes
0
points
109
downloads

Publisher

unverified uploader

Weekly Downloads

A high-performance, ultra-lightweight, and fine-grained reactive state management solution for Flutter.

Homepage
Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter

More

Packages that depend on solid_signals