Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Write and Read nested Map to Firebase Document from Flutter

I am struggling to write (and also haven't yet figured out how to read) nested maps in firebase from my flutter app. I'm writing an expense tracker where the list of categories is stored within each logbook. I can map, save, and retrieve the primitive fields to and from firebase, where I get lost is trying to write the map of the categories and subcategories to firebase and then how to read them.

Solving for both categories and subcategories is essentially the same so I'll just present one of them. Also, I currently have the category Id as both the key and part of the category class itself, I'll be deleting the Id from the class itself later, as I believe its bad practice, it's just helping me at the moment elsewhere.

I've also been following the BLOC method, so I also have models that convert to entities for dealing with firebase.

This is a category (I've removed irrelevant info such as props and to string):

<pre>
    class MyCategory extends Equatable {
  final String id;
  final String name;
  final IconData iconData;

  MyCategory({@required this.id, @required this.name, this.iconData});
  MyCategoryEntity toEntity() {
    return MyCategoryEntity(
      id: id,
      name: name,
      iconCodePoint: iconData.codePoint.toString(),
      iconFontFamily: iconData.fontFamily,
    );
  }

  static MyCategory fromEntity(MyCategoryEntity entity) {
    return MyCategory(
      id: entity.id,
      name: entity.name,
      iconData: IconData(int.parse(entity.iconCodePoint),
          fontFamily: entity.iconFontFamily),
    );
  }
}
</pre>

The model entity for the category has JsonSerialization and also uses the Icon codepoint/fontFamily as it seems to be more friendly to pass back and forth from firebase than IconData directly.

<pre>
    @JsonSerializable()
class MyCategoryEntity extends Equatable {
  final String id;
  final String name;
  final String iconCodePoint;
  final String iconFontFamily;

  const MyCategoryEntity(
      {this.id, this.name, this.iconCodePoint, this.iconFontFamily});

  factory MyCategoryEntity.fromJson(Map<String, dynamic> json) =>
      _$MyCategoryEntityFromJson(json);

  Map<String, dynamic> toJson() => _$MyCategoryEntityToJson(this);
</pre>

I don't think you need to see the Log model as its more the LogEntity that speaks directly to firebase. This is where I'm completely stuck. I've seen a few examples of how to pass a list of objects, but I can't seem to figure out how to pass a map of objects. I'm also not sure how I'd read it back into the LogEntity from firestore (as can be seen in my two TODOs down below).

<pre>
    class LogEntity extends Equatable {
  final String uid;
  final String id;
  final String logName;
  final String currency;
  final bool active;
  //String is the category id
  final Map<String, MyCategory> categories;
  final Map<String, MySubcategory> subcategories;


  const LogEntity({this.uid, this.id, this.logName, this.currency, this.categories, this.subcategories, this.active});


  static LogEntity fromSnapshot(DocumentSnapshot snap) {
    return LogEntity(
      uid: snap.data[UID],
      id: snap.documentID,
      logName: snap.data[LOG_NAME],
      currency: snap.data[CURRENCY_NAME],
      //TODO de-serialize categories and subcategories
      active: snap.data[ACTIVE],

    );
  }

  Map<String, Object> toDocument() {
    return {
      UID: uid,
      LOG_NAME: logName,
      CURRENCY_NAME: currency,
    //TODO serialize categories and subcategories, this does doesn't work
      CATEGORIES: categories.map((key, value) => categories[key].toEntity().toJson()).toString(),
      ACTIVE: active,
    };
  }
}
</pre>

I'm really hoping for some help on this as the only examples I've been able to understand are lists, not maps. Also I want to use a similar method in a few places in this app, so figuring this out is critical.

Git: https://github.com/cver22/expenses2

like image 278
cVer Avatar asked May 31 '20 22:05

cVer


1 Answers

Exporting Data

Firebase needs a Map<String, dynamic>, where dynamic is one of its supported types (like a string, number, map, array, etc.). Thus, if you have a custom object, like MyCategory in your example, you need to .map() it to one of the supported types (yet another Map<String, dynamic> in this case). Thus, you can do a <map that you need to convert>.map((key, value) => MapEntry(key, <method to convert each value to a Map<String, dynamic>>)). Your example is almost correct, but it has a toString() which will make Firebase store it as a string, and not as a map (which is not the desired behavior). As discussed in the comments, removing the toString() should resolve that issue.

Retrieving Data

Similar to the exporting data, Firebase will provide you with a Map<String, dynamic> where dynamic is one of Firebase's supported types, which for category and subcategory are yet again Map<String, dynamic>s. Consequently, you need to convert the snap.data[KEY], which is a dynamic, to a Map<String, YourObject> of the corresponding type in your code. The steps to parse this are like so:

  1. Take data from snapshot as dynamic
  2. Cast this dynamic data to a Map
  3. .map() this cast-ed data to your own types

Thus, something like the following should work:

    return LogEntity(
      uid: snap.data[UID],
      id: snap.documentID,
      logName: snap.data[LOG_NAME],
      currency: snap.data[CURRENCY_NAME],
      categories: (snap.data[CATEGORIES] as Map<String, dynamic>).map((key, value) => MapEntry(key, MyCategory.fromEntity(MyCategoryEntity.fromJson(value)))),
      // todo subcategories should follow the same form as categories; I will leave that one up to you
      active: snap.data[ACTIVE],

    );

Retrieving Nested Primitive Types

I'd like to make a note here as well in case anyone finds this question searching for how to parse a nested map/list that uses primitive types from Firebase (like a Map<String, String>, Map<String, int>, or List<String> to name a few). In that case, check out the Map<String, PrimitiveType>.from() and List<PrimitiveType>.from() constructors; they will probably do exactly what you want, assuming PrimitiveType is one of Firebase's supported types. If not, see the above answer.


One more small thing: I did not look over all of your code, but one thing I couldn't help but notice was your static LogEntity fromSnapshot(). In Dart, you typically use factory or named constructors depending on the use case, but by no means am I an expert.

like image 196
Gregory Conrad Avatar answered Oct 20 '22 08:10

Gregory Conrad