Search code examples
fluttergoogle-cloud-firestoreflutter-dependenciesflutter-providerriverpod

How can we use Provider with Future?


I'm trying to use MultiProvider with FutureBuilder.

Is there source code that uses Firestore with Riverpod or any package to create a cart?

Also, how can we use Riverpod and Cart along with Firestore?

class MyApp extends StatelessWidget {
  MyApp({super.key});
  // List<CartModel> cartModels = new List<CartModel>.empty(growable: true);
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );

    return MaterialApp.router(
      routerConfig: _router,
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: Firebase.initializeApp(
        options: DefaultFirebaseOptions.currentPlatform,
      ),
      builder: (context, snapshot) {
        switch (snapshot.connectionState) {
          case ConnectionState.done:
            final user = FirebaseAuth.instance.currentUser;
            devtools.log(user.toString());
            if (user != null) {
              if (user.emailVerified) {
                devtools.log("verified");
                return const HomePage();
              } else {
                devtools.log("not verified!");
                return const Login();
              }
            } else {
              return const Login();
            }
          default:
            return const CircularProgressIndicator();
        }
      },
    );
  }
}


Solution

  • import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        // Using MultiProvider is convenient when providing multiple objects.
        return MultiProvider(
          providers: [
            // In this sample app, CatalogModel never changes, so a simple Provider
            // is sufficient.
            Provider(create: (context) => CatalogModel()),
            // CartModel is implemented as a ChangeNotifier, which calls for the use
            // of ChangeNotifierProvider. Moreover, CartModel depends
            // on CatalogModel, so a ProxyProvider is needed.
            ChangeNotifierProxyProvider<CatalogModel, CartModel>(
              create: (context) => CartModel(),
              update: (context, catalog, cart) {
                if (cart == null) throw ArgumentError.notNull('cart');
                cart.catalog = catalog;
                return cart;
              },
            ),
          ],
          child: const MaterialApp(
            home: MyCatalog(),
          ),
        );
      }
    }
    
    class MyCatalog extends StatelessWidget {
      const MyCatalog({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: CustomScrollView(
            slivers: [
              _MyAppBar(),
              const SliverToBoxAdapter(child: SizedBox(height: 12)),
              SliverList(
                delegate: SliverChildBuilderDelegate(
                    (context, index) => _MyListItem(index)),
              ),
            ],
          ),
        );
      }
    }
    
    class _AddButton extends StatelessWidget {
      final Item item;
    
      const _AddButton({required this.item});
    
      @override
      Widget build(BuildContext context) {
        // The context.select() method will let you listen to changes to
        // a *part* of a model. You define a function that "selects" (i.e. returns)
        // the part you're interested in, and the provider package will not rebuild
        // this widget unless that particular part of the model changes.
        //
        // This can lead to significant performance improvements.
        var isInCart = context.select<CartModel, bool>(
          // Here, we are only interested whether [item] is inside the cart.
          (cart) => cart.items.contains(item),
        );
    
        return TextButton(
          onPressed: isInCart
              ? null
              : () {
                  // If the item is not in cart, we let the user add it.
                  // We are using context.read() here because the callback
                  // is executed whenever the user taps the button. In other
                  // words, it is executed outside the build method.
                  var cart = context.read<CartModel>();
                  cart.add(item);
                },
          style: ButtonStyle(
            overlayColor: MaterialStateProperty.resolveWith<Color?>((states) {
              if (states.contains(MaterialState.pressed)) {
                return Theme.of(context).primaryColor;
              }
              return null; // Defer to the widget's default.
            }),
          ),
          child: isInCart
              ? const Icon(Icons.check, semanticLabel: 'ADDED')
              : const Text('ADD'),
        );
      }
    }
    
    class _MyAppBar extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return SliverAppBar(
          title: Text('Catalog', style: Theme.of(context).textTheme.displayLarge),
          floating: true,
          actions: [
            IconButton(
              icon: const Icon(Icons.shopping_cart),
              onPressed: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (context) => const MyCart()),
                );
              },
            ),
          ],
        );
      }
    }
    
    class _MyListItem extends StatelessWidget {
      final int index;
    
      const _MyListItem(this.index);
    
      @override
      Widget build(BuildContext context) {
        var item = context.select<CatalogModel, Item>(
          // Here, we are only interested in the item at [index]. We don't care
          // about any other change.
          (catalog) => catalog.getByPosition(index),
        );
        var textTheme = Theme.of(context).textTheme.titleLarge;
    
        return Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          child: LimitedBox(
            maxHeight: 48,
            child: Row(
              children: [
                AspectRatio(
                  aspectRatio: 1,
                  child: Container(
                    color: item.color,
                  ),
                ),
                const SizedBox(width: 24),
                Expanded(
                  child: Text(item.name, style: textTheme),
                ),
                const SizedBox(width: 24),
                _AddButton(item: item),
              ],
            ),
          ),
        );
      }
    }
    
    class MyCart extends StatelessWidget {
      const MyCart({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Cart', style: Theme.of(context).textTheme.displayLarge),
            backgroundColor: Colors.blue,
          ),
          body: Container(
            color: Colors.yellow,
            child: Column(
              children: [
                Expanded(
                  child: Padding(
                    padding: const EdgeInsets.all(32),
                    child: _CartList(),
                  ),
                ),
                const Divider(height: 4, color: Colors.black),
                _CartTotal()
              ],
            ),
          ),
        );
      }
    }
    
    class _CartList extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        var itemNameStyle = Theme.of(context).textTheme.titleLarge;
        // This gets the current state of CartModel and also tells Flutter
        // to rebuild this widget when CartModel notifies listeners (in other words,
        // when it changes).
        var cart = context.watch<CartModel>();
    
        return ListView.builder(
          itemCount: cart.items.length,
          itemBuilder: (context, index) => ListTile(
            leading: const Icon(Icons.done),
            trailing: IconButton(
              icon: const Icon(Icons.remove_circle_outline),
              onPressed: () {
                cart.remove(cart.items[index]);
              },
            ),
            title: Text(
              cart.items[index].name,
              style: itemNameStyle,
            ),
          ),
        );
      }
    }
    
    class _CartTotal extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        var hugeStyle =
            Theme.of(context).textTheme.displayLarge!.copyWith(fontSize: 48);
    
        return SizedBox(
          height: 200,
          child: Center(
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                // Another way to listen to a model's change is to include
                // the Consumer widget. This widget will automatically listen
                // to CartModel and rerun its builder on every change.
                //
                // The important thing is that it will not rebuild
                // the rest of the widgets in this build method.
                Consumer<CartModel>(
                    builder: (context, cart, child) =>
                        Text('\$${cart.totalPrice}', style: hugeStyle)),
                const SizedBox(width: 24),
                TextButton(
                  onPressed: () {
                    ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(content: Text('Buying not supported yet.')));
                  },
                  style: TextButton.styleFrom(foregroundColor: Colors.white),
                  child: const Text('BUY'),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    class CartModel extends ChangeNotifier {
      /// The private field backing [catalog].
      late CatalogModel _catalog;
    
      /// Internal, private state of the cart. Stores the ids of each item.
      final List<int> _itemIds = [];
    
      /// The current catalog. Used to construct items from numeric ids.
      CatalogModel get catalog => _catalog;
    
      set catalog(CatalogModel newCatalog) {
        _catalog = newCatalog;
        // Notify listeners, in case the new catalog provides information
        // different from the previous one. For example, availability of an item
        // might have changed.
        notifyListeners();
      }
    
      /// List of items in the cart.
      List<Item> get items => _itemIds.map((id) => _catalog.getById(id)).toList();
    
      /// The current total price of all items.
      int get totalPrice =>
          items.fold(0, (total, current) => total + current.price);
    
      /// Adds [item] to cart. This is the only way to modify the cart from outside.
      void add(Item item) {
        _itemIds.add(item.id);
        // This line tells [Model] that it should rebuild the widgets that
        // depend on it.
        notifyListeners();
      }
    
      void remove(Item item) {
        _itemIds.remove(item.id);
        // Don't forget to tell dependent widgets to rebuild _every time_
        // you change the model.
        notifyListeners();
      }
    }
    
    class CatalogModel {
      static List<String> itemNames = [
        'Code Smell',
        'Control Flow',
        'Interpreter',
        'Recursion',
        'Sprint',
        'Heisenbug',
        'Spaghetti',
        'Hydra Code',
        'Off-By-One',
        'Scope',
        'Callback',
        'Closure',
        'Automata',
        'Bit Shift',
        'Currying',
      ];
    
      /// Get item by [id].
      ///
      /// In this sample, the catalog is infinite, looping over [itemNames].
      Item getById(int id) => Item(id, itemNames[id % itemNames.length]);
    
      /// Get item by its position in the catalog.
      Item getByPosition(int position) {
        // In this simplified case, an item's position in the catalog
        // is also its id.
        return getById(position);
      }
    }
    
    @immutable
    class Item {
      final int id;
      final String name;
      final Color color;
      final int price = 42;
    
      Item(this.id, this.name)
          // To make the sample app look nicer, each item is given one of the
          // Material Design primary colors.
          : color = Colors.primaries[id % Colors.primaries.length];
    
      @override
      int get hashCode => id;
    
      @override
      bool operator ==(Object other) => other is Item && other.id == id;
    }

    Here is a complete example with Flutter that you can test.