1
votes

I can't stream (read) Documents that contain array properties that are Maps in Firestore.

Using Firestore with a document containing an array with a simple String type works as expected. Easy to write (append with FieldValue.arrayUnion(['data1','data2','data3']) and stream back out with code like:

var test2 = List<String>();
    for (var item in data['items']) {
         print('The item $item');
         test2.add(item);
    }

test2 can now be used as my items property. When I try and use a List where item type becomes a Map in Firestore and is just a simple Class containing a few Strings and a date property. I can write these to FireStore but I can't read them back out.

The following code fails: (no errors in the Debug Console but it doesn't run)

var test2 = List<Item>();
    data['items'].forEach((item) {
      var description = item['description'];
       print('The description $description'); // <-- I don't get past this
       final x = Item.fromMap(item); // <-- so I can't even attempt this
       return test2.add(x);
     });

I never get to actually call my Item.fromMap constructor: here is another try:

//  fails
final theItems = data['items'].map((x) {
      return Item.fromMap(x); // <-- never get here
    });

Although the DEBUG CONSOLE doesn't say there is any problem. If I inspect theItems variable (the debugger 'jumps' a couple of lines down to my return) after the failed iteration it looks like this:

MappedListIterable
_f:Closure
_source:List (1 item)
first:Unhandled exception:\ntype '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of type 'Map<String, dynamic>'\n#0      new Listing.fromMap.<anonymous closure> isEmpty:false
isNotEmpty:true
iterator:ListIterator
last:Unhandled exception:\ntype '_InternalLinkedHashMap<dynamic, dynamic>' is not a subtype of 

So bad things have happened but I have no idea why!

Has anyone actually written and retrieved Firestore array properties that contain Maps? Any help in how to proceed would be greatly appreciated!

More: screen shot of the document enter image description here

Here is the code that reads (streams) the collection

Stream<List<T>> collectionStream<T>({
    @required String path,
    @required T builder(Map<String, dynamic> data, String documentID),
    @required String userId,
    Query queryBuilder(Query query),
    int sort(T lhs, T rhs),
  }) {
    Query query = Firestore.instance.collection(path);
    if (queryBuilder != null) {
      query = queryBuilder(query);
    }
    final Stream<QuerySnapshot> snapshots = query.snapshots();
    return snapshots.map((snapshot)  {
      //print('document: path $path ${snapshot.documents[0]?.documentID}');
      final result = snapshot.documents
          .map((snapshot) => builder(snapshot.data, snapshot.documentID)) 
          .where((value) => value != null)
          .toList();

      if (sort != null) {
        result.sort(sort);
      }
      print('returning from CollectionStream');
      return result;
    });
  }

the .map is where the problem comes in. The builder function resolves to this:

builder: (data, documentId) {
          return Listing.fromMap(data, documentId);
        },

Which ends up here

factory Listing.fromMap(
    Map<String, dynamic> data,
    String documentId,
  ) {
    if (data == null) {
      return null;
    }
    final theTime = (data['createdAt'] as Timestamp).toDate();
// see above code where I fail at getting at the items property/field

Here is the Item Class:

class Item {
  Item(this.description, this.imageURL, this.thumbnailURL,
      {this.status = ItemStatus.active,
      this.type = ListingType.free,
      this.price = 0,
      this.isUpdate = false,
      this.createdAt});


  final String description;
  final String imageURL;
  final String thumbnailURL;
  final ItemStatus status;
  final ListingType type;
  final int price;
  final bool isUpdate;
  DateTime createdAt;
  factory Item.fromMap(
    Map<String, dynamic> data,

  ) {
    if (data == null) {
      return null;
    }
    final theTime = (data['createdAt'] as Timestamp).toDate();
    return Item(

      data['description'],
      data['imageURL'],
      data['thumbnailURL'],
      status: _itemStatusFromString(data['status']),
      type: data['type'] == 'free' ? ListingType.free : ListingType.flatRate,
      createdAt: theTime,
    );
  }
  Map<String, dynamic> toMap() {
    // enums are for shit in Dart
    final statusString = status.toString().split('.')[1];
    final typeString = type.toString().split('.')[1];
    return {
      'description': description,
      'imageURL': imageURL,
      'thumbnailURL': thumbnailURL,
      'itemStatus': statusString,
      'price': price,
      'listingType': typeString,
      if (!isUpdate) 'createdAt': DateTime.now(),
      if (isUpdate) 'updatedAt': DateTime.now(),
    };
  }
}

The above is never called to read (we crash) ... it is called to write the data.

2
Could you show a screenshot of your document, so we can all be clear on what exactly you're writing? Also it would be helpful to see the code that actually reads and writes that document using the Firestore SDK. There is really no such thing as "array properties that are Maps". Firestore has separate list and object types - they are not the same. - Doug Stevenson
If you look in the Firestore console you will see Firestore calls it an array and the property type can be a map. They are NOT called List and Objects they are called array and map. In Dart the array becomes a List and the map an instance of a Class. Screen shot to follow... - Pablo
Could you please share your Item model class? - João Soares
Sure I'll post up the Item class. But realize the code NEVER gets that far. Item.fromMap never gets even an attempt at a call. - Pablo

2 Answers

3
votes

This is a known issue with Firestore. Here is the starting thread Flutter issues I found

From reading through the issues it seems lots of folks use a serializer package which is where the issue surfaced. Its still active...

Here is my solution NOT using a serializer and just doing it 'by hand'.

I was able simplify the problem and generate an error that was Google-able. Below is a single page which just writes and reads a single Document. The Class A and B are as small as can be.

So the code writes a single document to Firestore the contains two properties. A name and items. The items being the List of Class B that Class A contains. Checkout the Firebase console. The reading just does that.

No StreamController just the Console. The problem is in the fromMap method where we have to convert the array of objects in Firesote to a List of class instances in Dart. This should not be this difficult and at a minimum it should be documented ....

the line

var theRealItems = data['items'].map((i) => B.fromMap(i));

will generate the error. And needs to be replaced with

var theItems = data['items'].map((i) {
      var z = Map<String, dynamic>.from(i);
      print(z['description']);
      return B.fromMap(z);
    }).toList();
    var theRealItems = List<B>.from(theItems);

Why this is so difficult is still a mystery to me! Anyone improving this code: I'm all ears.

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';

class A {
  A(this.name, this.items);
  final name;
  final List<B> items;
  factory A.fromMap(
    Map<String, dynamic> data,
    String documentId,
  ) {
    if (data == null) {
      return null;
    }
    // we crash here !!!  
    var theRealItems = data['items'].map((i) => B.fromMap(i));

    // uncomment the 6 lines below
    // var theItems = data['items'].map((i) {
    //   var z = Map<String, dynamic>.from(i);
    //   print(z['description']);
    //   return B.fromMap(z);
    // }).toList();
    // var theRealItems = List<B>.from(theItems);

    return A(data['name'], theRealItems);
  }
  Map<String, dynamic> toMap() {
    var theItems = items.map((i) => i.toMap()).toList();
    return {'name': name, 'items': theItems};
  }
}

class B {
  B(this.description);
  final description;

  factory B.fromMap(
    Map<String, dynamic> data,
  ) {
    if (data == null) {
      return null;
    }
    return B(data['description']);
  }
  Map<String, dynamic> toMap() {
    return {
      'description': description,
    };
  }
}


class Test extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            RaisedButton(
              onPressed: () => _write(),
              child: Text('Write the Doc'),
            ),
            RaisedButton(
              onPressed: () => _read(),
              child: Text('Read the Doc ... check Debug Console!'),
            ),
          ],
        ),
      ),
    );
  }

  _write() async {
    try {
      var b = B('Inside B!');
      List<B> theList = List<B>();
      theList.add(b);
      var a = A('myName', theList);
      await Firestore.instance
          .collection('test')
          .document('testDoc')
          .setData(a.toMap());

      print('returning from write!');
    } catch (e) {
      print('Error ${e.toString()}');
    }
  }
   _read() async {
    try {
     var aFromFs = await Firestore.instance
          .collection('test')
          .document('testDoc')
          .get();
      var a = A.fromMap(aFromFs.data, aFromFs.documentID);
      print('the A from FireBase $a with name ${a.name} first item ${a.items.first.description}');

      print('returning from read!');
    } catch (e) {
      print('Oh no Error! ${e.toString()}');
    }
  }
}
0
votes

In the below code Name refers to the name of your respective document and collection. Let's say you want to get "imageURL" and "thumbnailUrl" for now and update the values without deleting or changing other fields inside the array.

String imageUrl ;
String thumbnailUrl;
DocumentReference _docReference = FirebaseFirestore.instance.collection(NAME).doc(NAME);
//refering to the specific document

Map<String, dynamic> neededData= allDocsFromTraining.data();
//getting all the keys and values inside the document


List<Map<String, dynamic>> _yourDocument=
        (neededData["items"] as List<dynamic>)
            .map((m) => Map<String, dynamic>.from(m))
            .toList();
//only getting the list of items

    for (int index = 0; index < _yourDocument.length; index++) {
      Map<String, dynamic> _getMap = _yourDocument[index];
      if (_getMap["price"] == 0)
//giving a condition to get the desired value and make changes
 {
//setting the variables with the value from the database
              setState(() {
                imageUrl = _getMap["imageURL"];
                thumbnailUrl = _getMap["thumbnailURL"]
              });


///Use this if you want to update value of imageURL and thumbnailURL at that specific index of an array
                _getMap['imageURL'] = "Your Updated URL";
        _getMap['thumbnailURL'] = "Your Updated Url;

        break;
      } else {}
    }

    await _docReference.update({"items": _yourDocument}); 
//this adds the updated value to your database.