I've got the following code from https://pub.dartlang.org/packages/cloud_firestore#-readme-tab-, but I'm not sure how to get each document's key. What I want to do is tap on each term to view or got to an edit page.
Firestore data model:
-content
--sPuJxAJu0dBMZLBTakd4
---term
---body content
Code:
class _TermsState extends State<Terms> {
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('content').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Text('Loading...');
default:
return ListView(
children:
snapshot.data.documents.map((DocumentSnapshot document) {
print(document['term']);
return ListTile(
title: Text(document['term']),
);
}).toList(),
);
}
},
);
}
}
When you have a DocumentSnapshot, you can use document.documentID to get its key and document.reference.path to get the whole path.
DocumentSnapshot.documentID
DocumentSnapshot.reference returns the DocumentReference for this snapshot, which can be used to (also) get the documentID and also the complete path of the document.
DocumentReference.documentID
DocumentReference.path
In this case document is an object of type DocumentSnapshot, which you already retrieve correctly.
An update to the above answer from creativecreatorormaybenot, the document ID can now be found in document.id from DocumentSnapshot. document.documentID will not return the id.
Here's a link to the answer I found
Related
In my application i want call data from firebase different collections. First I want to list all items and take the id.
Using that id i want to retrieve price from price collection. After that i want to retrieve data from discount. for taking discount.
Here i am using loops.
In the below code the output is coming. First loading list after that it calling second collection price.
Any one know the solution.
I want to listen for calling three collection. Because if any data change i want to update.
#override
void initState() {
super.initState();
_loadItems();
}
Future _loadItems() async {
int price;
int discount;
//calling first collection for getting id and name
firestore.collection("item").snapshots().listen((itemData)async{
for(int i=0;i<itemData.documents.length;i++){
// calling second collection for getting price
firestore.collection("price").where("id",isEqualTo: itemData.documents[i].data["id"])
.snapshots().listen((priceData) async{
price=priceData.documents[0].data['price'];
debugPrint("price showing before loading:"+price.toString());
//calling third collection for getting discount
firestore.collection("discount")
.where("id",isEqualTo: itemData.documents[i].data["id"])
.snapshots().listen((discountData) async{
for(int j=0;j<discountData.documents.length;j++){
discount=discountData.documents.data['discount'];
}
});
});
setState(() {
debugPrint("price showing after loading:"+price.toString());
this.documents.add(new CartProduct(
name:itemData.documents[i].data["id"],
label:itemData.documents[i].data["label"],
price:price,
discount:discount
));
});
}
});
}
Present output
price showing after loading:0
price showing after loading:0
price showing after loading:0
price showing before loading:10.0
price showing before loading:10.0
price showing before loading:10.0
Expected output
price showing before loading:10.0
price showing before loading:10.0
price showing before loading:10.0
price showing after loading:10.0
price showing after loading:10.0
price showing after loading:10.0
I thing you can use nested StreamBuilder's
Widget getTripleCollectionFromFirebase() {
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection("item").snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) return Text("Error: ${snapshot.error}");
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text("No data, yet.");
case ConnectionState.waiting:
return Text('Loading...');
case ConnectionState.active:
case ConnectionState.done:
if (snapshot.data == null) {
return Text("No record");
} else {
// Do your staff after first query then call the other collection
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance
.collection("price")
.where("id", isEqualTo: "fill_it_with_your_code")
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) return Text("Error: ${snapshot.error}");
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text("No data, yet.");
case ConnectionState.waiting:
return Text('Loading...');
case ConnectionState.active:
case ConnectionState.done:
if (snapshot.data == null) {
return Text("No record");
} else {
// do your staff after second Query
return StreamBuilder<QuerySnapshot>(
stream: Firestore.instance
.collection("discount")
.where("id", isEqualTo: "something")
.snapshots(),
builder: (BuildContext context,
AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError)
return Text("Error: ${snapshot.error}");
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text("No data, yet.");
case ConnectionState.waiting:
return Text('Loading...');
case ConnectionState.active:
case ConnectionState.done:
if (snapshot.data == null) {
return Text("No record");
} else {
// do your staff after third Query
// return the widget which you want to build when all data comes.
}
}
},
);
}
}
},
);
}
}
},
);
}
This is my code. I will explain it step by step so you can convert it to your's.
buildUserActions returns a StreamBuilder that StreamBuilder takes all documents which is in actions collection in cloud firestore. When ConnectionState is active, or done if I have data I assign it to variable named _lastActionDocuments.
QuerySnapshot _lastActionDocuments;
Stream<String> streamOfFillActionFields;
Widget buildUserActions() {
return StreamBuilder(
initialData: _lastActionDocuments,
stream: Firestore.instance.collection('actions').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return Center(
child: CircularProgressIndicator(),
);
case ConnectionState.active:
case ConnectionState.done:
if (snapshot.hasError)
return Center(child: Text('Error: ${snapshot.error}'));
if (!snapshot.hasData) return Text('No data finded!');
_lastActionDocuments = snapshot.data;
streamOfFillActionFields = fillActionFields();
return reallyBuildActions();
}
},
);
}
then I have a Stream function
Stream<String> fillActionFields() async* {
try {
List<ActionModel> newActionList = [];
for (DocumentSnapshot actionSnapshot in _lastActionDocuments.documents) {
var currentAction = ActionModel.fromSnapshot(actionSnapshot);
// I awaiting to get and fill all data.
await currentAction.fillAllFields();
newActionList.add(currentAction);
}
actionList = newActionList;
// what I yield is not important this case
yield 'data';
} catch (e) {
print(e);
yield 'nodata';
}
}
currentAction.fillAllFields basicly that function ask to firebase to get the related data to fill all fields in my Action Object.
Future<void> fillAllFields() async {
DocumentSnapshot ownerSnapshot = await ownerRef.get();
owner = UserModel.fromSnapshot(ownerSnapshot);
DocumentSnapshot routeSnapshot = await routeRef.get();
route = RouteModel.fromSnapshot(routeSnapshot);
}
then I have another widget which is returning a StreamBuilder. this widget build the real UI widget(buildAllActions) after all data arrived from reference calls.
Widget reallyBuildActions() {
return StreamBuilder(
stream: streamOfFillActionFields,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return Center(
child: CircularProgressIndicator(),
);
case ConnectionState.active:
case ConnectionState.done:
if (snapshot.data == 'data') {
return buildAllActions();
} else {
return Center(
child: Column(
children: <Widget>[
CircularProgressIndicator(),
Text('Data Loading...')
],
),
);
}
}
},
);
}
I have got answer Use StreamSubscription and call one by one. First I run one loop and check whether it is completed or not than after only call second loop. It working fine but taking delays. when I using StreamBuilder it not completing the request. I don't know why it happening. My code is shown below.
StreamSubscription<QuerySnapshot> streamSub1;
StreamSubscription<QuerySnapshot> streamSub2;
StreamSubscription<QuerySnapshot> streamSub3;
var list = new List();
_loadItems() {
int price;
int discount;
int count =1;
//calling first collection for getting id and name
streamSub1= firestore.collection("item").snapshots().listen((itemData)async{
for(int i=0;i<itemData.documents.length;i++){
list.add(id:itemData.documents[0].data['id'],name:itemData.documents[0].data['id');
if(onFavData.documents.length==productCount){
debugPrint("loop completed");
_loadPrice();
}
}
});
}
void _loadPrice(){
streamSub1.cancel();
int count =1;
for(int i=0;i<list.length;i++){
streamSub2= firestore.collection("price").where("id",isEqualTo: itemData.documents[i].data["id"])
.snapshots().listen((priceData) async{
list[i].price= priceData['price'];
if(count==list.length){
debugPrint("loop completed");
_loadDiscount();
}
});
}
}
_loadDiscount();{
streamSub2.cancel();
int count =1;
for(int i=0;i<list.length;i++){
streamSub3= firestore.collection("price").where("id",isEqualTo: itemData.documents[i].data["id"])
.snapshots().listen((priceData) async{
list[i].discount= priceData['price'];
if(count==list.length){
debugPrint("loop completed");
}
});
}
}
How to get X value from FutureBuilder?
final url = FutureBuilder<List<Url>>(
future: getUrlFromCache(),
builder: (context,snapshot){var x = snapshot.data[1].uniform_resource_locator;},
);
I think using 'x' will cause 'x' always will be equal to 'null'. It is possible to return it dynamically this way:
return ListTile(
title: FutureBuilder(
future: pair,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text( snapshot.data.title);
} else {
return Text( 'Loading...');
}
},
),
);
FutureBuilder always returns Widget type. Put your functionality inside the FutureBuilder.
In my case it returns Text widget. It is assigned to 'title:' and dynamically displayed on the screen as a ListTile.
Just replace the Text widget with the required widget that will use the data.
check this out : FutureBuilder
you can initilaize a global variable then assign a value inside the FutureBuilder but I dont know it's a good aproach doing this inside FutureBuilder.
// instead of creating dynamic types try to create static types.
var x;
FutureBuilder<String>(
future: _calculation, // a previously-obtained Future<String> or null
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('Press button to start.');
case ConnectionState.active:
case ConnectionState.waiting:
return Text('Awaiting result...');
case ConnectionState.done:
if (snapshot.hasError)
return Text('Error: ${snapshot.error}');
x = snapshot.data[1].uniform_resource_locator;
return Text('Result: ${snapshot.data}');
}
return null; // unreachable
},
)
I have a Future method like below:
Future<Map<String,String>> readFavorites() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
names = prefs.getKeys();
for (var key in names) {
debugPrint("key is " + key);
debugPrint("value is " + prefs.get(key));
pairs.putIfAbsent(key, () => prefs.get(key));
}
return pairs;
}
I want to get the snapshot length plus the map's values in the futurebuilder below:
Widget build(BuildContext ctxt) {
return Container(
child: FutureBuilder(
future: readFavorites(),
builder: (context, AsyncSnapshot<Map<String,String>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(
//replace this with a loading icon
child: new CircularProgressIndicator());
} else {
return ListView.builder(
itemExtent: 90,
itemCount: snapshot.data.length, <== How to get the map length?
itemBuilder: (BuildContext context, int index) {
return SingleDish(
dish_name: snapshot.data[index],
dish_picture: snapshot.data[index]., <== How to get the value from the map?
);
});
}
},
),
);
}
I tried the following but I got a null exception: snapshot.data[snapshot.data[index]]. Will appreciate any help.
UPDATE
What is interesting is that when I printed the key I got the following:
lib_cached_image_data_last_clean
Future<Map<String, String>> readFavorites() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
names = prefs.getKeys();
//This returned the correct value because I hardcoded the key
print("hardcoded key is " + prefs.getString("Cutlet"));
for (var key in names) {
//This fellow here returned lib_cached_image_data_last_clean
print("key is" + key);
pairs.putIfAbsent(key, () => prefs.get(key));
// print("key is " + pairs.length.toString());
}
return pairs;
}
So, I know for a fact that readFavorites() returns values. But am not sure why the key is not what I added in the SharedPreferences.
Take a look at this code it is auto explained and you can adapt this code to your needs.
Widget build(BuildContext ctxt) {
return Container(
child: FutureBuilder(
future: readFavorites(),
builder: (context, AsyncSnapshot<Map<String,String>> snapshot) {
switch( snapshot.connectionState){
case ConnectionState.none:
return Text("there is no connection");
case ConnectionState.active:
case ConnectionState.waiting:
return Center( child: new CircularProgressIndicator());
case ConnectionState.done:
if (snapshot.data != null){
Map<String,String> myMap = Map.from( snapshot.data ); // transform your snapshot data in map
var keysList = myMap.keys.toList(); // getting all keys of your map into a list
return ListView.builder(
itemExtent: 90,
itemCount: myMap.length, // getting map length you can use keyList.length too
itemBuilder: (BuildContext context, int index) {
return SingleDish(
dish_name: keysList[index], // key
dish_picture: myMap[ keysList[index] ], // getting your map values from current key
);
}
);
}
// here your snapshot data is null so SharedPreferences has no data...
return Text("No data was loaded from SharedPreferences");
}//end switch
},
),
);
}
I'm trying to figure out a way to indicate to a surrounding class when the FutureBuilder is done loading. RefreshIndicator takes a Future as a parameter and stops showing the refresh indicator when the Future completes. I don't have access to the exact same Future variable that's being passed to the FutureBuilder, especially when these are in two separate classes, unless I can pass a reference to one and when it completes in the other class, I'll know...
I'm searching for this answer too. Finally i figured it out...
Here is how i done
FutureBuilder<String>(
future: _calculation,
// a previously-obtained Future<String> or null
builder: (BuildContext context,
AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('Press button to start.');
case ConnectionState.active:
case ConnectionState.waiting:
return Text('Awaiting result...');
case ConnectionState.done:
if (snapshot.hasError)
return Text('Error: ${snapshot.error}');
return new RefreshIndicator(
key: _refreshIndicatorKey,
color: Colors.blue,
onRefresh: () {
return _calculation = getCalculation(); // EDITED
},
child: SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Text('Result: ${snapshot.data}')
)
);
break;
default:
return null;
}
},
)
Future<String> getCalculation() async {
try {
/*Write your API here or what ever u want to get when pull to refresh*/
return ""/*the value of api*/; //EDITED
} catch (e) {
///Handle Exception here. So in FutureBuilder we can capture it in snapshot.hasError
return Future.error(e.toString());
}
}
You have to access the snapshot, provided builder parm:
So, snapshot.data gives you the Future.
new FutureBuilder<String>(
future: _calculation, // a Future<String> or null
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none: return new Text('Press button to start');
case ConnectionState.waiting: return new Text('Awaiting result...');
default:
if (snapshot.hasError)
return new Text('Error: ${snapshot.error}');
else
return new Text('Result: ${snapshot.data}');
}
},
)
Example: https://flutter.io/cookbook/networking/background-parsing/
Doc: https://docs.flutter.io/flutter/widgets/FutureBuilder-class.html
I've come across a problem while trying out Flutter that I can't figure out. The case I'm thinking of has a FutureBuilder widget like below:
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Example Page"),
),
body: new FutureBuilder(
future: _exampleFuture,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting: return new Center(child: new CircularProgressIndicator(),);
default:
if(snapshot.hasError) {
return new Center(child: new Text('Error: ${snapshot.error}'),);
}
else {
return new Center(child: new Text("Result: ${snapshot.data}"),);
}
}
}
)
);
}
Now let's assume the future is an http call that ends up with a 401 error, indicating that the user is unauthorized. At this point, I'd like the app to erase any token that's stored and redirect to the login page or just rebuild the app. But I can't call a method that does that in the build function, and I don't think didUpdateWidget() is guaranteed to be called, as the future might return it's value before build is called? Maybe I'm approaching this completely wrong, but is there a way to do this in Flutter?
You can check for a statusCode inside your Async method, and use setState to erase the value of the token based on the statusCode value; otherwise, if the connection is authorized, return your desired data. Now, in your FutureBuilder , check if the you snapshot is null to show a SignIn() page instead.
For example, your method that handles the http requests might look something like:
_Request() async {
var httpClinet = createHttpClient();
var response = await httpClinet.get(
url, headers: {'Authorization': "Bearer $_currentUserToken"});
if (response.statusCode == 200) {
var myRequest = JSON.decode(response.body);
var myDesiredData;
///TODO: Some data conversions and data extraction
return myDesiredData;
}
else {
setState(() {
_currentUserToken = null;
});
return null;
}
}
Then you can have a FutureBuilder like this:
#override
Widget build(BuildContext context) {
return new FutureBuilder(
future: _request(),
builder: (BuildContext context, AsyncSnapshot response) {
response.hasData==false? new SignIn(): new Scaffold(
appBar: new AppBar(title: new Text("Future Builder"),),
body: new Center(
child: new Text("Build your widgets"),
),
);
},
);
}