flutter/firebase - The method XX was called on null - firebase

I am new to flutter and I am trying to integrate a firebase backend to store my data. I am trying to establish a stream using firebase but when I try to create a listview with the stream I get the following message:
The method 'collection' was called on null.
Receiver: null
Tried calling: collection("betslips")
Here is my code:
class Database {
final FirebaseFirestore firestore;
Database(this.firestore);
Stream<List<BetSlipModel>> streamBetSlip({String uid}) {
try {
print(firestore.collection("betslips"));
return firestore
.collection("betslips")
.snapshots()
.map((query) {
List<BetSlipModel> retVal;
for(final DocumentSnapshot doc in query.docs) {
retVal.add(BetSlipModel.fromDocumentSnapshot(documentSnapshot: doc));
}
return retVal;
});
} catch(e) {
rethrow;
}
}
}
I then try and access the values here:
body: Expanded(
child: StreamBuilder(
stream: Database(widget.firestore)
.streamBetSlip(uid: widget.auth.currentUser.uid),
builder: (BuildContext context,
AsyncSnapshot<List<BetSlipModel>> snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
if (snapshot.data.isEmpty) {
return const Center(
child: Text("Empty"),
);
}
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (_, index) {
return BetSlipCard(
firestore: widget.firestore,
uid: widget.auth.currentUser.uid,
betslip: snapshot.data[index],
);
},
);
} else {
return const Center(
child: Text("loading..."),
);
}
},
),
),
Any ideas? Thanks

The method 'collection' was called on null.
Receiver: null
Tried calling: collection("betslips")
means that firestore variable is not referencing anything, check the below on how to solve it:
You are creating an instance of the class here:
stream: Database(widget.firestore)
widget is an instance variable of the class State, therefore inside the State class initialize firestore:
final FirebaseFirestore firestore = FirebaseFirestore.instance;

Related

Bad state: Snapshot has neither data nor error in flutter when using StreamBuilder and Firestore

I'm adding data from Firestore to a Stream from StreamBuilder, but I'm getting the following error:
Exception has occurred. StateError (Bad state: Snapshot has neither data nor error
My code.
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
AppState? estado;
static String? userID = FirebaseAuth.instance.currentUser?.uid;
static final userColeccion = FirebaseFirestore.instance.collection("users");
var groupfav = ' ';
Stream<QuerySnapshot>? taskGroup;
#override
void initState() {
super.initState();
getGroupFavData();
}
void getGroupFavData() async {
var groupFavData = await userColeccion.doc("$userID").get();
var groupfav = groupFavData.data()!['groupfav'];
taskGroup = FirebaseFirestore.instance
.collection("groups")
.doc(groupfav) // pass the obtained value
.collection("task")
.snapshots();
}
#override
Widget build(BuildContext context) {
estado = Provider.of<AppState>(context, listen: true);
return Scaffold(
appBar: AppBar(
title: const Text("Home"),
automaticallyImplyLeading: false,
),
body: StreamBuilder(
stream: taskGroup,
builder: (
BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot,
) {
if (snapshot.hasError) {
return const Text("error");
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Text("Loading");
}
var data = snapshot.requireData;
return ListView.builder(
itemCount: data.size,
itemBuilder: (context, index) {
return Card(
child: ListTile(
title: Text("${data.docs[index]['titulo']}"),
subtitle: Text("${data.docs[index]['contenido']}"),
onTap: () {},
trailing: IconButton(
icon: const Icon(Icons.delete),
color: Colors.red[200],
onPressed: () {},
),
),
);
},
);
},
),
);
}
}
Ok, looking at your issue, I see that 1) you need to get the data of the document BEFORE you start listening on that document, which is normal, so you want to do a call first to the collection, get the document, then listen on the document's collection called task, which makes sense. Your issue is still an asynchronous issue. The app is rebuilding on a stream that still hasn't arrived; you have to fix the sequence of things.
You then need to switch things up a bit and do the following:
Option #1:
a) Use a FutureBuilder: this will allow you to make the async call to get the document name based on the user Id
b) After you get the document associated to that user, you want to listen on the stream produced by the collection called tasks in that document. There is where then you can hook up the StreamBuilder.
Option #2:
a) Keep things the way you have, but do a listen on the taskGroup snapshots; but keep rebuilding the list as the values arrive on that collection.
Those are my suggestions.
Here's some brief code on option 1:
// .. in your Scaffold's body:
Scaffold(
body: FutureBuilder( // the future builder fetches the initial data
future: userColeccion.doc("$userID").get(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasData) {
var groupfav = snapshot.data()!['groupfav'];
// then once the 'groupfav' has arrived,
// start listening on the taskGroup
taskGroup = FirebaseFirestore.instance
.collection("groups")
.doc(groupfav) // pass the obtained value
.collection("task")
.snapshots();
return StreamBuilder(
stream: taskGroup,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
// the rest of your code
});
}
return CircularProgressIndicator();
}
)
)
Option 2 would be something like:
List<Task> userTasks = [];
void getGroupFavData() async {
var groupFavData = await userColeccion.doc("$userID").get();
var groupfav = groupFavData.data()!['groupfav'];
taskGroup = FirebaseFirestore.instance
.collection("groups")
.doc(groupfav) // pass the obtained value
.collection("task")
.snapshots().listen((snapshot) {
// here populate a list of your tasks
// and trigger a widget rebuild once you've grabbed the values
// and display it as a list on the UI
setState(() {
userTasks = snapshot.docs.map((d) => Task.fromJson(d.data())).toList();
});
});
}
And in your Scaffold, you can have a ListView just rendering the items on that task list, like:
ListView.builder(
itemCount: userTasks.length,
itemBuilder: (context, index) {
// render your tasks here
})
Here's a Gist with some working code to illustrate my point. Run it on DartPad and you'll see how using a FutureBuilder wrapping a StreamBuilder will accomplish what you want.
If you run the above code on DartPad, you'll get the following output:
Hope those pointers take you somewhere.

Getting an error when trying to retrieve data from Firebase FireStore document

I'm trying to retrieve all the courses that the user has enrolled in, these courses are present in an array within the document.
After retrieving the course ID from the users collection, I'm trying to retrieve the course details from the courses collection.
But before the courses variable is populated, the coursesCollection statement is executed and throwing the below error.
======== Exception caught by widgets library =======================================================
The following assertion was thrown building _BodyBuilder:
'in' filters require a non-empty [List].
'package:cloud_firestore/src/query.dart':
Failed assertion: line 706 pos 11: '(value as List).isNotEmpty'
Here is the error causing code:
List courses = [];
var coursesCollection;
void fetchCourses() async {
final loggedInUser = FirebaseAuth.instance.currentUser;
if (loggedInUser != null) {
final userCollection = await FirebaseFirestore.instance.collection('users').doc(loggedInUser.uid).get();
courses = userCollection.get('coursesEnrolled');
}
}
#override
void initState() {
fetchCourses();
coursesCollection = FirebaseFirestore.instance.collection('courses').where('courseID', whereIn: courses);
super.initState();
}
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: coursesCollection.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(backgroundColor: kBrandColor),
);
}
}
final courseListStream = snapshot.data!.docs.map((course) {
return CourseData.fromDocument(course);
}).toList();
List<BadgedCourseCard> courseCards = [];
for (var course in courseListStream) {
final courseDocID = course.courseDocID;
final courseID = course.courseID;
final courseTitle = course.courseTitle;
final courseImage = course.courseImage;
final courseBgColor = hexToColor(course.courseBackgroundColor.toString());
hexToColor(course.courseFgColor.toString());
final badgedCourseCard = BadgedCourseCard(
courseTitle: courseTitle.toString(),
courseTitleTextColor: courseFgColor,
cardBackgroundColor: courseBgColor,
courseImage: courseImage.toString(),
courseCardTapped: () {
Provider.of<CourseProvider>(context, listen: false).currentCourseDetails(
currentCourseDocID: courseDocID,
currentCourseID: courseID,
);
Navigator.of(context).push(ScaledAnimationPageRoute(CourseLandingPage(courseID: courseID.toString())));
},
courseBookmarkTapped: () => print("Course Bookmark Tapped"),
rightPadding: 3,
bottomPadding: 0.5,
cardWidth: 80,
);
courseCards.add(badgedCourseCard);
}
return SizedBox(
height: 20.5.h,
child: ListView(
physics: BouncingScrollPhysics(),
scrollDirection: Axis.horizontal,
children: courseCards,
),
);
},
);
}
How can I fix this issue?
Here,
coursesCollection = FirebaseFirestore.instance.collection('courses').where('courseID', whereIn: courses);
courses would be [] because fetchCourses is an async call.
Change the return type of fetchCourses from void to Future<void> & try using a then callback:
#override
void initState() {
super.initState();
fetchCourses().then((val) {
coursesCollection = FirebaseFirestore.instance.collection('courses').where('courseID', whereIn: courses);
setState(() {});
});
}
I would also recommend to use FutureBuilder as a better alternative.
coursesCollection is null that's why you're getting another error. Render StreamBuilder only when coursesCollection is not null.
coursesCollection != null ? StreamBuilder(...) : SizedBox(),
For listening to the user's enrolled courses, another StreamBuilder can be used. It would be a nested StreamBuilder setup.
StreamBuilder<DocumentSnapshot<Map<String, dynamic>>>(
stream: FirebaseFirestore.instance.collection('users').doc(FirebaseAuth.instance.currentUser.uid).snapshots(),
builder: (context, snapshot) => snapshot.hasData ? StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
stream: FirebaseFirestore.instance.collection('courses').where('courseID', whereIn: snapshot.data!.data()!['coursesEnrolled']).snapshots(),
builder: (context, snapshotTwo) {},
) : Text('Loading...'),
),

How to use firebase collection length in list view builder flutter null safety

I am relatively new to flutter and firebase and trying to rtrieve messages from a collection of messages, each having message properties like message text, sender etc.
however, when using list view builder, I can't use itemcount as it says, Future cant be used in place of int or 'data can't be unconditionally called as it may be null'. Adding null check (!) causes it to say that it can't be used for object. please help.
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MaterialApp(
home: readMessage(),
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
primarySwatch: Colors.purple,
)));
}
class readMessage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _readMessageState();
}
}
class _readMessageState extends State<readMessage> {
bool isMe = true;
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseFirestore.instance
.collection('users')
.doc('ItpNRR6uNfl4Mf4qvlIa')
.collection('messages')
.snapshots(),
builder: ((context, snapshot) {
/// This is where I need the listview builder of collection inside the document
}));
}
BoxDecoration messageBubbleMaker(bool isMe){
return BoxDecoration(
borderRadius: BorderRadius.only(topRight: Radius.circular(20), topLeft: Radius.circular(20), bottomRight: isMe? Radius.zero : Radius.circular(20), bottomLeft: !isMe? Radius.zero : Radius.circular(20)),
color: isMe? Colors.black : Colors.blueAccent
);
}
}
This is my code.
it supports null safety
For your stream you need to convert each snapshot from json to an object and then convert it to a list. Then, you can use snapshot.data.length as the itemcount in your ListView.builder. For example,
return StreamBuilder(
stream: FirebaseFirestore.instance
.collection('users')
.doc('ItpNRR6uNfl4Mf4qvlIa')
.collection('messages')
.snapshots().map((snapshot) =>
snapshot.docs.map((doc) => Message.fromJson(doc.data())).toList()),
builder: ((context, snapshot) {
ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
// return something for every item in the list
}),
}));
And have a model class for messages called message.dart. For example:
class Message {
final String messageId;
final String content;
Message(this.messageId, this.content);
Message.fromJson(Map<String, dynamic> json)
: messageId = json['messageId'],
content = json['content'];
Map<String, dynamic> toJson() => {
'messageId': messageId,
'content': content,
};
}
This works
StreamBuilder(
stream: FirebaseFirestore.instance
.collection('collection name')
.doc('doc id')
.collection('collection name')
.snapshots();
Gets a collection as QuerySnapshot (data type).
If you are using a document, then it will be a DocumentSnapshot instead of querySnapshot
Now in the builder
builder: ((context, snapshot) {
if (snapshot.hasData) {
var collectionData = snapshot.data as QuerySnapshot;
}
This will check presence of data and optional but recommended.
You can define the collectionData without if statement or add using the ? interpolation like
snapshot.hasData ? <What to do if true> : <what to do if false>
In listview builder inside the same if statement above:
itemCount: messages.docs.length,
itembuilder: (BuildContext context, int index)
{
var collectionDataList = collectionData.docs.toList();
var CollectionDoc = collectionDataList[index];
return Text(CollectionDoc['field name']);
})
Replace the Text field with anything you like, like a custom message bubble maker for a chat app where you can provide the field values as parameters.

The getter 'docs' isn't defined for the type 'AsyncSnapshot<Object?>'

After migrating to null safety getting error The getter 'docs' isn't defined for the type 'AsyncSnapshot<Object?>'.
Try importing the library that defines 'docs', correcting the name to the name of an existing getter, or defining a getter or field named 'docs'.
Code snippet where error is
return FutureBuilder(
future: searchResultsFuture,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return circularProgress();
}
List<UserResult> searchResults = [];
snapshot.docs.forEach((doc) { //have error here
User user = User.fromDocument(doc);
UserResult searchResult = UserResult(user);
searchResults.add(searchResult);
});
return ListView(
children: searchResults,
);
},
);
}
searchResultsFuture
handleSearch(String query) {
Future<QuerySnapshot> users =
usersRef.where("displayName", isGreaterThanOrEqualTo: query).get();
setState(() {
searchResultsFuture = users;
});
}
clearSearch() {
searchController.clear();
}
The snapshot in your code is an AsyncSnapshot, which indeed doesn't have a docs child. To get the docs from Firestore, you need to use:
snapshot.data.docs
Also see the FlutterFire documentation on listening for realtime data, which contains an example showing this - and my answer here explaining all snapshot types: What is the difference between existing types of snapshots in Firebase?
change like this:
return FutureBuilder(
future: searchResultsFuture,
builder: (context, **AsyncSnapshot** snapshot) {
if (!snapshot.hasData) {
return circularProgress();
}
List<UserResult> searchResults = [];
**snapshot.data!.docs.forEach((doc) {**
User user = User.fromDocument(doc);
UserResult searchResult = UserResult(user);
searchResults.add(searchResult);
});
return ListView(
children: searchResults,
);
},
);
}
usually, it takes a few ms for data to retrieve so I tried this to
make sure my operations are performed after data is retrieved
return StreamBuilder<QuerySnapshot>(
stream: Collectionreference
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> activitySnapshot) {
if (activitySnapshot.hasError) {
return Center(
child: Text('Something went wrong'),
);
}
if (activitySnapshot.connectionState == ConnectionState.waiting) {
return Center(
child: SpinKitWave(
color: constants.AppMainColor,
itemCount: 5,
size: 40.0,
)));
}
if (!activitySnapshot.hasData || activitySnapshot.data.docs.isEmpty) {
return Center(
child: Text('Nothing to Display here'),
);
}
if (activitySnapshot.hasData) {
activitySnapshot.data.docs.forEach(doc=>{
print(doc);
})
}
}
});

How to efficiently access a firestore reference field's data in flutter?

Using similar code as flutter's firestore example, suppose there is a reference field stored in a snapshot document, called: document['userRef'].
First of all, how do I access the data of userRef? Using document['userRef'].get().data or document['userRef'].get().username I wasn't able to access the data. (NoSuchMethodError: Class 'Future<DocumentSnapshot>' has no instance getter 'data')
I also tried using document['userRef'].get().then(...) but getting the error: type 'Future<dynamic>' is not a subtype of type 'String'
Even if .then would work, wouldn't it then look up the same reference again for each message? Here the database is updated in realtime, but it's unnecessary to make the same lookup for multiple messages in the ListView.
class MessageList extends StatelessWidget {
MessageList({this.firestore});
final Firestore firestore;
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: firestore.collection('messages').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return const Text('Loading...');
final int messageCount = snapshot.data.documents.length;
return ListView.builder(
itemCount: messageCount,
itemBuilder: (_, int index) {
final DocumentSnapshot document = snapshot.data.documents[index];
// document['userRef'] exists here
return ListTile(
title: Text(document['message'] ?? '<No message retrieved>'),
subtitle: Text('Message ${index + 1} of $messageCount'),
);
},
);
},
);
}
}
Edit:
I was able to fetch the nested data using FutureBuilder, though not sure how efficient it is. (Wouldn't this possibly send loads of redundant requests to Firebase?)
Creating a widget for the nested data, where document['userRef'] exists:
FutureBuilder(
future: userData(document['userRef']),
builder: (BuildContext context,
AsyncSnapshot<dynamic> uData) {
return Text(uData.data['username']);
},
);
And the userData function looks like this:
Future<dynamic> userData(DocumentReference user) async {
DocumentSnapshot userRef = await user.get();
return userRef.data;
}
Sticking to the Firebase and Flutter way, it is possible to use a Streambuilder inside a Streambuilder. That is, instead of using a FutureBuilder for the nested data, which makes you wait for each .get request.
(The code is untested, but the principle is tested.)
class MessageList extends StatelessWidget {
MessageList({this.firestore});
final Firestore firestore;
#override
Widget build(BuildContext context) {
Map UserSnapshot = Map(); // create a variable for accessing users by id
return StreamBuilder<QuerySnapshot>(
stream: firestore.collection('users').snapshots(),
builder:
(BuildContext context, AsyncSnapshot<QuerySnapshot> UsersSnapshot) {
// process usersnapshot from list to map
UsersSnapshot.data.documents.forEach((userRecord) {
//print(optionRecord.documentID); // debug
UserSnapshot[userRecord.documentID] = userRecord;
});
// user data can be accessed as soon as there is a reference field or documentID:
// UserSnapshot[document['userRef']]['userName'}
return StreamBuilder<QuerySnapshot>(
stream: firestore.collection('messages').snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> MessagesSnapshot) {
if (!MessagesSnapshot.hasData) return const Text('Loading...');
final int messageCount = MessagesSnapshot.data.documents.length;
return ListView.builder(
itemCount: messageCount,
itemBuilder: (_, int index) {
final DocumentSnapshot document =
MessagesSnapshot.data.documents[index];
// document['userRef'] exists here
// UserSnapshot[document['userRef']]['userName'} is accessible here
return ListTile(
title:
Text(document['message'] ?? '<No message retrieved>'),
subtitle: Text('Message ${index + 1} of $messageCount'),
);
},
);
},
);
});
}
}

Resources