Flutter DropdownButton - populate items from rest webservice - asynchronous

My fetchCities() method returns Future<List<City>> and it loads data from rest webservice. Code to populate items:
Widget buildCitiesSelector(){
return new Center(
child: FutureBuilder(
future: fetchCities() ,
builder: (context, snapshot){
if (snapshot.hasData) {
return new DropdownButton <City>(
hint: Text('Wybierz miasto'),
items: snapshot.data.map<DropdownMenuItem<City>>((City value) {
return DropdownMenuItem<City>(
value: value,
child: Text(value.name),
);
}).toList(),
onChanged: (value) {
setState(() {_selectedCity = value;});
},
value: _selectedCity,
);
}
else{
return CircularProgressIndicator();
}
}
)
);
}
Result: items are correctly displayed in selector. However, when selecting any particular item, I'm getting exception:
I/flutter (13910): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY
╞═══════════════════════════════════════════════════════════ I/flutter
(13910): The following assertion was thrown building
FutureBuilder>(dirty, state: I/flutter (13910):
_FutureBuilderState>#dacd9): I/flutter (13910): 'package:flutter/src/material/dropdown.dart': Failed assertion: line
560 pos 15: 'items == null || I/flutter (13910): items.isEmpty ||
value == null || items.where((DropdownMenuItem item) => item.value
== I/flutter (13910): value).length == 1': is not true.
How to properly select item? Any ideas what's wrong?

You should not use FutureBuilder for this situation. Rather fetch the data in initState() and then cause a rebuild using setState() to update the view.
If fetchCities() creates a new Future every time it is called, then build() will invoke that fetch every time the UI is rebuilt (which can be quite often)
https://docs.flutter.io/flutter/widgets/FutureBuilder-class.html
The future must have been obtained earlier, e.g. during State.initState, ...

child: FutureBuilder(
future: Webservice().load(Country.countries) ,
builder: (context, snapshot){
if(snapshot.hasError)
return Text(snapshot.error);
if (snapshot.hasData) {
return DropdownButtonFormField(
decoration: new InputDecoration(icon: Icon(Icons.language)), //, color: Colors.white10
value: selectedCountry,
items: snapshot.data.map<DropdownMenuItem<Country>>((Country country) {
return DropdownMenuItem<Country>(
value: country,
child: Text(country.name, style: TextStyle(color: Color.fromRGBO(58, 66, 46, .9))),
);
})
.toList(),
onChanged: (Country newValue) {
setState(() => selectedCountry = newValue);
// selectedCountry = newValue;
print(newValue.id);
print(newValue.name);
},
);
}
return CircularProgressIndicator();

I had the same issue today, and after some digging, I found a mistake in my code: the value in the DropdownButton wasn't in the items list.
I assumed (wrongly) that the dropdown would handle the "empty" value - but that is not the case.

You can declare a Future and init in initState and in FutureBuilder use this future.
City cityModel;
Future _future;
#override
void initState() {
_future = fetchCities();
super.initState();
}
body: FutureBuilder<List<City>>(
future: _future,

Related

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...'),
),

Flutter cloud firestore read data with reference

Firestore collection chat has a field user_id it's type is reference and it refers to users collection document id. I want to use this reference to get user information from user collection like join in sql.
What I'm getting in my app.
{message: Hello Zia, user_id: DocumentReference(users/mfzfwKvXo5M5dFFuqKeM), event_id: DocumentReference(events/J3HTFRpL0HEbmMV2BSSb), time: Timestamp(seconds=1618466409, nanoseconds=0)}
My Code
ListView(
children: snapshot.data.docs.map((DocumentSnapshot document) {
print(document.data()['user_id']);
try {
print(document.data()['user_id'].toString());
} catch (e) {}
return new ListTile(
leading: FutureBuilder<DocumentSnapshot>(
future: user.doc(**document.data()['user_id']**).get(),
// document.data()['user_id]) it's a DocumentReference.
builder: (BuildContext context,
AsyncSnapshot<DocumentSnapshot> snapshot) {
if (snapshot.hasError) {
return Text("Something went wrong");
}
if (snapshot.connectionState == ConnectionState.done) {
Map<String, dynamic> data = snapshot.data.data();
return Text(
"Full Name: ${data['full_name']} ${data['last_name']}");
}
return Text("loading");
},
),
subtitle: new Text(document.data()['message']),
// subtitle: Text(document.data()['time']),
);
}).toList(),
);
document.data()['user_id]) type 'DocumentReference' is not a subtype of type 'String'
I got the solution I'm posting solution it may helps others
document.data()['user_id].path
path properties return the path of the document

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>.

Getting data as null from snapshot.data in Flutter

I can't find the reason for getting this error cuz there is data stored on firestore and i've also handled possible exceptions. Th StreamBuilder QuerySnapshot Widget is throwing exception. How could i tackle this problem plzz help
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
class AdminScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
CollectionReference users = FirebaseFirestore.instance.collection('complaints');
return StreamBuilder<QuerySnapshot>(
stream: users.snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Text("Loading");
}
return ListView(
children: snapshot.data.docs.map((DocumentSnapshot document) {
return new ListTile(
title: new Text(document.data()['Name']),
subtitle: new Text(document.data()['Complaint']),
);
}).toList(),
);
},
);}
}
Error:
A non-null String must be provided to a Text widget.
'package:flutter/src/widgets/text.dart':
Failed assertion: line 370 pos 10: 'data != null'
by StreamBuilder<QuerySnapshot> Widget
Firestore SS:
StreamBuilder will emit null as the first event if no initialData is given. To mitigate, you should first check if the data is null using snapshot.hasData before trying to read it such as:
if(!snapshot.hasData) {
return Center(child: CircularProgressIndicator()));
}
// from here on you can access the data after you have checked it's not null:

Firebase Real Time Database triggers `null` error if there is no matching data

In my flutter code, I am trying to get data from the Firebase Real-Time Database. Below is my code.
final DatabaseReference reference = FirebaseDatabase.instance.reference().child('chat_room');
return Scaffold(
body: StreamBuilder(
stream:
reference.orderByChild("email").equalTo("abcd#test.com").onValue,
builder: (context, snapshot) {
if (snapshot == null || !snapshot.hasData) {
return Container(child: Center(child: Text("No data")));
} else {
Map<dynamic, dynamic> map = snapshot.data.snapshot.value;
return ListView.builder(
itemCount: map.values.toList().length,
itemBuilder: (context, index) {
String imageURL = map.values.toList()[index]["imageUrl"];
return Container(
margin: EdgeInsets.only(top: 10),
child: ListTile(
leading: CircleAvatar(
radius: 30.0,
backgroundImage: NetworkImage(imageURL),
backgroundColor: Colors.transparent,
),
title: Text(
map.values.toList()[index]["email"],
),
),
);
});
}
}),
);
Notice, I am loading data where the email is equal to abcd#test.com. The code works great if there are record for abcd#test.com. But if the database is empty or no records for abcd#test.com, I AM getting the below error
The following NoSuchMethodError was thrown building StreamBuilder<Event>(dirty, state: _StreamBuilderBaseState<Event, AsyncSnapshot<Event>>#ad47f):
The getter 'values' was called on null.
Receiver: null
Tried calling: values
The relevant error-causing widget was
StreamBuilder<Event>
package:xxx/…/chat/chat_list_supplier.dart:19
When the exception was thrown, this was the stack
#0 Object.noSuchMethod (dart:core-patch/object_patch.dart:51:5)
#1 _ChatListSupplierState.build.<anonymous closure>
package:xxx/…/chat/chat_list_supplier.dart:28
How can I fix this?
The problem is that there is a snapshot, but the snapshot contains no data. It's easiest to catch this in:
builder: (context, snapshot) {
if (snapshot == null || !snapshot.hasData || snapshot.data.snapshot.value == null) {
return Container(child: Center(child: Text("No data")));
} else {
...

Resources