I've recently started learning Flutter and the FlutterFire plugins. Yesterday I was working with the
firebase_database plugin which allows adding Firebase Realtime Database to Flutter. While trying out a
simple read on the database I noticed some strange behavior in one of the Streams provided by firebase_database which is onChildAdded.
So my problem is that when I use the onChildAdded stream with a StreamBuilder it only returns a single latest child. When I used to work with Firebase Database in Java the onChildAdded method was called for each child of a DatabaseReference instead of just the latest child. (Assuming that onChildAdded provides the same behavior in both Java and Dart)
I should also mention that when I use the onValue stream everything works fine and I get all the children of my DatabaseReference.
This is how my Firebase Database looks:
Code using onChildAdded
Widget _getBody(BuildContext context) {
final DatabaseReference databaseRef = FirebaseDatabase.instance.reference();
return StreamBuilder(
stream: databaseRef.child("notes").child('android').onChildAdded,
builder: (BuildContext context, AsyncSnapshot<Event> snapshot) {
if (snapshot.hasData) {
Map<dynamic, dynamic> notes = snapshot.data.snapshot.value;
notes.forEach(
(key, value) {
print(notes[key]);
}
);
}
return Container(); //Just a blank widget because builder has to return a widget
},
);
}
Output for onChildAdded:
Code using onValue
Widget _getBody(BuildContext context) {
final DatabaseReference databaseRef = FirebaseDatabase.instance.reference();
return StreamBuilder(
stream: databaseRef.child("notes").child('android').onValue,
builder: (BuildContext context, AsyncSnapshot<Event> snapshot) {
if (snapshot.hasData) {
List<dynamic> notes = snapshot.data.snapshot.value;
notes.forEach(
(item) {
print("$item \n");
}
);
}
return Container(); //Just a placeholder because builder has to return a widget
},
);
}
Output for onValue:
So I was hoping for a way where it's possible to get all the children using the onChildAdded stream. Any help is appreciated!
Related
Problem: Both of my streams from the code below do not update my UI automatically.
So the new data is only fetched and displayed when I do a hot reload or a hot restart. I am trying to fetch the most recent messages from each chat room and display them to the user.
Question: How can I change my code to make the streams work properly? Or is there maybe a better solution to what I am doing below?
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:blabber_tech/services/auth.dart';
import 'package:blabber_tech/services/chat_services.dart';
class MyChatsScreen2 extends StatelessWidget {
static const String id = "mychats2_screen";
// get current user id
String? userId = AuthService().getUserId();
// Stream of all rooms of current user
Stream getRoomsStream() async* {
// get rooms of current user
QuerySnapshot roomsSnapshot = await FirebaseFirestore.instance
.collection("rooms")
.where("userId1", isEqualTo: userId)
.get();
// get rooms of current user
QuerySnapshot roomsSnapshot2 = await FirebaseFirestore.instance
.collection("rooms")
.where("userId2", isEqualTo: userId)
.get();
// add rooms of current user to rooms list
List<QueryDocumentSnapshot> rooms = roomsSnapshot.docs;
// add rooms of current user to rooms list
List<QueryDocumentSnapshot> rooms2 = roomsSnapshot2.docs;
// add rooms of current user to rooms list
rooms.addAll(rooms2);
// sort rooms list by when last message was sent
// rooms.sort(
// (a, b) => b["lastMessageSentAt"].compareTo(a["lastMessageSentAt"]));
yield rooms;
}
// Stream to get last message of each room
Stream getLastMessageStream(String roomId) async* {
try {
// get last message of room
QuerySnapshot lastMessageSnapshot = await FirebaseFirestore.instance
.collection("rooms")
.doc(roomId)
.collection("messages")
.orderBy("createdAt", descending: true)
.limit(1)
.get();
// get last message of room
List lastMessage = lastMessageSnapshot.docs;
// return last message of room
yield lastMessage;
} catch (error) {
print(error);
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
// create listview of all chats of current user and show last message and other user name and photo
child: StreamBuilder(
stream: getRoomsStream(),
builder: (context, AsyncSnapshot<dynamic> snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) {
return StreamBuilder(
stream: getLastMessageStream(snapshot.data[index].id),
builder: (context, AsyncSnapshot<dynamic> snapshot2) {
if (snapshot2.hasData) {
return ListTile(
leading: CircleAvatar(
//backgroundImage: NetworkImage(
//snapshot.data[index]["userPhotoUrl"]),
),
//title: Text(snapshot.data[index]["userName"]),
subtitle: Text(snapshot2.data[0]["message"]),
);
} else {
return Container();
}
},
);
},
);
} else {
return Container();
}
},
),
),
);
}
}
Since you're using get() when the widget is created, the data is only loaded from the database once when the widget is created. If you want to get the new data whenever it is updated, use a snapshot() listener - which returns a stream which gets an initial event with the initial data, and a new event whenever the data is updated.
To wire the Stream up in your build method, you'll want to use a StreamBuilder as shown in the Firebase documentation on listening for realtime updates in Flutter.
I am trying to show data from the text file as per the data stored in shared preference i have another screen to save data in the text file i have a stream builder earlier it was future builder So i am trying to refresh the screen when coming back from second screen i tried to call a method when pop the method is getting called in the viewmodel calss of provider but the streambuilder is not getting updated
this is the code
to fetch data
Future<List<String>> fetchdata() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? category = prefs.getString('category');
if (category != null) {
lines = await locator<JsonAPI>().fetchquotes(category);
} else {
lines = await locator<JsonAPI>().fetchquotes('quotes');
}
// data = lines as Future<List<String>>;
notifyListeners();
return lines;
}
stream builder
var quotesdata = Provider.of<HomeViewModel>(context, listen: false);
StreamBuilder(
stream: quotesdata.fetchdata().asStream(),
builder: (context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
List<String> lines = quotesdata.lines;
// List<String>? lines = snapshot.data as List<String>?;
return ScreenShotWidget(
homeViewModel: quotesdata,
list: lines,
);
} else {
return Container();
}
}),
method that i call when pop
function(data) {
category = data.toString();
fetchdata();
notifyListeners();
setState() {}
}
any idea how to update the screen
Every time your widget rebuilds, you get a new stream. This is a mistake. You should obtain the stream only once (for example, in initState)
#override
void initState() {
_stream = quotesdata.fetchdata().asStream();
}
and use that stream variable with StreamBuilder
StreamBuilder(
stream: _stream,
Later, when you want to update the stream, you can do
setState(() {
_stream = quotesdata.fetchdata().asStream();
})
to change the stream and force a refresh.
Please go over your code and change all such usages
StreamBuilder(
stream: quotesdata.fetchdata().asStream(),
to this kind of usage.
StreamBuilder(
stream: _stream,
Otherwise you may get a high backend bill someday. Right now every screen refresh does a new query to the backend.
I'm new of Flutter and Dart in general, I'm trying to do a expansive computation during the loading of the page but the loader is stuck when I try to do something like this:
body: Center(
child:FutureBuilder(
future: _lorem()
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done){
print("loader");
return Center(
child: CircularProgressIndicator(
backgroundColor: Theme.of(context).primaryColor)
);
}
[...]
Future<void> _lorem() async {
//there is not a request to service, there is a more than one filter on map and some lists. I set the for loop for example of a local computation
return Future(() {
for (int i = 0; i < 50000; i++){
print(i);
}
}
);
}
I think the easier way to implement this is using a field in your widget of type Completer, eg Completer calc. You can start your expensive computation in your widget initialization (never in your build function), and when the computation is done you complete that Completer by calling calc.complete().
In your widget's FutureBuilder you should then listen to calc's future by including future: calc.future instead of your future: _lorem().
See FutureBuilder for an example of this UI paradigm.
Solved with a Future and compute.
In the detail:
Future<List<CustomObject>> _retrieveCustomObjects() async {
SourceData data = SourceData(CustomSourceData());
return compute(getFilteredClients, data);
}
List<CustomObject> computeCustomObject(SourceData data) {
List<CustomObject> list = [];
// expensive logic on data, not only network call
return list;
}
class LoremIpsumClass {
// use where you need `List<CustomObject> value = await _retrieveFilteredClient();`
}
I'm trying to use where in a Firebase firestore query in my flutter application but it is showing all the data in the collection without filtering it ., here is my code :
Widget buildingMessages() {
print('message room id $roomID'); //The correct id is being printed here
var theMessages = FirebaseFirestore.instance.collection('messages');
theMessages.where('room_id',isEqualTo: roomID).orderBy('created', descending: true);
return StreamBuilder<QuerySnapshot>(
stream: theMessages.snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (snapshot.hasError) {
return Text('Something went wrong');
}
return new ListView(
children: snapshot.data.docs.map((DocumentSnapshot document) {
//....
The problem is in stream: theMessages.snapshots(). You are referencing the theMessages. and you are not using your where clause. extends it with your where clause. like
stream: theMessages.snapshots().where(
'room_id',isEqualTo: roomID).orderBy('created', descending: true);
Edit: Or initialize it as
var theMessages = FirebaseFirestore.instance.collection('messages').
where('room_id',isEqualTo: roomID).orderBy('created', descending: true);
class Authservice {
handleAuth() {
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, snapshot) {
if (snapshot.hasData) {
var role;
Firestore.instance
.collection('users')
.document(snapshot.data.uid)
.get()
.then((DocumentSnapshot ds) {
role = ds.data['role'];
});
print('role');
print(role);
if (role == 'user') {
return OwnerHome();
} else {
return CustomerHome();
}
} else {
return Signin();
}
});
}
role prints a null value
i'm trying to show different views depending on the firestore data(role) for a logged in user
That happens because you are using .then instead of await.
When you use .then the code will execute without waiting for the firebase call to complete and that will print your role as null because it doesn't have data.
So you'll have to use await to get the user data.
You can either use a future builder or use some other approach to update the UI once you get the user data.
Firebase methods are asynchronous meaning they return a Future that will be resolved in future. So here in the code Firebase document get method simply return a future that has not been resolved yet, thus printing a null value for role, Since you can not use asyn-await inside of a builder method you can use a FutureBuilder Widget to tackle this issue, the code would look something like this-
class Authservice {
handleAuth() {
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, snapshot) {
if (snapshot.hasData) {
return FutureBuilder<DocumentSnapshot>(
future: Firestore.instance
.collection('users')
.document(snapshot.data.uid)
.get(),
builder: (BuildContext context,
AsyncSnapshot<DocumentSnapshot> snapshot) {
if (snapshot.hasData) {
DocumentSnapshot ds = snapshot.data;
role = ds.data['role'];
print('role');
print(role);
if (role == 'user') {
return OwnerHome();
} else {
return CustomerHome();
}
} else {
//Handle whenn user does not exists!
}
},
);
} else {
return Signin();
}
},
);
}
}
As you can see I have added FutureBuilder to the code, this will make Firebase document get method to wait until its future is resolved then only it will proceed with code.
Hope this helps!