33
votes

I have a class that has several embedded arrays as well as a couple of objects. I'm using Flutter and can't figure out how to read/write to Cloud Firestore.

I can read/write data members that are default types like String and Int. Here is the constructor I'm trying to use to instantiate an object from a DocumentSnapshot:

 class GameReview {
   String name;
   int howPopular;
   List<String> reviewers;
 }

 class ItemCount {
   int itemType;
   int count;

   ItemCount.fromMap(Map<dynamic, dynamic> data)
       : itemType = data['itemType'],
         count = data['count'];
 }

 class GameRecord {
   // Header members
   String documentID;
   String name;
   int creationTimestamp;
   List<int> ratings = new List<int>();
   List<String> players = new List<String>();
   GameReview gameReview;
   List<ItemCount> itemCounts = new List<ItemCount>();

   GameRecord.fromSnapshot(DocumentSnapshot snapshot)
       : documentID = snapshot.documentID,
         name = snapshot['name'],
         creationTimestamp = snapshot['creationTimestamp'],
         ratings = snapshot['ratings'], // ERROR on run
         players = snapshot['players'], // ERROR on run
         gameReview = snapshot['gameReview']; // ERROR on run
         itemCount = ????
 }

It works until I add the last 3 members (ratings, players and gameReview). This should be obvious but none the less, it eludes me.

Help!

UPDATE: Here is a sample of the document stored in Cloud Firestore. This is stored in a single document. In other words, I'm not using sub-collections for the embedded objects. I put it into a JSON format for clarity. I hope this helps.

 {
   "documentID": "asd8didjeurkff3",
   "name": "My Game Record",
   "creationTimestamp": 1235434,
   "ratings": [
     4,
     2012,
     4
   ],
   "players": [
     "Fred",
     "Sue",
     "John"
   ],
   "gameReview": {
     "name": "Review 1",
     "howPopular": 5,
     "reviewers": [
       "Bob",
       "Hanna",
       "George"
     ]
   },
  "itemCounts": [
     {
       "itemType": 2,
       "count": 3
     },
     {
       "itemType": 1,
       "count": 2
     }
   ]
 }

UPDATE 2: I didn't put in the whole class definition because I thought it would be obvious to me how to do the rest but alas that was not the case.

I have a list of objects that I want to load.vbandrade's answer is BANG on but I can't quite figure out how I'm supposed to create the list of objects. List.from(...) is looking for an iterator, not a created class. I'm sure it's some variation of creating a new object and then adding it to a list but I'm a little confused. (see edits in class above, specifically, the "itemCounts" member.

4
It might help people answer if you posted an example document from your collection (so we can see the schema). - RedBrogdon

4 Answers

55
votes

load the list from the array and let the framework take care of type casting.

an object is simply a map, like you wrote in your Json. I also use named constructor. ((still learning and dont know how to use the static constructor @ganapat mentioned))

here“s the working code. I kept firebase auth out and used the StreamBuilder widget.

import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'model/firebase_auth_service.dart';

void main() async {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  final firebaseAuth = new FirebaseAuthService();

  MyApp() {
    firebaseAuth.anonymousLogin();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        home: Scaffold(
            body: Center(
                child: FlatButton(
      color: Colors.amber,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Text("get Game Record"),
          StreamBuilder<GameRecord>(
            stream: getGame(),
            builder: (BuildContext c, AsyncSnapshot<GameRecord> data) {
              if (data?.data == null) return Text("Error");

              GameRecord r = data.data;

              return Text("${r.creationTimestamp} + ${r.name}");
            },
          ),
        ],
      ),
      onPressed: () {
        getGame();
      },
    ))));
  }
}

Stream<GameRecord> getGame() {
  return Firestore.instance
      .collection("games")
      .document("zZJKQOuuoYVgsyhJJAgc")
      .get()
      .then((snapshot) {
    try {
      return GameRecord.fromSnapshot(snapshot);
    } catch (e) {
      print(e);
      return null;
    }
  }).asStream();
}

class GameReview {
  String name;
  int howPopular;
  List<String> reviewers;

  GameReview.fromMap(Map<dynamic, dynamic> data)
      : name = data["name"],
        howPopular = data["howPopular"],
        reviewers = List.from(data['reviewers']);
}

class GameRecord {
  // Header members
  String documentID;
  String name;
  int creationTimestamp;
  List<int> ratings = new List<int>();
  List<String> players = new List<String>();
  GameReview gameReview;

  GameRecord.fromSnapshot(DocumentSnapshot snapshot)
      : documentID = snapshot.documentID,
        name = snapshot['name'],
        creationTimestamp = snapshot['creationTimestamp'],
        ratings = List.from(snapshot['ratings']),
        players = List.from(snapshot['players']),
        gameReview = GameReview.fromMap(snapshot['gameReview']);
}

snapshot['itemCount'] is an array of objects. map each item in that array to an ItemCount object and return as a List:

    itemCounts = snapshot['itemCount'].map<ItemCount>((item) {
      return ItemCount.fromMap(item);
    }).toList();
12
votes

If you came here because of List<dynamic> is not of type List<someType> error while reading data from firestore, you can just use List.castFrom

Example: List<String> cards = List.castFrom(cardsListFromFirebase);

Checkout Flutter firebase, List<dynamic> is not of type List<String>

10
votes

You can use JsoSerializable()

Add the following dependencies to pubspec.yaml

dependencies:
  # Your other regular dependencies here
  json_annotation: ^2.0.0

dev_dependencies:
  # Your other dev_dependencies here
  build_runner: ^1.0.0
  json_serializable: ^2.0.0

and make your classes JsonSerializable()

import 'package:json_annotation/json_annotation.dart';

part 'game.g.dart';

@JsonSerializable()
 class GameReview {
   String name;
   int howPopular;
   List<String> reviewers;

  GameReview();

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

  Map<String, dynamic> toJson() => _$GameReviewToJson(this);
 }

@JsonSerializable()
 class ItemCount {
   int itemType;
   int count;

   ItemCount();

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

  Map<String, dynamic> toJson() => _$ItemCountToJson(this);
 }

 class GameRecord {
   // Header members
   String documentID;
   String name;
   int creationTimestamp;
   List<int> ratings = new List<int>();
   List<String> players = new List<String>();
   GameReview gameReview;
   List<ItemCount> itemCounts = new List<ItemCount>();

  GameRecord();

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

  Map<String, dynamic> toJson() => _$GameRecordToJson(this);
 }

Then generate the JSON serialization code by running the code generation utility from your terminal:

flutter packages pub run build_runner build

Now you can use jsonEncode() and jsonDecode() in order to store and retrieve objects from the firestore

For setting data:

Firestore.instance
      .collection("games")
      .document("zZJKQOuuoYVgsyhJJAgc")
      .setData(jsonDecode(jsonEncode(gameRecord)));

For retrieving data:

 GameRecord.fromJson(jsonDecode(jsonEncode(snapshot.data)));
1
votes

Firebase packages returns List types for Array/List types present in a snapshot. Try converting List to List or List before assigning to variables. And for GameReview object, currently, you are trying to assign object of Map to the object, it would be beneficial if you write static fromMap method in GameReview class which takes map argument and converts it to desired object structure, as you would see in many of flutters example codes.

class GameReivew{

  static GameReivew fromMap(Map<String, dynamic> map){
    GameReivew gameReivew = new GameReivew();
    gameReivew.name = map["name"];
    gameReivew.howPopular = map["howPopular"];
    ....

    return gameReivew;
  }
}