In my flutter project, I need to access the realtime database to get the name of the user when they create a post. This is the code I'm working with:
class PostScreen extends StatelessWidget {
static const String idScreen = 'post';
final String name1 = FirebaseDatabase.instance
.reference()
.child('users')
.child(FirebaseAuth.instance.currentUser.uid)
.child('name1')
.toString();
final String name2 = FirebaseDatabase.instance
.reference()
.child('users')
.child(FirebaseAuth.instance.currentUser.uid)
.child('name2')
.toString();
#override
Widget build(BuildContext context) {
return Scaffold(
body: FlatButton(
child: Text('Create Post'),
onPressed: () {
MainScreen.posts.add(Post(
name1: name1,
name2: name2,
));
Navigator.pushNamed(context, MainScreen.idScreen);
},
),
);
}
}
class Post extends StatelessWidget {
String name1 = '';
String name2 = '';
Post({#required name1, #required name2});
#override
Widget build(BuildContext context) {
return Card(
child: Row(
children: [
Text(name1),
Text(" and "),
Text(name2),
],
),
);
}
}
What happens though is that the name is left blank and just creates a card that says " and ". What could I be doing wrong?
Your code doesn't read anything from the database yet. For that to happen you need to call the once() stream, or listen to the onValue or onChild... streams.
I also recommend simplifying your problem before continuing. So instead of reading the data for the current user (which requires that you have a current user), simply write some hard-coded data at a known location in the database and read that first. That should look something like this:
final DatabaseReference ref = FirebaseDatabase.instance
.reference()
.child('test');
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: ref.onValue,
builder: (BuildContext context, snapshot) {
if(snapshot.hasData) => return Text(snapshot.value);
else if(snapshot.hasError) => return Text("Error");
else return Text("No data (yet)");
}
);
}
There may be syntax errors in this code, so treat it as an example of an approach instead of a copy/paste solution please. If you find any of such errors, please try to solve them on your own - and edit the answer with any fixes.
Also see:
How To Use Firebase RTDB with Flutter Stream
more of these search results
You should be able to do something like this:
DatabaseReference myRef = FirebaseDatabase.instance
.reference()
.child('users')
.child(FirebaseAuth.instance.currentUser.uid)
.child('name1');
StreamBuilder(
stream: myRef.onValue,
builder: (context, AsyncSnapshot<Event> snap) {
if (snap.hasData && !snap.hasError && snap.data.snapshot.value != null) {
// Handle snapshot data
}
}
If you don't need to continue getting changes from the location you can probably use a future builder and .once() method. Don't have experience with that myself though.
Related
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.
I want to get a string from my DB in Firebase, I'm very confused and I don't know how to do that!
I made a big search in the few past days about this idea but unf I don't get any useful result
what do I want? I want to make a Method that returns the 'Question' string.
DB:Collection / History/question
thank you for your time
the incorrect code :
Future loadData() async {
await Firebase.initializeApp();
if (snapshot.hasError) {
return Scaffold(
body: Center(
child: Text("Error: ${snapshot.error}"),
),
);
}
// Collection Data ready to display
if (snapshot.connectionState == ConnectionState.done) {
// Display the data inside a list view
return snapshot.data.docs.map(
(document) {
return method(
document.data()['question'].toString().toString(),
); //Center(
},
);
}
}
Here is the official documentation from Flutter Fire - https://firebase.flutter.dev/docs/firestore/usage/
Read data from Cloud firestore
Cloud Firestore gives you the ability to read the value of a collection or a document. This can be a one-time read or provided by real-time updates when the data within a query changes.
One-time Read
To read a collection or document once, call the Query.get or DocumentReference.get methods. In the below example a FutureBuilder is used to help manage the state of the request:
class GetUserName extends StatelessWidget {
final String documentId;
GetUserName(this.documentId);
#override
Widget build(BuildContext context) {
CollectionReference users = FirebaseFirestore.instance.collection('users');
return FutureBuilder<DocumentSnapshot>(
future: users.doc(documentId).get(),
builder:
(BuildContext context, AsyncSnapshot<DocumentSnapshot> snapshot) {
if (snapshot.hasError) {
return Text("Something went wrong");
}
if (snapshot.hasData && !snapshot.data.exists) {
return Text("Document does not exist");
}
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");
},
);
}
}
To learn more about reading data whilst offline, view the Access Data Offline documentation.
Realtime changes
FlutterFire provides support for dealing with real-time changes to collections and documents. A new event is provided on the initial request, and any subsequent changes to collection/document whenever a change occurs (modification, deleted, or added).
Both the CollectionReference & DocumentReference provide a snapshots() method which returns a Stream:
Stream collectionStream = FirebaseFirestore.instance.collection('users').snapshots();
Stream documentStream = FirebaseFirestore.instance.collection('users').doc('ABC123').snapshots();
Once returned, you can subscribe to updates via the listen() method. The below example uses a StreamBuilder which helps automatically manage the streams state and disposal of the stream when it's no longer used within your app:
class UserInformation extends StatefulWidget {
#override
_UserInformationState createState() => _UserInformationState();
}
class _UserInformationState extends State<UserInformation> {
final Stream<QuerySnapshot> _usersStream = FirebaseFirestore.instance.collection('users').snapshots();
#override
Widget build(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: _usersStream,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('Something went wrong');
}
if (snapshot.connectionState == ConnectionState.waiting) {
return Text("Loading");
}
return new ListView(
children: snapshot.data.docs.map((DocumentSnapshot document) {
return new ListTile(
title: new Text(document.data()['full_name']),
subtitle: new Text(document.data()['company']),
);
}).toList(),
);
},
);
}
}
By default, listeners do not update if there is a change that only affects the metadata. If you want to receive events when the document or query metadata changes, you can pass includeMetadataChanges to the snapshots method:
FirebaseFirestore.instance
.collection('users')
.snapshots(includeMetadataChanges: true)
So I need to get access to a piece of data called groupId which will be used as a doc path for a collection called Members that I will be used to retrieve data from and put in a list. The problem is that it requires an async which I don't know where to put as I get errors. Here's the code:
#override
Widget build(BuildContext context) {
final CollectionReference users = firestore.collection('UserNames');
final String uid = auth.currentUser.uid;
final result = await users.doc(uid).get(); //This await requires an async but I don't how to do that
final groupId = result.data()['groupId'];
// <1> Use FutureBuilder
return FutureBuilder<QuerySnapshot>(
// <2> Pass `Future<QuerySnapshot>` to future
future: FirebaseFirestore.instance.collection('Groups').doc(groupId).collection('Members').get(), //Once the async problem is solved i will be able to save the groupId as. variable to be used in my doc path to access this collection. How do I do this?
builder: (context, snapshot) {
if (snapshot.hasData) {
// <3> Retrieve `List<DocumentSnapshot>` from snapshot
final List<DocumentSnapshot> documents = snapshot.data.docs;
return ListView(
children: documents
.map((doc) => Card(
child: ListTile(
title: Text(doc['displayName']),
subtitle: Text(doc['plastics'].toString()),
),
))
.toList());
} else if (snapshot.hasError) {
return Text('Its Error!');
}
});
}
Wrap your FutureBuilder in another FutureBuilder.
users.doc(uid).get() returns a Future. If you are using it in a widget, you use FutureBuilder.
await and futureBuilder do the similar things
#override
Widget build(BuildContext context) {
final CollectionReference users = firestore.collection('UserNames');
final String uid = auth.currentUser.uid;
return FutureBuilder(
future: users.doc(uid).get(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final result = snapshot.data;
final groupId = result.data()['groupId'];
return FutureBuilder<QuerySnapshot>(
// <2> Pass `Future<QuerySnapshot>` to future
future: FirebaseFirestore.instance
.collection('Groups')
.doc(groupId)
.collection('Members')
.get(), //Once the async problem is solved i will be able to save the groupId as. variable to be used in my doc path to access this collection. How do I do this?
builder: (context, snapshot) {
if (snapshot.hasData) {
// <3> Retrieve `List<DocumentSnapshot>` from snapshot
final List<DocumentSnapshot> documents = snapshot.data.docs;
return ListView(
children: documents
.map((doc) => Card(
child: ListTile(
title: Text(doc['displayName']),
subtitle: Text(doc['plastics'].toString()),
),
))
.toList());
} else if (snapshot.hasError) {
return Text('Its Error!');
}
});
}
});
}
I'm trying to query a few documents from a collection, this query should listen to changes made in the queried documents, so I'd need a stream. I'm doing following (in Dart/Flutter)
Stream<List<MatchRequest>> _getNewMatches() {
return Collection<MatchRequest>(path: 'requests')
.ref
.where('status', isNull: true)
.where('users', arrayContains: ['$currentUid'])
.orderBy('last_activity')
.snapshots()
.map((list) => list.documents.map(
(doc) => Global.models[MatchRequest](doc.data) as MatchRequest));
}
(The object Collection sets the path to the ref in it's constructor, eg: ref = db.collection($path) and the map makes a model of the results)
Then I'm using a StreamBuilder with stream invoking the method above and builder checking if snapshot.hasData. But it keeps loading, snapshot.hasData keeps being false. What am I doing wrong here?
EDIT:
My firestore security rules contain:
match /requests/{requestId} {
allow read: if isLoggedIn();
allow write: if isLoggedIn();
}
When removing every where and orderBy, it doesn't find anything as well. And there are documents present in the requests-collection
When trying to query only 1 document as a stream from the requests-collection, he does find the result
Is it because I should add indexes to my firestore indexes? But this won't solve my first problem which is that even without where and orderBy, it doesn't get any data
I've written a simple example of it seems to be like what you are trying to do but are missing the listen() method:
Firestore.instance.collection('collection')
.where('field', isEqualTo: 'value')
.orderBy('field')
.snapshots()
.listen((QuerySnapshot querySnapshot){
querySnapshot.documents.forEach((document) => print(document));
}
);
This is just an example of how you can take the data from a Firestore Stream and use it on a StreamBuilder:
class _MyHomePageState extends State<MyHomePage> {
Stream dataList;
#override
void initState() {
dataList = Firestore.instance.collection('collection')
.where('field', isEqualTo: 'value')
.orderBy('field')
.snapshots();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: StreamBuilder(
stream: dataList,
builder: (context, asyncSnapshot) {
if(asyncSnapshot.hasError)
return Text('Error: ${asyncSnapshot.error}');
switch (asyncSnapshot.connectionState) {
case ConnectionState.none: return Text('No data');
case ConnectionState.waiting: return Text('Awaiting...');
case ConnectionState.active:
return ListView(
children: asyncSnapshot.data.map((document) => Text(document['value'])),
);
break;
case ConnectionState.done: return ListView(
children: asyncSnapshot.data.map((document) => Text(document['value'])),
);
break;
}
return null;
}),
),
);
}
}
I'm trying to create a UI where the user submits a "comment" on each "article". As you can see below, in my code, I get username of the user from Firestore and then checks if this user already has a comment for the respective article.
If the comment exists, it returns a stream of all the comments for that given article. If it does not exist, I return the CommentCollector that is a widget to collect the comment and post it to Firestore. This overall works, expect when I submit a comment via CommentCollector, the UserPostGetter widget does not does not rebuild.
How can I trigger this rebuild? I thought using StreamBuilder to listen to see if there is a comment would be enough, but clearly not so. What am I missing?
Really appreciate the help.
class UserPostGetter extends StatelessWidget {
final String articleId;
final String articleHeader;
UserPostGetter({this.articleId, this.articleHeader});
#override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
body: new Container(
child: new FutureBuilder<FirebaseUser>(
future: FirebaseAuth.instance.currentUser(),
builder: (context, AsyncSnapshot<FirebaseUser> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
String userNumber = snapshot.data.uid;
return new FutureBuilder(
future: getUser(userNumber),
builder: (context, AsyncSnapshot<User> snapshot) {
if (snapshot?.data == null)
return new Center(
child: new Text("Loading..."),
);
String username = snapshot.data.username.toString();
return StreamBuilder(
stream: doesNameAlreadyExist(articleId, username),
builder: (context, AsyncSnapshot<bool> result) {
if (!result.hasData)
return Container(); // future still needs to be finished (loading)
if (result
.data) // result.data is the returned bool from doesNameAlreadyExists
return PostGetter(
articleId: articleId,
);
else
return CommentCollector(
articleID: articleId,
userName: username,
articleTitle: articleHeader,
);
},
);
},
);
} else {
return new Text('Loading...');
}
},
),
),
);
}
}
Stream<bool> doesNameAlreadyExist(String article, String name) async* {
final QuerySnapshot result = await Firestore.instance
.collection('post')
.where('article', isEqualTo: article)
.where('author', isEqualTo: name)
.getDocuments();
final List<DocumentSnapshot> documents = result.documents;
yield documents.length == 1;
}
Future<User> getUser(idNumber) {
return Firestore.instance
.collection('user')
.document('$idNumber')
.get()
.then((snapshot) {
try {
return User.fromSnapshot(snapshot);
} catch (e) {
print(e);
return null;
}
});
}
class User {
String email;
String user_id;
String username;
User.fromSnapshot(DocumentSnapshot snapshot)
: email = snapshot['email'],
user_id = snapshot['user_id'],
username = snapshot['username'];
}