State updated in Flutter UI not reflected in Firebase Firestore realtime - firebase

Here I have a stream of objects of which I'm returning from Firestore and being displayed in my UI. My StreamBuilder is properly returning the data from my database and is displaying my objects correctly in the UI, however I'm trying to accomplish a two way binding here, when my PlannerItemContainer is tapped and the value of the boolean isChecked changes, it updates in both the UI and in Firestore.
Now when I update my boolean's value, the change is reflected in real time in the UI as expected but when I attempt to make changes to the boolean from my UI, it's new value is not reflected in Firestore.
My diagnosis is that because PlannerItemContainer maintains its own state, my StreamBuilder isn't notified whenever a change is made.
My two questions are: 1.) Am I on the right track with my thinking and 2.) If I am is there a notifier function that I need to pass up from my PlannerItemContainer to the StreamBuilder that will notify it that there has been a change made to the PlannerItemContainer so that the realtime update is made to Firestore??
StreamBuilder(
stream: userProvider.watchHealthPlannerItems(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
plannerController.add(snapshot.data);
return Timetable<HealthPlannerItem>(
eventBuilder: (HealthPlannerItem item) {
return Row(
children: [
Expanded(
child: PlannerItemContainer(item),
),
],
);
},
);
},
);
Stream passed to StreamBuilder:
Stream<List<HealthPlannerItem>> watchHealthPlannerItems() {
return FirebaseFirestore.instance
.collection('organizations')
.doc(this.organizationId)
.collection('apps')
.doc(this.appId)
.collection('clients')
.doc(this.user.id)
.collection('healthplanner')
.snapshots()
.map((snapshot) {
return snapshot.docs.map((doc) {
Map<String, dynamic> healthPlannerData = doc.data();
HealthPlannerItem healthPlannerItem =
HealthPlannerItem.fromJson(healthPlannerData);
return healthPlannerItem;
}).toList();
});
}
PlannerItemContainer widget:
class PlannerItemContainer extends StatefulWidget {
final HealthPlannerItem item;
PlannerItemContainer(this.item);
#override
_PlannerItemContainerState createState() => _PlannerItemContainerState();
}
class _PlannerItemContainerState extends State<PlannerItemContainer> {
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() => widget.item.isChecked = !widget.item.isChecked);
},
child: widget.item.isChecked
? _buildCheckedItemContainer(widget.item)
: _buildUncheckedItemContainer(widget.item),
);
}
}

I'm not following everything completely but a quick scan shows me you're trying to change an immutable (final) variable "item" in your setState. you can move item into your state object and it can be mutable in there. Hopefully that gets you unstuck.

Related

Flutter Firestore: FirestoreBuilder with initial data

I'm making my first Flutter app and I encounter a problem and doesn't found any solution for it.
I have a view where I render a Firestore document, and there is two ways of getting there:
From a list where I already loaded my documents
From Dynamic Links with uid attached as arguments (args)
So in order to listen document changes and loading the data when arriving from the link I used FirestoreBuilder like this:
return FirestoreBuilder<EventDocumentSnapshot>(
ref: eventsRef.doc(args.uid),
builder: (context, AsyncSnapshot<EventDocumentSnapshot> snapshot, Widget? child) {
if (!snapshot.hasData) {
return Container();
}
Event? event = snapshot.requireData.data;
return Scafold(); //Rest of my rendering code
}
);
How I could avoid first call to Firebase when I already have the data but still listen to changes? The main problem is that my hero animation doesn't work because of this.
I tried with a StreamBuilder and initialDataparam but since it's expecting stream I didn't know how to cast my data.
Okay, so I found the solution myself after many tries, so I added my Model object that can be null as initialData, but the thing that makes me struggle with is how you get the data in the builder. You have to call different methods depending on where the data is coming from.
return StreamBuilder(
initialData: args.event
ref: eventsRef.doc(args.uid),
builder: (context, AsyncSnapshot<dynamic> snapshot) {
// Here is the trick, when data is coming from initialData you only
// need to call requireData to get your Model
Event event = snapshot.requireData is EventDocumentSnapshot ? snapshot.requireData.data : snapshot.requireData;
return Scafold(); //Rest of my rendering code
}
);
Reading through cloud_firestore's documentation you can see that a Stream from a Query can be obtained via snapshots()
StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('books').snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return new Text('Loading...');
return new ListView(
children: snapshot.data.documents.map((DocumentSnapshot document) {
return new ListTile(
title: new Text(document['title']),
subtitle: new Text(document['author']),
);
}).toList(),
);
},
);
This won't help you, but with GetX it's simple to implement like this: You don't need StreamBuilder anymore.
//GetXcontroller
class pageController extends GetXcontroller {
...
RxList<EventModel> events = RxList<EventModel>([]);
Stream<List<EventModel>> eventStream(User? firebaseUser) =>
FirebaseFirestore.instance
.collection('events')
.snapshots()
.map((query) =>
query.docs.map((item) => UserModel.fromMap(item)).toList());
#override
void onReady() async {
super.onReady();
events.bindStream(
eventStream(controller.firebaseUser)); // subscribe any change of events collection
}
#override
onClose() {
super.onClose();
events.close(); //close rxObject to stop stream
}
...
}
You can use document snapshots on StreamBuilder.stream. You might want to abstract the call to firebase and map it to an entity you defined.
MyEntity fromSnapshot(DocumentSnapshot<Map<String, dynamic>> snap) {
final data = snap.data()!;
return MyEntity (
id: snap.id,
name: data['name'],
);
}
Stream<MyEntity> streamEntity(String uid) {
return firebaseCollection
.doc(uid)
.snapshots()
.map((snapshot) => fromSnapshot(snapshot));
}
return StreamBuilder<MyEntity>(
// you can use firebaseCollection.doc(uid).snapshots() directly
stream: streamEntity(args.uid),
builder: (context, snapshot) {
if (snapshot.hasData) {
// do something with snapshot.data
return Scaffold(...);
} else {
// e.g. return progress indicator if there is no data
return Center(child: CircularProgressIndicator());
}
},
);
For more complex data models you might want to look at simple state management or patterns such as BLoC.

flutter firebase get string from database using future

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)

Flutter How to pass in documents.length from Firestore in another method?

I have a method which takes an int value, which is supposed to be the value of the length of an array field in a document in a collection in Cloud Firestore.
I'm very new to both Flutter and especially Firestore, and I figure it has something to do with how futures, queries and streams works, but I can't seem to understand it correctly.
This is what I'm trying to achieve:
class UserBookmarksSection extends StatefulWidget {
#override
_UserBookmarksSectionState createState() => _UserBookmarksSectionState();
}
class _UserBookmarksSectionState extends State<UserBookmarksSection> {
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
HeadlineItem(title: "Your bookmarks"),
StreamBuilder(
stream: Firestore.instance.collection("brites").snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const Text("Loading...");
return Column(
children: populateBriteList(snapshot, x, true), //Here I want to pass in the length of the array in firestore into x
);
}
),
],
);
}
}
Basically I've tried to create a method which returns a Future<int> and tried to pass that, but that didn't work because a Future<int> isn't the same type as int. That I understand, but the solution is not clear to me.
I'm not sure the relationship between brites and bookmarks- that is, which you expect to change more often. I'm going to assume that the bookmarks isn't changing once the user opens the bookmarks section.
If that is the case you can retrieve the bookmarks length value in initState. You will override the state class's initState() method, and then call an async method which retrieves the value from the database. (init state can't be async itself). Once the value is retrieved, you can then call setState() to update the widget with the value of bookmarks set as an int.
Might look something like this:
class UserBookmarksSection extends StatefulWidget {
#override
_UserBookmarksSectionState createState() => _UserBookmarksSectionState();
}
class _UserBookmarksSectionState extends State<UserBookmarksSection> {
int bookmarksLength;
#override
void initState(){
super.initState();
getBookmarksLength();
}
Future<void> getBookmarksLength() async {
bookmarksLength = await getArrayLength(id);
setState((){});
}
#override
Widget build(BuildContext context) {
if(bookmarksLength == null) return CircularProgressIndicator();
else return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
HeadlineItem(title: "Your bookmarks"),
StreamBuilder(
stream: Firestore.instance.collection("brites").snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const Text("Loading...");
return Column(
children: populateBriteList(snapshot, bookmarksLength, true),
);
}
),
],
);
}
}
Firestore firestore = Firestore.instance;
Future<int> getArrayLength(String documentId) async {
DocumentSnapshot snapshot = await firestore.collection('collection').document(documentId).get();
return snapshot.data['array'].length;
}
A more robust solution might be to have a separate class that handles both listening to the stream and retrieving the length of the array, and then combining all of the information you're going to display into some abstract data type which you put into a separate stream you specify that is specifically for the UI to listen to.

Want to show a list of cards with text which is coming from textfield on a screen when the send button is pressed

I want to show a card with text containing in it. The text value is coming from the TextField input and it should instantly display the values on a new Card whenever the button is pressed.
I have created two separate files :
notestream.dart to display the cards, and notetextfield.dart to send the value to the database
notetextfield.dart
TextField
TextField(
controller: _textEditingController,
textInputAction: TextInputAction.newline,
onChanged: (value) {
messageText = value;
noteText = value;
},
......
......
),
onPressed
IconButton(
onPressed: () {
_textEditingController.clear();
/Implement send functionality.
final newNote = Note(noteText: noteText);
if (newNote.noteText.isNotEmpty) {
/*Create new Note object and make sure
the Note textis not empty,
because what's the point of saving empty
Note
*/
noteBloc.addNote(newNote);
noteBloc.getNotes();
}
},
notestream.dart
The card will be generate with the help of a separate file containing code for the cards.
final NoteBloc noteBloc = NoteBloc();
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: noteBloc.notes,
builder: (
BuildContext context,
AsyncSnapshot<List<Note>>snapshot
) {
if (snapshot.hasData) {
/*Also handles whenever there's stream
but returned returned 0 records of Note from DB.
If that the case show user that you have empty Notes
*/
return snapshot.data.length != 0
? ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, itemPosition) {
Note note = snapshot.data[itemPosition];
return NoteCard(
noteText: note.noteText,
noteImagePath: note.noteImagePath,
);
})
bloc.dart
class NoteBloc {
//Get instance of the Repository
final _noteRepository = NoteRepository();
final _noteController = StreamController<List<Note>>.broadcast();
get notes => _noteController.stream;
NoteBloc() {
getNotes();
}
getNotes({String query}) async {
//sink is a way of adding data reactively to the stream
//by registering a new event
_noteController.sink.add(await _noteRepository.getAllNotes(query:
query));
}
addNote(Note note) async {
await _noteRepository.insertNote(note);
getNotes();
}
updateTodo(Note note) async {
await _noteRepository.updateNote(note);
getNotes();
}
dispose() {
_noteController.close();
}
}
Whenever I am pressing the onPressed button from the notetextfield.dart file the list of cards are not getting displayed on the screen.
Looks like you're using a different instance of NoteBloc on each file, you should have one single source of truth.
Try using the provider library to provide instances on parent and consume it on its children.
you would provide the bloc on the parent Widget like this
Provider<NoteBloc>(
builder: (context) => NoteBloc(noteRepository: NoteRepository()),
dispose: (context, value) => value.dispose()
child: ParentWidget(),
)
and you can consume it on any child like this
final bloc = Provider.of<NoteBloc>(BuildContext context)
or this if you prefer to wrap the Widget
Consumer<NoteBloc>(
builder: (context, bloc, _) => ChildWidget()

StreamController with Firestore

I want to user a StreamController to control a StreamBuilder that gets data from a collection in Firestore. This will enable me to use a RefereshIndicator so that when I pull down on the list, it refreshes/ fetches more data if there is any.
I used most of the information in this article. My current code is below
class _Lists extends State<List> {
StreamController _controller;
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
#override
void initState() {
_controller = new StreamController();
loadPosts();
super.initState();
}
Future fetchPost() async {
return await .
Firestore.instance.collection(_locationState).snapshots();
}
Future<Null> _handleRefresh() async {
count++;
print(count);
fetchPost().then((res) async {
_controller.add(res);
showSnack();
return null;
});
}
showSnack() {
return scaffoldKey.currentState.showSnackBar(
SnackBar(
content: Text('New content loaded'),
),
);
}
loadPosts() async {
fetchPost().then((res) async {
print(res.document);
_controller.add(res);
return res;
});
}
#override
Widget build(BuildContext context) {
final topBar = AppBar(Title("List"));
bottom: TabBar(
indicatorColor: Colors.blueAccent,
indicatorWeight: 3.0,
//indicatorSize: 2.0,
indicatorPadding:
const EdgeInsets.only(bottom: 10.0, left: 47.0, right:
47.0),
tabs: [
Tab(
child: Image(
image: AssetImage("MyImage1"),
width: 65.0,
height: 65.0,
),
),
Tab(
child: Image(
image: AssetImage("Image2"),
width: 90.0,
height: 90.0,
),
),
],
),
return DefaultTabController(
length: 2,
child: Scaffold(
key: scaffoldKey,
appBar: topBar,
body: StreamBuilder(
stream: _controller.stream,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasError) {
return Text(snapshot.error);
}
if (snapshot.connectionState == ConnectionState.active) {
List aList = new List();
aList.clear();
for (DocumentSnapshot _doc in snapshot.data.documents) {
Model _add = new Model.from(_doc);
aList.add(_add);
}
return TabBarView(
children: <Widget>[
RefreshIndicator(
onRefresh: _handleRefresh,
child: ListView.builder(
itemCount: aList.length,
itemBuilder: (context, index) {
return Card(aList[index]);
},
),
),
Icon(Icons.directions_transit),
],
);
} else {
return Container(
child: Center(child: CircularProgressIndicator()));
}
})));
}
}
}
The Problem I have with this is I keep getting the error
flutter: ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY
╞═══════════════════════════════════════════════════════════
flutter: The following NoSuchMethodError was thrown building
StreamBuilder<dynamic>(dirty, state:
flutter: _StreamBuilderBaseState<dynamic,
AsyncSnapshot<dynamic>>#53c04):
flutter: Class '_BroadcastStream<QuerySnapshot>' has no instance getter 'documents'.
flutter: Receiver: Instance of '_BroadcastStream<QuerySnapshot>'
flutter: Tried calling: documents
Any ideas on how to go about using the StreamController with data from Firestore ?
Keeping a close eye on return types in your IDE will likely help avoid a lot of confusing issues like this. Unfortunately, that blog does not indicate any types for the API call, StreamController, or 'res' in the then statement. Having those types declared will help show what you are working with (at least it does for me in Android Studio). For example in my StreamBuilder with a stream from Firestore, I use AsyncSnapshot<QuerySnapshot> snapshot instead of just AsyncSnapshot. This allows the tools in Android Studio to tell me that snapshot.data.documents is the map from the QuerySnapshot class. If I don't add the extra type, I can't see that.
Here is an example of listening to the stream from the Firestore Dart package.
//Performing a query:
Firestore.instance
.collection('talks')
.where("topic", isEqualTo: "flutter")
.snapshots()
.listen((data: QuerySnapshot) =>
// do stuff here
);
Since you're using the async/await style (also perfectly fine), you'll have the same result that will be inside of .listen((data) =>. We can follow the documentation/classes to see what types are returned.
Firestore.instance.collection(<whatever>).snapshots()
will return
Stream<QuerySnapshot>,
so we know that
await Firestore.instance.collection(<whatever>).snapshots()
will return
QuerySnapshot.
Digging further into the class, we see it has a property called documents.
/// Gets a list of all the documents included in this snapshot
final List<DocumentSnapshot> documents;
This finally gives us those DocumentSnapshots, which you'll have to pull the data property from.
So in your case, I believe the res type being QuerySnapshot will help show you what data to put in your stream, which can be done multiple ways at this point. List<DocumentSnapshot> seems to look like what you're going for, but you could go farther to List<YourClass> built from the DocumentSnapshot data property. With that, you can say what data type your StreamController will return, making the builder's AsyncSnapshot<your stream type> much more clear to work with.
I'm not sure what development tools you are using, but in case you aren't familiar most will allow you to do something like: press/hold (command or ctrl), hover over the type/class/function/var you want to see, left click, and you should be taken to the source files/declarations (I find this super handy).

Resources