Files
retail/docs/QUICK_START_PROVIDERS.md
Phuoc Nguyen b94c158004 runable
2025-10-10 16:38:07 +07:00

18 KiB

Quick Start Guide - Riverpod 3.0 Providers

Setup Complete!

All Riverpod 3.0 providers have been successfully implemented and code has been generated.


Quick Import Reference

Import All Cart Providers

import 'package:retail/features/home/presentation/providers/providers.dart';

Import All Product Providers

import 'package:retail/features/products/presentation/providers/providers.dart';

Import All Category Providers

import 'package:retail/features/categories/presentation/providers/providers.dart';

Import All Settings Providers

import 'package:retail/features/settings/presentation/providers/providers.dart';

Import Core Providers (Sync, Network)

import 'package:retail/core/providers/providers.dart';

Usage Examples

1. Display Products

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:retail/features/products/presentation/providers/providers.dart';

class ProductsPage extends ConsumerWidget {
  const ProductsPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final productsAsync = ref.watch(productsProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Products')),
      body: productsAsync.when(
        data: (products) => GridView.builder(
          gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
            crossAxisCount: 2,
            childAspectRatio: 0.75,
          ),
          itemCount: products.length,
          itemBuilder: (context, index) {
            final product = products[index];
            return Card(
              child: Column(
                children: [
                  Text(product.name),
                  Text('\$${product.price.toStringAsFixed(2)}'),
                  ElevatedButton(
                    onPressed: () {
                      ref.read(cartProvider.notifier).addItem(product, 1);
                    },
                    child: const Text('Add to Cart'),
                  ),
                ],
              ),
            );
          },
        ),
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (error, stack) => Center(child: Text('Error: $error')),
      ),
    );
  }
}

2. Search and Filter Products

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:retail/features/products/presentation/providers/providers.dart';
import 'package:retail/features/categories/presentation/providers/providers.dart';

class FilteredProductsPage extends ConsumerWidget {
  const FilteredProductsPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final filteredProducts = ref.watch(filteredProductsProvider);
    final searchQuery = ref.watch(searchQueryProvider);
    final categoriesAsync = ref.watch(categoriesProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Products'),
        bottom: PreferredSize(
          preferredSize: const Size.fromHeight(60),
          child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: TextField(
              decoration: const InputDecoration(
                hintText: 'Search products...',
                prefixIcon: Icon(Icons.search),
                border: OutlineInputBorder(),
              ),
              onChanged: (value) {
                ref.read(searchQueryProvider.notifier).setQuery(value);
              },
            ),
          ),
        ),
      ),
      body: Column(
        children: [
          // Category filter chips
          categoriesAsync.when(
            data: (categories) => SizedBox(
              height: 50,
              child: ListView.builder(
                scrollDirection: Axis.horizontal,
                itemCount: categories.length + 1,
                itemBuilder: (context, index) {
                  if (index == 0) {
                    return Padding(
                      padding: const EdgeInsets.all(4.0),
                      child: FilterChip(
                        label: const Text('All'),
                        selected: ref.watch(selectedCategoryProvider) == null,
                        onSelected: (_) {
                          ref.read(selectedCategoryProvider.notifier).clearSelection();
                        },
                      ),
                    );
                  }
                  final category = categories[index - 1];
                  return Padding(
                    padding: const EdgeInsets.all(4.0),
                    child: FilterChip(
                      label: Text(category.name),
                      selected: ref.watch(selectedCategoryProvider) == category.id,
                      onSelected: (_) {
                        ref.read(selectedCategoryProvider.notifier).selectCategory(category.id);
                      },
                    ),
                  );
                },
              ),
            ),
            loading: () => const SizedBox.shrink(),
            error: (_, __) => const SizedBox.shrink(),
          ),
          // Products grid
          Expanded(
            child: GridView.builder(
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 2,
              ),
              itemCount: filteredProducts.length,
              itemBuilder: (context, index) {
                final product = filteredProducts[index];
                return Card(
                  child: Column(
                    children: [
                      Text(product.name),
                      Text('\$${product.price}'),
                    ],
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

3. Shopping Cart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:retail/features/home/presentation/providers/providers.dart';

class CartPage extends ConsumerWidget {
  const CartPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final cartItems = ref.watch(cartProvider);
    final cartTotal = ref.watch(cartTotalProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text('Cart (${cartTotal.itemCount})'),
        actions: [
          IconButton(
            icon: const Icon(Icons.delete_outline),
            onPressed: () {
              ref.read(cartProvider.notifier).clearCart();
            },
          ),
        ],
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              itemCount: cartItems.length,
              itemBuilder: (context, index) {
                final item = cartItems[index];
                return ListTile(
                  title: Text(item.productName),
                  subtitle: Text('\$${item.price.toStringAsFixed(2)}'),
                  trailing: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      IconButton(
                        icon: const Icon(Icons.remove),
                        onPressed: () {
                          ref.read(cartProvider.notifier).decrementQuantity(item.productId);
                        },
                      ),
                      Text('${item.quantity}'),
                      IconButton(
                        icon: const Icon(Icons.add),
                        onPressed: () {
                          ref.read(cartProvider.notifier).incrementQuantity(item.productId);
                        },
                      ),
                      IconButton(
                        icon: const Icon(Icons.delete),
                        onPressed: () {
                          ref.read(cartProvider.notifier).removeItem(item.productId);
                        },
                      ),
                    ],
                  ),
                );
              },
            ),
          ),
          // Cart summary
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                children: [
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      const Text('Subtotal:'),
                      Text('\$${cartTotal.subtotal.toStringAsFixed(2)}'),
                    ],
                  ),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      Text('Tax (${(cartTotal.taxRate * 100).toStringAsFixed(0)}%):'),
                      Text('\$${cartTotal.tax.toStringAsFixed(2)}'),
                    ],
                  ),
                  const Divider(),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      const Text('Total:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
                      Text('\$${cartTotal.total.toStringAsFixed(2)}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
                    ],
                  ),
                  const SizedBox(height: 16),
                  ElevatedButton(
                    onPressed: cartItems.isEmpty ? null : () {
                      // Handle checkout
                    },
                    child: const Text('Checkout'),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

4. Settings Page

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:retail/features/settings/presentation/providers/providers.dart';

class SettingsPage extends ConsumerWidget {
  const SettingsPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final settingsAsync = ref.watch(settingsProvider);
    final themeMode = ref.watch(themeModeProvider);

    return Scaffold(
      appBar: AppBar(title: const Text('Settings')),
      body: settingsAsync.when(
        data: (settings) => ListView(
          children: [
            // Theme settings
            ListTile(
              title: const Text('Theme'),
              subtitle: Text(themeMode.toString().split('.').last),
              trailing: SegmentedButton<ThemeMode>(
                segments: const [
                  ButtonSegment(value: ThemeMode.light, label: Text('Light')),
                  ButtonSegment(value: ThemeMode.dark, label: Text('Dark')),
                  ButtonSegment(value: ThemeMode.system, label: Text('System')),
                ],
                selected: {themeMode},
                onSelectionChanged: (Set<ThemeMode> newSelection) {
                  ref.read(settingsProvider.notifier).updateThemeMode(newSelection.first);
                },
              ),
            ),
            // Language
            ListTile(
              title: const Text('Language'),
              subtitle: Text(settings.language),
              trailing: DropdownButton<String>(
                value: settings.language,
                items: ref.watch(supportedLanguagesProvider).map((lang) {
                  return DropdownMenuItem(
                    value: lang.code,
                    child: Text(lang.nativeName),
                  );
                }).toList(),
                onChanged: (value) {
                  if (value != null) {
                    ref.read(settingsProvider.notifier).updateLanguage(value);
                  }
                },
              ),
            ),
            // Tax rate
            ListTile(
              title: const Text('Tax Rate'),
              subtitle: Text('${(settings.taxRate * 100).toStringAsFixed(1)}%'),
              trailing: SizedBox(
                width: 100,
                child: TextField(
                  keyboardType: TextInputType.number,
                  decoration: const InputDecoration(suffix: Text('%')),
                  onSubmitted: (value) {
                    final rate = double.tryParse(value);
                    if (rate != null) {
                      ref.read(settingsProvider.notifier).updateTaxRate(rate / 100);
                    }
                  },
                ),
              ),
            ),
            // Store name
            ListTile(
              title: const Text('Store Name'),
              subtitle: Text(settings.storeName),
              trailing: IconButton(
                icon: const Icon(Icons.edit),
                onPressed: () {
                  // Show dialog to edit
                },
              ),
            ),
          ],
        ),
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (error, stack) => Center(child: Text('Error: $error')),
      ),
    );
  }
}

5. Sync Data

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:retail/core/providers/providers.dart';

class SyncButton extends ConsumerWidget {
  const SyncButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final syncAsync = ref.watch(syncStatusProvider);
    final lastSync = ref.watch(lastSyncTimeProvider);

    return syncAsync.when(
      data: (syncResult) {
        if (syncResult.isSyncing) {
          return const CircularProgressIndicator();
        }

        return Column(
          children: [
            ElevatedButton.icon(
              icon: const Icon(Icons.sync),
              label: const Text('Sync Data'),
              onPressed: () {
                ref.read(syncStatusProvider.notifier).syncAll();
              },
            ),
            if (lastSync != null)
              Text(
                'Last synced: ${lastSync.toString()}',
                style: Theme.of(context).textTheme.bodySmall,
              ),
            if (syncResult.isOffline)
              const Text(
                'Offline - No internet connection',
                style: TextStyle(color: Colors.orange),
              ),
            if (syncResult.isFailed)
              Text(
                'Sync failed: ${syncResult.message}',
                style: const TextStyle(color: Colors.red),
              ),
            if (syncResult.isSuccess)
              const Text(
                'Sync successful',
                style: TextStyle(color: Colors.green),
              ),
          ],
        );
      },
      loading: () => const CircularProgressIndicator(),
      error: (error, stack) => Text('Error: $error'),
    );
  }
}

6. Main App Setup

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:retail/features/settings/presentation/providers/providers.dart';

void main() {
  runApp(
    // Wrap entire app with ProviderScope
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final themeMode = ref.watch(themeModeProvider);

    return MaterialApp(
      title: 'Retail POS',
      themeMode: themeMode,
      theme: ThemeData.light(),
      darkTheme: ThemeData.dark(),
      home: const HomePage(),
    );
  }
}

Common Patterns

Pattern 1: Optimized Watching (Selective Rebuilds)

// Bad - rebuilds on any cart change
final cart = ref.watch(cartProvider);

// Good - rebuilds only when length changes
final itemCount = ref.watch(cartProvider.select((items) => items.length));

Pattern 2: Async Operations

// Always use AsyncValue.guard for error handling
Future<void> syncData() async {
  state = const AsyncValue.loading();
  state = await AsyncValue.guard(() async {
    return await dataSource.fetchData();
  });
}

Pattern 3: Listening to Changes

ref.listen(cartProvider, (previous, next) {
  if (next.isNotEmpty && previous?.isEmpty == true) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Item added to cart')),
    );
  }
});

Pattern 4: Invalidate and Refresh

// Invalidate - resets provider
ref.invalidate(productsProvider);

// Refresh - invalidate + read immediately
final products = ref.refresh(productsProvider);

Testing Providers

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:retail/features/home/presentation/providers/providers.dart';

void main() {
  test('Cart adds items correctly', () {
    final container = ProviderContainer();
    addTearDown(container.dispose);

    // Initial state
    expect(container.read(cartProvider), isEmpty);

    // Add item
    final product = Product(/*...*/);
    container.read(cartProvider.notifier).addItem(product, 1);

    // Verify
    expect(container.read(cartProvider).length, 1);
    expect(container.read(cartItemCountProvider), 1);
  });
}

Next Steps

  1. Providers are implemented and generated
  2. All dependencies are installed
  3. Code generation is complete
  4. 🔄 Replace mock data sources with Hive implementations
  5. 🔄 Build UI pages using the providers
  6. 🔄 Add error handling and loading states
  7. 🔄 Write tests for providers
  8. 🔄 Implement actual API sync

Need Help?

  • Full Documentation: See PROVIDERS_DOCUMENTATION.md
  • Provider List: See PROVIDERS_SUMMARY.md
  • Riverpod Docs: https://riverpod.dev

All Providers Ready to Use! 🚀

Start building your UI with confidence - all state management is in place!