Search code examples
fluttergoogle-cloud-firestoreshopping-cart

How to load data from Firestore using keys


I'm trying to build an e-commerce app using Flutter and Firestore, I'm having a challenge in building the cart. using the codes below, I have been able to get the products a user wishes to add to the cart using the product id. My challenge is how to use the id or the keys to fetch the details of the products from Firestore and store the cart products either in Firestore or SQLite so that I can query from there and display them on the cart page.

appstate.dart


class AppStateModel extends Model {

  final Map<int, int> _productsInCart = <int, int>{};

  Map<int, int> get productsInCart => Map<int, int>.from(_productsInCart);



  void addProductToCart(int productId) {
    if (!_productsInCart.containsKey(productId)) {
      _productsInCart[productId] = 1;
    } else {
      _productsInCart[productId]++;
    }

    notifyListeners();
  }


  void removeItemFromCart(int productId) {
    if (_productsInCart.containsKey(productId)) {
      if (_productsInCart[productId] == 1) {
        _productsInCart.remove(productId);
      } else {
        _productsInCart[productId]--;
      }
    }

    notifyListeners();
  }

  void clearCart() {
    _productsInCart.clear();
    notifyListeners();
  }


}

product_display.dart page with onPressed function to get the id of the item clicked to add to cart

  CupertinoActionSheetAction(
    child: const Text('Add To Cart'),
      onPressed: () {
   model.addProductToCart(products[index].id);
  },
)

product.dart

class Products{
 final  String category;
 final  String description;
 final  int id;
 final int  price;
 final  String title;
 final  String url;


 const Products( {this.category,this.description,this.id, this.price, this.title, this.url,
 });


}

CartProduct.dart

class CartProducts{
 final  String category;
 final  String description;
 final  int id;
 final int  price;
 final  String title;
 final  String url;


 const CartProducts( {this.category,this.description,this.id, this.price, this.title, this.url,
 });


}

Now let say I have products with ids 1, 4, 6, 9, 11 in product cart, when I print in the console using print(model.productsInCart.keys), this was the output (1, 4, 6, 9, 11), now my challenge is how to use these ids to query the products with ids 1,4,6,9,11 from Firestore collection products and store them either in Firebase or SQLite so that I can display them in the cart page for a user to view his/her items in cart.

firestore database


Solution

  • I think this is what you are trying to do is

    Firestore.instance.collection("collection").document("id").get().then((querySnapshot){
        print(querySnapshot.data);
      });
    

    Obviously replace collection with your collection name and id with the id you are trying to fetch. Here I am using .then syntax, but you can pass everything before .then to the FutureBuilder as usual.

    Edit: You'll need to add a helper method for fetching all the data from the firestore.

    Future<List<Products>> getCollection() async {
      List<int> idList = [];
      // productsInCart[key] = value; where key is id and value is amount in cart
      productsInCart.forEach((key, value) {
        idList.add(key);
      });
      List<Products> productList = [];
      for (var id in idList) {
        var documents = (await Firestore.instance
                .collection('products')
                .where('id', isEqualTo: id)
                .getDocuments())
            .documents;
        if (documents.length > 0) {
          var doc = documents[0]
              .data; // if there are multiple documents with given id get first document
          var prod = Products(
              id: doc['id'],
              title: doc['title'],
              price: doc['price'],
              category: doc['category'],
              description: doc['description']);
          productList.add(prod);
        }
      }
      return productList;
    }
    

    then use FutureBuilder to build the list

    FutureBuilder<List<Products>>(
      future: getCollection(),
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          var list = snapshot.data;
          return ListView.builder(
              itemCount: list.length,
              itemBuilder: (context, index) => ListTile(
                    title: Text("${list[index].title}"),
                    subtitle: Text(
                        "Amount in cart : ${productsInCart[list[index].id]}"),
                  ));
        } else {
          return Text("");
        }
      },
    );
    

    I am using Future and FutureBuilder instead of Stream and StreamBuilder, because having to query using multiple ids is a tedious task in cloud firestore, since firestore doesn't have official support for Logical OR. So having to gather data from multiple stream sources is difficult. Using FutureBuilder has the same output as using StreamBuilder as long as product detail is not being changed while app is being used.

    Edit 2: To use multiple stream sources use StreamGroup from async package. Here is what final code looks like

    Stream<List<Products>> _getCollection() async* {
      List<int> idList = [];
      // productsInCart[key] = value; where key is id and value is amount in cart
      productsInCart.forEach((key, value) {
        idList.add(key);
      });
      StreamGroup<QuerySnapshot> streamGroup = StreamGroup();
      for (var id in idList) {
        var stream = Firestore.instance
            .collection('products')
            .where('id', isEqualTo: id)
            .snapshots();
        streamGroup.add(stream);
      }
      //using map to store productDetails so that same products from multiple stream events don't get added multiple times.
      Map<int, Products> productMap = {};
      await for (var val in streamGroup.stream) {
        var documents = val.documents;
        var doc = documents[0].data;
        var product = Products.fromMap(doc);
        productMap[product.id] = product;
        yield productMap.values.toList();
      }
    }
    
    StreamBuilder<List<Products>>(
      stream: _getCollection(),
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          var values = snapshot.data;
          return ListView.builder(
              itemCount: values.length,
              itemBuilder: (context, index) => ListTile(
                    title: Text(values[index].title),
                    subtitle: Text(
                        "Amount in cart : ${productsInCart[values[index].id]}"),
                  ));
        } else {
          print("There is no data");
          return Text("");
        }
      },
    ),
    

    I've added named constructor for convenience

    Products.fromMap(Map map) {
      this.id = map['id'];
      this.title = map['title'];
      this.description = map['description'];
      this.price = map['price'];
      this.category = map['category'];
    }