Getting null on Query snapshot - firebase

I ma getting a null return on a stream query. The funny thing is that data came through but on processing it to use it in the app it gets lost somewhere.I know i probably made a silly mistake somewhere yet i've been looking at this issue for three days now. Please help.
Here is the stream
Stream <SellerProfile> get sellerProfile {
return sellerProfileCollection.document(uid).snapshots()
.map(yieldSellerProfile);
}
SellerProfile yieldSellerProfile(DocumentSnapshot snapshot) {
print(snapshot.data['shopName']);
return SellerProfile(
shopName: snapshot.data['shopName'] ?? '',
phone: snapshot.data['phone']??'',
credit: snapshot.data['credit'] ?? '',
posts: snapshot.data['posts'] ?? '',
sales: snapshot.data['sales'] ?? '',
avatarUrl: snapshot.data['avatarUrl'] ?? '',
location:snapshot.data['location'] ?? '',
rating: snapshot.data['rating'] ?? '',
joinedDate: snapshot.data['joinedDate'] ?? '',
);
}
My idea is that after querying the stream sellerProfile i want to map it into a custom model to use it in the app.
As in the code, i print the snapshot.data['shopName'] before it is processed and i get the output
I/flutter ( 1008): Soko
which means the data arrives from firestore but as i try to access the data on my frontend i receive a null
Here is the frontend
Widget build(BuildContext context) {
final AuthService _auth = AuthService();
final user = Provider.of<User>(context);
return StreamBuilder<SellerProfile>(
stream: SellerDatabaseService(uid: user.uid).sellerProfile,
builder: (context, snapshot) {
SellerProfile profile=snapshot.data;
print(profile);
return Scaffold(
backgroundColor: Colors.white,
appBar: header(context,strTitle: "Profile"),
body: SingleChildScrollView(),
);
}
);
}
and here is the output when i try to print the profile
I/flutter ( 1008): null
Where am i going wrong? Thanks in advance!

I go the issue. I was trying to build the stream with stream builder instead of returning it from a provider.
So i changed this...
Widget build(BuildContext context) {
final AuthService _auth = AuthService();
final user = Provider.of<User>(context);
return StreamBuilder<SellerProfile>(
stream: SellerDatabaseService(uid: user.uid).sellerProfile,
builder: (context, snapshot) {
SellerProfile profile=snapshot.data;
print(profile);
return Scaffold(
backgroundColor: Colors.white,
appBar: header(context,strTitle: "Profile"),
body: SingleChildScrollView(),
);
}
);
}
To this...
return StreamProvider<BuyerProfile>.value(
value: BuyerDatabaseService(uid: user.uid).buyerProfile,
builder: (context, snapshot) {
BuyerProfile profile=Provider.of<BuyerProfile>(context);
if(profile!=null){
return Scaffold(...

You're not checking if the snapshot has data yet. Add a check for this with the hasData property of the AsyncSnapshot:
return StreamBuilder<SellerProfile>(
stream: SellerDatabaseService(uid: user.uid).sellerProfile,
builder: (context, snapshot) {
if(snapshot.hasError) {
return Text(snapshot.error.toString());
}
if(!snapshot.hasData) {//Check if the snapshot actually has data
return CircularProgressIndicator();
}
SellerProfile profile=snapshot.data;
print(profile);
return Scaffold(
backgroundColor: Colors.white,
appBar: header(context,strTitle: "Profile"),
body: SingleChildScrollView(),
);
}
);
Ideally you should also check if it hasError as well, and if you want more granular control over what to show, you could use the connectionState.

Related

The argument type 'Object?' can't be assigned to the parameter type 'DocumentSnapshot'

I just updated to Dart2 and Flutter sdk: '>=2.12.0 <3.0.0' and now I got an error:
return Scaffold(
body: FutureBuilder(
future: usersRef.doc(widget.accountiD).get(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return buildLoading();
}
UserAccount currentUser = UserAccount.fromDocument(snapshot.data);
return ListView(
children: []
);
}
),
);
The error was on the snapshot.data saying The argument type 'Object?' can't be assigned to the parameter type 'DocumentSnapshot'.
What to do? I need help.
Change it to this:
return Scaffold(
body: FutureBuilder<DocumentSnapshot>(
future: usersRef.doc(widget.accountiD).get(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return buildLoading();
}
UserAccount currentUser = UserAccount.fromDocument(snapshot.data.data()); //you need to add "data()" to access the map of objects inside snapshot.data
return ListView(
children: []
);
}
),
);
I had the same problem a while ago and just had it again and had forgotten the solution which brought me here.
The solution I had found was using the "as" keyword
Example
snapshot.data as int
or
snapshot.data as String
Just do the same with any other data type.
Hope it helps. Answer sent from 🇵🇷.
return Scaffold(
body: FutureBuilder<DocumentSnapshot>(
future: usersRef.doc(widget.accountiD).get(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return buildLoading();
}
UserAccount currentUser = UserAccount.fromDocument(snapshot.data!); //(Add after snapshot.data)
return ListView(
children: []
);
}
),
);
You may try UserAccount.fromDocument(snapshot as DocumentSnapshot); This kind of casting may help to convert your need of Object convertion of DocumentSnapshot.
Working code in 2022. It listens to changes on the document and updates it:
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseFirestore.instance
.collection('tasks')
.doc('B2tRytXodOwoYA1U4gK2')
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return CircularProgressIndicator();
}
TaskModel task =
TaskModel.fromSnapshot(snapshot.data as DocumentSnapshot);
return Text(task.category);
});
}
}
You need a model like following:
import 'package:cloud_firestore/cloud_firestore.dart';
class TaskModel {
String _category = '';
String get category => _category;
TaskModel.fromSnapshot(DocumentSnapshot snapshot) {
_category = snapshot['category'];
}
}
Or you can get values as a map instead without having a class.

Flutter/Firestore/Provider - Error shown for split second then stream displayed, how can I load stream values on startup?

I am using a Stream Provider to access Firestore data and pass it around my app. The problem I am facing starts when I first run the app. Everything starts as normal but as I navigate to the screen where I am using the Stream values in a list view, I initially get an error before the UI rebuilds and the list items appear after a split second. This is the error I get:
════════ Exception caught by widgets library ═══════════════════════════════════
The following NoSuchMethodError was thrown building OurInboxPage(dirty, dependencies: [_InheritedProviderScope<List<InboxItem>>]):
The getter 'length' was called on null.
Receiver: null
Tried calling: length
I'm guessing this has something to do with the load time to access the values and add them to the screen? How can I load all stream values when the app starts up to avoid this?
Here is my Stream code:
Stream<List<InboxItem>> get inboxitems {
return orderCollection
.where("sendTo", isEqualTo: FirebaseAuth.instance.currentUser.email)
.snapshots()
.map(
(QuerySnapshot querySnapshot) => querySnapshot.docs
.map(
(document) => InboxItem.fromFirestore(document),
)
.toList(),
);
}
I then add this to my list of Providers:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
MultiProvider(
providers: [
StreamProvider<List<InboxItem>>.value(value: OurDatabase().inboxitems),
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<OurUser>(
builder: (_, user, __) {
return MaterialApp(
title: 'My App',
theme: OurTheme().buildTheme(),
home: HomepageNavigator(),
);
},
);
}
}
And finally the page I want to display the stream items:
class OurInboxPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
List<InboxItem> inboxList = Provider.of<List<InboxItem>>(context);
return Scaffold(
body: Center(
child: ListView.builder(
itemCount: inboxList.length,
itemBuilder: (context, index) {
final InboxItem document = inboxList[index];
return Card(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(document.event),
Icon(Icons.arrow_forward_ios)
],
),
);
},
),
),
);
}
}
Thanks
Yeah its trying to build before the data is populated, hence the null error.
Wrap your ListView.builder in a StreamBuilder and having it show a loading indicator if there's no data.
StreamBuilder<List<InboxItem>>(
stream: // your stream here
builder: (context, snapshot) {
if (snapshot.hasData) {
return // your ListView here
} else {
return CircularProgressIndicator();
}
},
);
I'm assuming your not using the latest version of provider because the latest version requires StreamProvider to set initialData.
If you really want to use StreamProvider and don't want a null value, just set its initialData property.
FROM:
StreamProvider<List<InboxItem>>.value(value: OurDatabase().inboxitems),
TO:
StreamProvider<List<InboxItem>>.value(
value: OurDatabase().inboxitems,
initialData: <InboxItem>[], // <<<<< THIS ONE
),
If you want to display some progress indicator while getter function inboxitems is executed initially. You don't need to modify the StreamProvider, and just add a null checking in your OurInboxPage widget.
class OurInboxPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
final List<InboxItem>? inboxList =
Provider.of<List<InboxItem>?>(context, listen: false);
return Scaffold(
body: inboxList == null
? const CircularProgressIndicator()
: ListView.builder(
itemCount: inboxList.length,
itemBuilder: (_, __) => Container(
height: 100,
color: Colors.red,
),
),
);
}
}
There are 2 ways to solve the issue.
Use the progress bar while the data is loading.
StreamBuilder<int>(
stream: getStream(),
builder: (_, snapshot) {
if (snapshot.hasError) {
return Text('${snapshot.error}');
} else if (snapshot.hasData) {
return Text('${snapshot.data}');
}
return Center(child: CircularProgressIndicator()); // <-- Use Progress bar
},
)
Provide dummy data initially.
StreamBuilder<int>(
initialData: 0, // <-- Give dummy data
stream: getStream(),
builder: (_, snapshot) {
if (snapshot.hasError) return Text('${snapshot.error}');
return Text('${snapshot.data}');
},
)
Here, getStream() return Stream<int>.

How to acces data from StreamProvider Flutter

I'm trying to show a list of reminder where user see all their reminders and able to edit it. I'm using StreamProvider so that I'm able to access in child widget. I map the DocumentSnapShot into MedReminder object. The StreamProvider should return List of MedReminder object. But in the ReminderList widget the Provider return null.
Stream:
List<MedReminder> _medReminderListFromSnapshot(QuerySnapshot snapshot) {
print('here is db');
return snapshot.documents.map((doc) {
return MedReminder(
remindId: doc.data['remindId'] ?? '',
howManyTimeDay: doc.data['howManyTimeDay'] ?? '',
frequency: doc.data['frequecy'] ?? '',
hour: doc.data['hour'] ?? '',
min: doc.data['min'] ?? '',
dateStarted: doc.data['dateStarted'] ?? '',
dateEnded: doc.data['dateEnded'] ?? '',
dateRefill: doc.data['dateRefill'] ?? '',
quantityTime: doc.data['quantityTime'] ?? '',
);
}).toList();
}
Stream<List<MedReminder>> get medReminders {
return userCollection.document(uid).collection('medic_reminder').snapshots()
.map(_medReminderListFromSnapshot);
}
Reminder Page:
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
return StreamProvider<List<MedReminder>>.value(
value: DatabaseService(uid: user.uid).medReminders,
child: Scaffold(
body: Container(
child: ReminderList(),
),
),
);
}
Reminder List widget(here the provider returning null):
Widget build(BuildContext context) {
final reminders = Provider.of<List<MedReminder>>(context) ?? [];
reminders.forEach((reminder) {
print(reminder.remindId);
});
return ListView.builder(
itemCount: reminders.length,
itemBuilder: (context, index) {
return ReminderTile();
});
}
Please help me thanks.
Stream<List<MedReminder>> get medReminders {
return userCollection.snapshots()
.map(_medReminderListFromSnapshot);
}
Store Data In userCollection with uid, dont add other collections
like
collectionReference userCollection = Firestore.instance.collection('users');
when saving data in 'users'
userCollection.document(uid).setData()..
then it works

StreamBuilder in Flutter stuck with ConnectionState.waiting and displays only the loading mark

Hi I am trying to display the data inside the Firebase documents into my Flutter dynamically where they get rendered using a loop, so I made a List<Widget> Cards and added to it the function makeItem() that contains the cards, and put them inside a loop, so the problem is that when I run the code it outputs print(snapshot.connectionState); as ConnectionState.waiting all the time and it should be async snapshot yet it refuses to load the data as required, I should mention that the data is display as wanted when I hit "Hot reload in Android Studio" .
so I don't know how resolve this issue. Thanks in Advance
I had the same problem when using STREAM BUILDER with PROVIDER&CHANGE NOTIFIER.
When returning back to the view, one should re-assign the stream itself.
Make a get function for your stream and in that function before returning your stream re-assign the stream. That solved the problem of loading issue for me.
Can you try the following?
class MyList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _firestore.collection(widget.city).snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError)
return Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.waiting: return Center(child: CircularProgressIndicator(backgroundColor: Colors.amber,strokeWidth: 1),),
default:
return ListView(
children: snapshot.data.documents.map((DocumentSnapshot document) {
return makeItem(
pointName: document['name'],
huge: document['lastname'],
moderate: document['mobileNumber'],
none: document['location'],
fights: document['job'],
);
}).toList(),
);
}
},
);
}
}
I think I got something for you try this out. It works on my emulator.
List<Widget> cards = [];
Stream<QuerySnapshot> firebaseStream;
#override
void initState() {
super.initState();
firebaseStream = Firestore.instance.collection('Hearings').snapshots();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: StreamBuilder<QuerySnapshot>(
stream: firebaseStream,
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> asyncSnapshot) {
List<DocumentSnapshot> snapData;
if (asyncSnapshot.connectionState == ConnectionState.waiting) {
return Container(
child: Center(
child: CircularProgressIndicator(
backgroundColor: Colors.amber,
strokeWidth: 1,
),
),
);
} else if (asyncSnapshot.connectionState ==
ConnectionState.active) {
snapData = asyncSnapshot.data.documents;
if (asyncSnapshot.hasData) {
for (int i = 0; i < snapData.length; i++) {
Widget card = Text(snapData[i].data['locationName']);
cards.add(card);
}
}
}
return ListView.builder(
itemCount: cards.length,
itemBuilder: (context, index) => cards[index],
);
},
),
),
);
I got bad news too though now that the data is updating it exposed some flaws in your logic its duplicating old entries in your array. You'll see. That should be easy to fix though.
Using stream.cast() on StreamBuilder solved my problem.

Flutter: Correct approach to get value from Future

I have a function which returns images directory path, it performs some additional check like if directory exists or not, then it behaves accordingly.
Here is my code:
Future<String> getImagesPath() async {
final Directory appDir = await getApplicationDocumentsDirectory();
final String appDirPath = appDir.path;
final String imgPath = appDirPath + '/data/images';
final imgDir = new Directory(imgPath);
bool dirExists = await imgDir.exists();
if (!dirExists) {
await new Directory(imgPath).create(recursive: true);
}
return imgPath;
}
This piece of code works as expected, but I'm having issue in getting value from Future.
Case Scenario:
I have data stored in local database and trying to display it, inside listview. I'm using FutureBuilder, as explained in this answer. Each data row has an image connected with it (connected means, the image name is stored in db).
Inside Widget build method, I have this code:
#override
Widget build(BuildContext context) {
getImagesPath().then((path){
imagesPath = path;
print(imagesPath); //prints correct path
});
print(imagesPath); //prints null
return Scaffold(
//removed
body: FutureBuilder<List>(
future: databaseHelper.getList(),
initialData: List(),
builder: (context, snapshot) {
return snapshot.hasData
? ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (_, int position) {
final item = snapshot.data[position];
final image = "$imagesPath/${item.row[0]}.jpg";
return Card(
child: ListTile(
leading: Image.asset(image),
title: Text(item.row[1]),
subtitle: Text(item.row[2]),
trailing: Icon(Icons.launch),
));
})
: Center(
child: CircularProgressIndicator(),
);
}));
}
Shifting return Scaffold(.....) inside .then doesn't work. Because widget build returns nothing.
The other option I found is async/await but at the end, same problem, code available below:
_getImagesPath() async {
return await imgPath();
}
Calling _getImagesPath() returns Future, instead of actual data.
I beleive there is very small logical mistake, but unable to find it myself.
I see that you have to build your widget from the output of two futures. You can either use two FutureBuilders or have a helper method to combine them into one simplified code unit.
Also, never compute/invoke async function from build function. It has to be initialized before (either in constructor or initState method), otherwise the widget might end up repainting itself forever.
Coming to the solution: to simplify code, it is better to combine both future outputs into a single class as in the example below:
Data required for build method:
class DataRequiredForBuild {
String imagesPath;
List items;
DataRequiredForBuild({
this.imagesPath,
this.items,
});
}
Function to fetch all required data:
Future<DataRequiredForBuild> _fetchAllData() async {
return DataRequiredForBuild(
imagesPath: await getImagesPath(),
items: await databaseHelperGetList(),
);
}
Now putting everything together in Widget:
Future<DataRequiredForBuild> _dataRequiredForBuild;
#override
void initState() {
super.initState();
// this should not be done in build method.
_dataRequiredForBuild = _fetchAllData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
//removed
body: FutureBuilder<DataRequiredForBuild>(
future: _dataRequiredForBuild,
builder: (context, snapshot) {
return snapshot.hasData
? ListView.builder(
itemCount: snapshot.data.items.length,
itemBuilder: (_, int position) {
final item = snapshot.data.items[position];
final image = "${snapshot.data.imagesPath}/${item.row[0]}.jpg";
return Card(
child: ListTile(
leading: Image.asset(image),
title: Text(item.row[1]),
subtitle: Text(item.row[2]),
trailing: Icon(Icons.launch),
));
})
: Center(
child: CircularProgressIndicator(),
);
},
),
);
}
Hope it helps.
Moving this piece of code inside FutureBuilder should resolve the issue.
getImagesPath().then((path){
imagesPath = path;
print(imagesPath); //prints correct path
});
So your final code should look like this:
#override
Widget build(BuildContext context) {
return Scaffold(
//removed
body: FutureBuilder<List>(
future: databaseHelper.getList(),
initialData: List(),
builder: (context, snapshot) {
return snapshot.hasData
? ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (_, int position) {
getImagesPath().then((path){
imagesPath = path;
});
final item = snapshot.data[position];
final image = "$imagesPath/${item.row[0]}.jpg";
return Card(
child: ListTile(
leading: Image.file(File(image)),
title: Text(item.row[1]),
subtitle: Text(item.row[2]),
trailing: Icon(Icons.launch),
));
})
: Center(
child: CircularProgressIndicator(),
);
}));
}
Hope it helps!

Resources