Search code examples
firebaseflutterdartgoogle-cloud-firestorestream-builder

How to retrieve nested data from Firebase using StreamBuilder and display it all to ListView


I am trying to retrieve nested array data from Firebase and display it on ListView Builder but I got an error of this below.

Exception has occurred.
StateError (Bad state: field does not exist within the DocumentSnapshotPlatform)

Please guide me. I need help.

The model that I used to store the nested data to main collection

class SPMModel {
  String? subjectName;
  String? subjectGrade;

  SPMModel(this.subjectName, this.subjectGrade);

  Map<String, dynamic> toMap() => {
    "subjectName": subjectName,
    "subjectGrade": subjectGrade,
  };
}

The model of the main collection

class UserProfileModel {
  String? uid;
  String? email;
  String? fullName;
  String? nric;
  String? age;
  String? gender;
  String? ethnicity;
  String? religion;
  String? address;
  String? state;
  String? country;
  String? phone;
  String? parentName;
  String? parentPhone;
  List<dynamic>? spmResult = []; //I'm using this variable to store the SPMModel data

  UserProfileModel({
    this.uid,
    this.email,
    this.fullName,
    this.nric,
    this.age,
    this.gender,
    this.ethnicity,
    this.religion,
    this.address,
    this.state,
    this.country,
    this.phone,
    this.parentName,
    this.parentPhone,
    this.spmResult
  });

  //receive data from database
  factory UserProfileModel.fromMap(map) {
    return UserProfileModel(
      uid: map['uid'],
      email: map['email'],
      fullName: map['fullName'],
      nric: map['nric'],
      age: map['age'],
      gender: map['gender'],
      ethnicity: map['ethnicity'],
      religion: map['religion'],
      address: map['address'],
      state: map['state'],
      country: map['country'],
      phone: map['phone'],
      parentName: map['parentName'],
      parentPhone: map['parentPhone'],
      spmResult: map['spmResult']
    );
  }

  //send data to database
  Map<String, dynamic> toMap() {
    return {
      'uid': uid,
      'email': email,
      'fullName': fullName,
      'nric': nric,
      'age': age,
      'gender': gender,
      'ethnicity': ethnicity,
      'religion': religion,
      'address': address,
      'state': state,
      'country': country,
      'phone': phone,
      'parentName': parentName,
      'parentPhone': parentPhone,
      'spmResult': spmResult
    };
  }
}

My StreamBuilder

StreamBuilder<QuerySnapshot>(
  stream: FirebaseFirestore.instance.collection("users").where('uid', isEqualTo: FirebaseAuth.instance.currentUser!.uid).snapshots(),
  builder: (context, snapshot){
    if(!snapshot.hasData){
      return const Center(child: Text('No data found'),);
    }
    return ListView.builder(
      shrinkWrap: true,
      itemCount: snapshot.data!.docs.length,
      itemBuilder: (context, index){
        return Column(
          children: <Widget>[
            Text('id : ${snapshot.data!.docs[index].toString()}'),
            Text('subject: ${snapshot.data!.docs[index]['subjectName']}'),
            Text('grade: ${snapshot.data!.docs[index]['subjectGrade']}'),
          ],
        );
      }
    );
  }
)

Here is the firebase data looks like FirestoreDB


Solution

  • I have solved my own problem.

    Here is the solution

    StreamBuilder(
      stream: FirebaseFirestore.instance.collection("users").doc(user!.uid).
    snapshots(),
      builder: (context, AsyncSnapshot snapshot){
        if(!snapshot.hasData){
          return const Text('No data found, yet');
        }
        return SingleChildScrollView(
          child: DataTable(
            border: TableBorder.all(),
            columnSpacing: 0,
            horizontalMargin: 8, 
            columns: <DataColumn>[
              DataColumn(
                label: SizedBox(
                  child: const Text('Subjects', textAlign: TextAlign.center),
                  width: MediaQuery.of(context).size.width * .60
                )
              ),
              DataColumn(
                label: SizedBox(
                  child: const Text('Grades', textAlign: TextAlign.center),
                  width: MediaQuery.of(context).size.width * .15
                )
              ),
              DataColumn(
                label: SizedBox(
                  child: const Text('Actions', textAlign: TextAlign.center),
                  width: MediaQuery.of(context).size.width * .15
                )
              ),
            ],
            rows: _buildList(context, snapshot.data['spmResult'])
          ),
        );
      },
    )
    

    After I get the snapshot, I used DataTable to display the array by creating these methods

    List<DataRow> _buildList(BuildContext context, List<dynamic> snapshot) {
      return snapshot.map((data) => _buildListItem(context, data)).toList();
    }
    DataRow _buildListItem(BuildContext context, data) {
      final record = SPMModel.fromJson(data);
      return DataRow(
        cells: <DataCell>[
          DataCell(Text(record.subjectName.toString())),
          DataCell(Center(child: Text(record.subjectGrade.toString()))),
          const DataCell(Center(child: Icon(Icons.delete)))
        ]
      );
    }
    

    For my models, here is the updated version

    User Collection model

    class UserProfileModel {
      List<SPMModel>? spmResult;
    
      UserProfileModel({
        this.spmResult,
      });
    
      //receive data from database
      factory UserProfileModel.fromMap(map) {
        return UserProfileModel(
          spmResult: SPMModel.fromJsonArray(map['spmResult'])
        );
      }
    
      //send data to database
      Map<String, dynamic> toMap() {
        return {
          'spmResult': spmResult
        };
      }
    }
    

    Array model

    class SPMModel {
      String? subjectName;
      String? subjectGrade;
    
      SPMModel({this.subjectName, this.subjectGrade});
    
      Map<String, dynamic> toMap() => {
        "subjectName": subjectName,
        "subjectGrade": subjectGrade,
      };
      
      factory SPMModel.fromJson(Map<String, dynamic> json){
        return SPMModel(
          subjectName: json['subjectName'],
          subjectGrade: json['subjectGrade']
        );
      }
    
      static List<SPMModel> fromJsonArray(List<dynamic> jsonArray){
        List<SPMModel> spmModelFromJson = [];
    
        for (var item in jsonArray) {
          spmModelFromJson.add(SPMModel.fromJson(item));
        }
    
        return spmModelFromJson;
      }
    }
    

    For storing the array to the main collection

    await FirebaseFirestore.instance
    .collection("users")
    .doc(FirebaseAuth.instance.currentUser!.uid)
    .update({
      "spmResult": FieldValue.arrayUnion([SPMModel(subjectName: subject, subjectGrad
    grade).toMap()])
    }).then((value) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text
    ('Successfully Added')));
    })                        
    .catchError((onError){
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('${onError.
    message}')));
    });
    

    Here is the output from emulator Output