Flutter FutureBuilder with multiple features, check which future has no data - flutter

I have a FutureBuilder with multiple futures, how can I which one of the futures has no data so I can display the proper widget.
Basically I want to be able to do something like:
if snapshot.data[0] has no data display widgetOne
else if snapshot.data[1] has no data display widgetTwo
else if snapshot.data[2] has no data display widgetThree
I tried snapshot.data[0].toString().isEmpty == true, snapshot.data[0] == null. Either of those throws
'[]'
js_primitives.dart:30 Dynamic call of null.
js_primitives.dart:30 Receiver: null
js_primitives.dart:30 Arguments: [0]
Using !snapshot.hasData tells me there's no data in one of the future but I want to know which one specifically so I can return the proper widget.
My actual code:
FutureBuilder(
future: Future.wait([
FirestoreService().getUser(widget.username),
FirestoreService().getUserInventory(widget.username),
FirebaseRTDB().getAllItems()
]),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const AwaitingResultWidget();
} else if (snapshot.hasData) {
// Account data
final AccountModel user = snapshot.data[0];
// Inventory List
final List<InventoryItem> inventoryList = snapshot.data[1];
// Market Data
final Map<String, Item> itemMap = snapshot.data[2];
return Column(
children: [
Column(
children: [
kIsWeb ? webUserHeader(user) : androidUserHeader(user),
],
),
Center(
child: SingleChildScrollView(
child: Column(
children: [
Text('foo'),
],
),
),
)
],
);
} else if (!snapshot.hasData) {
if (snapshot.data[0] != null) {
return Container(
child: Text('foo1'),
);
}
return Container(
child: Text('foo'),
);
} else if (snapshot.hasError) {
print(snapshot.error);
return const SomethingWentWrongWidget();
} else {
return const UserNotFound();
}
},
),

You can add futures to variables and check its data like
Future<String> firstFuture = FirestoreService().getUser(widget.username);
Future<int> secondFuture = FirestoreService().getUserInventory(widget.username);
FutureBuilder(
future: Future.wait([firstFuture, secondFuture]),
builder: (context, AsyncSnapshot<List<dynamic>> snapshot) {
snapshot.data[0]; //first future
snapshot.data[1]; //second future
},
);

Related

FutureBuilder not populating value

In my main.dart I have an async function to get data from an URL.
getShopLength() async {
final queryParameters = {
'api_key': '123',
'user_id': '123',
'lat': '123',
'long': '123',
'km': '123',
};
var response = await http.get(Uri.https('google.de','getSth', queryParameters));
var jsonData = jsonDecode(response.body);
List<Shops> shops = [];
for(var x in jsonData) {
Shops shop = Shops(x['name'], x['slogan']);
shops.add(shop);
}
return shops.length;
}
In my home.dart I want to get the value from getShopLength() but I always get the error: type 'Future<dynamic> is not a subtype of type 'Future<String>?'
I try to save the return value into valueShop and pass it to buildRestaurantRow('Top Angebote', context, valueShop)
home.dart
#override
Widget build(BuildContext context) {
var valueShop = "0";
FutureBuilder<String>(
future: getShopLength(),
builder: (context, snapshot) {
if (snapshot.hasData) {
valueShop = snapshot.data;
}
return CircularProgressIndicator();
}
);
return Scaffold(
appBar: buildSearchBar(context),
body: Padding(
padding: const EdgeInsets.fromLTRB(10.0, 0, 10.0, 0),
child: ListView(
children: <Widget>[
SizedBox(height: 20.0),
buildRestaurantRow('Top Angebote', context, valueShop),
SizedBox(height: 10.0),
buildRestaurantList(context),
SizedBox(height: 10.0),
buildCategoryRow('Nach Kategorie', context),
SizedBox(height: 10.0),
buildCategoryList(context),
SizedBox(height: 20.0),
buildCategoryRow('Deine Favoriten', context),
SizedBox(height: 10.0),
buildFriendsList(),
SizedBox(height: 30.0),
],
),
),
);
}
What am I missing?
So the problem lies here:
FutureBuilder<String>(
future: getShopLength(),
Your future builder has a type of string, which means that the future should be of type Future<String>, but when you declared the function getShopLength, you did this:
getShopLength() async {
You did not give it a return type, because of that, the default return type is Future<dynamic>.
The obvious solution is giving the function a return type, but you have another problem:
The futurebuilder expects a string value, but the function returns a number, so which is it?
If you want to return a string of the length, you can just do this:
Future<String> getShopLength() async {
...
return shops.length.toString();
}
Or you can also change the futurebuilder's value to be int:
Future<int> getShopLength() async {
...
return shops.length;
}
...
int valueShop = 0;
FutureBuilder<int>(
future: getShopLength(),
builder: (context, snapshot) {
if (snapshot.hasData) {
valueShop = snapshot.data;
}
return CircularProgressIndicator();
},
);
Side note:
Ok, I have a couple of things to mention about your code:
First of all, on your getShopsLength function, you have two lists, jsonData and shops, you don't actually need both, you can just use one:
var jsonData = jsonDecode(response.body);
return jsonData.length // no need for the shops list.
Second of all, what's up with your builder code?? You first declare a FutureBuilder, but then completely ignore it and move on to a Scaffold widget? I believe the scaffold code should be inside the future builder, as it stands, you will never see the circular progress indicator:
From:
var valueShop = '0';
FutureBuilder<String>(
future: getShopLength(),
builder: (context, snapshot) {
if (snapshot.hasData) {
valueShop = snapshot.data;
}
return CircularProgressIndicator();
}
);
return Scaffold(...);
To:
return FutureBuilder<String>(
future: getShopLength(),
builder: (context, snapshot) {
if (snapshot.hasData) {
var valueShop = snapshot.data;
return Scaffold(...);
}
return CircularProgressIndicator();
}
);

How can I use a future list of object from database to trigger rebuild using flutter provider?

class StudentProvider extends ChangeNotifier {
DataBaseHelper data = DataBaseHelper();
List<StudentModel>? student = [];
List<StudentModel>? searchStudentList;
Future<List<StudentModel>> getStudentList() async {
student = await data.getStudent();
return student!;
}
void insert(StudentModel studentModel) async {
print("reached insert in provider class");
await data.insertStudent(studentModel);
notifyListeners();
}
}
The code above is the provider class of mine and I want to use the list that I get from getStudentList list method to build widgets. The problem is it returns a future because I am getting the data from the database.
body: TabBarView(children: [
Consumer<StudentProvider>(
builder: (context, StudentProvider student, ch) {
print("rebuild happened");
return ListView(
children: [
...student.getStudentList().map((e) {
return HomeScreen(student: e);
}).toList(),
],
);
}),
This is where I am trying to build widgets. I am unable to do so.
in getStudentList() you have used Future<List<StudentModel>> so that future can not access in listview.
> So please use streambuilder or remove the future from the list
class StreamBuilderUsage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Expanded(
child: Center(
child: Consumer<StudentProvider>(
builder: (context, StudentProvider student, ch) {
return StreamBuilder(
stream: student.getStudentList().asStream(),
builder: (context, snapshot){
print(snapshot.connectionState);
if(snapshot.hasData){
var data = snapshot.data as List<String>;
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index){
return Text(data[index]);
},
);
}else{
return CircularProgressIndicator();
}
},
);
}
),
),
),
],
),
);
}
}
The answer I accepted will also work and we can use Future builder for the same.
Here is the code:-
body: TabBarView(
children: [
Consumer<StudentProvider>(
builder: (context, StudentProvider student, ch) {
print("rebuild happened");
return FutureBuilder(
future: student.getStudentList(),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<StudentModel> data =
snapshot.data as List<StudentModel>;
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return HomeScreen(student: data[index]);
});
} else
return Container(child: Text("No data"));
},
);
},
),
Container(),//neglect this container
],
),

StreamBuilder is not showing data from firestore

I am using streambuilder to display snapshot data but it is not displaying. The screen is just blank but When I use the future builder with get() methode it display the data but I want realtime changes. I am new to flutter please help me with this. here is code.
class TalentScreen2 extends StatelessWidget {
final Query _fetchFavUser = FirebaseRepo.instance.fetchFavUsers();
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: Column(
children: [
Text('Talent Screen 2(Favourites)'),
Expanded(child: _retrieveData(context))
],
),
),
);
}
Widget _retrieveData(BuildContext context) => StreamBuilder<QuerySnapshot>(
stream: _fetchFavUser.snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) return const Text('Something went wrong');
if (!snapshot.hasData) return const Text('Alas! No data found');
if (snapshot.connectionState == ConnectionState.waiting)
return Center(
child: CircularProgressIndicator(
strokeWidth: 2.0,
));
if (snapshot.connectionState == ConnectionState.done)
return theUserInfo(snapshot.data.docs);
return Container();
});
Widget theUserInfo(List<QueryDocumentSnapshot> data) {
return ListView.builder(
shrinkWrap: true,
itemCount: data.length,
itemBuilder: (BuildContext context, int index) {
var uid = data[index]['uid'];
TalentHireFavModel userData = TalentHireFavModel.fromMap(
data[index].data(),
);
return Card(
child: Column(
children: <Widget>[
Text(data[index]['orderBy']),
// Text(userData.name ?? ''),
Text(userData.categories),
Text(userData.skills),
// Text(userData.country ?? ''),
Text(userData.phoneNo),
Text(userData.hourlyRate),
Text(userData.professionalOverview),
Text(userData.skills),
Text(userData.expert),
// Text(userData.createdAt ?? ''),
_iconButton(userData.uid, context),
],
),
);
});
}
Future<DocumentSnapshot> fetch(data) async =>
await FirebaseRepo.instance.fetchWorkerUserData(data);
Widget _iconButton(uid, context) {
return IconButton(
icon: Icon(Icons.favorite),
onPressed: () {
BlocProvider.of<TalentFavCubit>(context).removeTalentFav(uid);
});
}
}
and here is the firestore query methode where I am just applying simple query to fetch all documents and display them. I want real-time changes
Query fetchFavUsers() {
var data = _firestore
.collection('workerField')
.doc(getCurrentUser().uid)
.collection('favourites')
// .where('uid', isNotEqualTo: getCurrentUser().uid)
.orderBy('orderBy', descending: true);
return data;
}
The solution is to just return the function. Get that method out of if statement and place it in just return statement.

How to check if current user is an admin in Flutter using Firestore and FirebaseAuth

I want to check if my user is an admin and display a widget depending on that. My data is stored in Firestore such that there is a document in a 'users' collection with a user ID that contains an 'isAdmin' field. My current code looks like this:
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
children: [
FutureBuilder(
future: FirebaseAuth.instance.currentUser(),
builder: (ctx, futureSnapshot) {
if (futureSnapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
final uid = futureSnapshot.data.uid;
return StreamBuilder(
stream: Firestore.instance
.collection('users')
.document(uid)
.snapshots(),
builder: (ctx, userSnapshot) {
if (userSnapshot.connectionState ==
ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
var userDocument = userSnapshot.data;
if (userDocument['isAdmin'] == true) {
return FloatingActionButton(
onPressed: () {},
);
}
},
);
},
),
],
),
),
);
}
However, this code runs and gives me an error that my build function returned null. How can I check if my current user is an admin if that data is stored in Firestore
Update your code to this
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
children: [
FutureBuilder(
future: FirebaseAuth.instance.currentUser(),
builder: (ctx, futureSnapshot) {
if (futureSnapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
final uid = futureSnapshot.data.uid;
return StreamBuilder(
stream: Firestore.instance
.collection('users')
.document(uid)
.snapshots(),
builder: (ctx, userSnapshot) {
if (userSnapshot.connectionState ==
ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
var userDocument = userSnapshot.data.data();
if (userDocument['isAdmin'] == true) {
return FloatingActionButton(
onPressed: () {},
);
}
},
);
},
),
],
),
),
);
}
Change 'var userDocument = userSnapshot.data;' to var userDocument = userSnapshot.data.data();

How check the ConnectionState of Firestore snapshot stream with StreamProvider?

This example from the cloud_firestore documentation uses a StreamBuilder and the ConnectionState of an AsyncSnapshot to handle the stream in its different states. Is there a similar way to manage the ConnectionState when accessing the stream via a StreamProvider instead of a StreamBuilder? What is the best way of avoiding it to return null in the short while until it actually has documents from Firestore?
Here the example from the cloud_firestore docs with the StreamBuilder:
class BookList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('books').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.waiting: return new Text('Loading...');
default:
return new ListView(
children: snapshot.data.documents.map((DocumentSnapshot document) {
return new ListTile(
title: new Text(document['title']),
subtitle: new Text(document['author']),
);
}).toList(),
);
}
},
);
}
}
I have a rather basic stream:
List<AuditMark> _auditMarksFromSnapshot(QuerySnapshot qs) {
return qs.documents.map((DocumentSnapshot ds) {
return AuditMark.fromSnapshot(ds);
}).toList();
}
Stream<List<AuditMark>> get auditMarks {
return Firestore.instance
.collection('_auditMarks')
.snapshots()
.map(_auditMarksFromSnapshot);
}
This is accessed via a StreamProvider (have omitted other providers here):
void main() async {
runApp(MultiProvider(
providers: [
StreamProvider<List<AuditMark>>(
create: (_) => DatabaseService().auditMarks, ),
],
child: MyApp(),
));
}
I have tried somehow converting the QuerySnapshot to an AsyncSnapshot<QuerySnapshot> but probably got that wrong.
Could of course give the StreamProvider some initialData like so - but this is cumbersome, error prone and probably expensive:
initialData: <AuditMark>[
AuditMark.fromSnapshot(await Firestore.instance
.collection('_auditMarks')
.orderBy('value')
.getDocuments()
.then((value) => value.documents.first))
...but I am hoping there is a smarter way of managing the connection state and avoiding it to return null before it can emit documents?
I have been dealing with this and didn't want to declare an initialData to bypass this issue.
What I did was creating a StreamBuilder as the child of StreamProvider.
So that I could use the snapshot.connectionState property of StreamBuilder in the StreamProvider.
Here's the code:
return StreamProvider<List<AuditMark>>.value(
value: DatabaseService().auditMarks,
child: StreamBuilder<List<AuditMark>>(
stream: DatabaseService().auditMarks,
builder: (context, snapshot) {
if (!snapshot.hasError) {
switch (snapshot.connectionState) {
case ConnectionState.none: // if no connection
return new Text(
"Offline!",
style: TextStyle(fontSize: 24, color: Colors.red),
textAlign: TextAlign.center,
);
case ConnectionState.waiting
// while waiting the data, this is where you'll avoid NULL
return Center(child: CircularProgressIndicator());
default:
return ListView.builder(
// in my case I was getting NULL for itemCount
itemCount: logs.length,
itemBuilder: (context, index) {
return LogsTile(log: logs[index]);
},
);
}
}
else {
return new Text(
"Error: ${snapshot.error}",
style: TextStyle(fontSize: 17, color: Colors.red),
textAlign: TextAlign.center,
);
}
}
)
);
Probably not the most elegant solution, but I ended up using a simple bool variable which is true while not all StreamProviders have emitted values.
bool _waitForStreams = false;
if (Provider.of<List<AuditMark>>(context) == null) _waitForStreams = true;
if (Provider.of<...>>(context) == null) _waitForStreams = true;
(etc. repeat for every StreamProvider)
// show "loading..." message while not all StreamProviders have supplied values
if (_waitForStreams) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 25.0),
Text('loading...'),
],
)
],
),
);
}
I don't know if it's correct but this is how I implement it.
Since streamProviser does not provide a connection state, I first use streamBuilder and then provider.value to distribute the data:
return StreamBuilder<BusinessM>(
stream: db.businessDetails(), //firebase stream mapped to business model class
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active)
return Provider<BusinessM>.value(
value: snapshot.data,
child: Businesspage(),
);
else
return Center(child: CircularProgressIndicator());
});
For someone who want to use StreamProvider but end up with no ConnectionState state to use. For some of the cases, null represent the state of "waiting for the first data", not "no data".
In StreamProvider, there is no build-in method to detect the state. But we can still warp the state outside of the data:
StreamProvider<AsyncSnapshot<QuerySnapshot?>>.value(
initialData: const AsyncSnapshot.waiting(),
value: FirebaseFirestore.instance
.collection('books')
.snapshots()
.map((snapshot) {
return AsyncSnapshot.withData(ConnectionState.active, snapshot);
}),
child: ...,
);
or for firebaseAuth:
StreamProvider<AsyncSnapshot<User?>>.value(
initialData: const AsyncSnapshot.waiting(),
value: FirebaseAuth.instance.userChanges().map(
(user) => AsyncSnapshot.withData(ConnectionState.active, user),
),
),