I am struggling to reference specific things in my firebase real time database. I understand that when you want to read something specific from the database you need to specify the exact path where the item can be found.
If you see the screenshot attached, I want a way to read the name of each menuItem and store them in a list.
It seems like this can only be done if you reference each menuItem ID individually in the path (like in the code below).
Is there not a way to say ("menuItem/itemID/itemName") so that I can access each menuItem name dynamically using a for loop with a terminating value equal to the number of menu items in the database?
Any help would be greatly appreciated!
final DatabaseReference _dbRef = FirebaseDatabase.instance.ref();
late DataSnapshot _itemStream;[![enter image description here][1]][1]
Future<List<String>> _readItemNames() async {
_itemStream = await _dbRef.child("menuItem/J1/itemName").get();
itemName = _itemStream.value.toString();
itemNames.addAll([itemName]);
_itemStream = await _dbRef.child("menuItem/J2/itemName").get();
itemName = _itemStream.value.toString();
itemNames.addAll([itemName]);
return itemNames;
}
If you want to read and process all menu items, you can do:
var snapshot = await _dbRef.child("menuItem").get();
snapshot.children.forEach((childSnapshot) {
var props = childSnapshot.val() as Map;
print(props["itemName"]);
});
For more on this, see the Firebase documentation on reading a list of items with a value event (you're just using get() instead of onValue, but the logic is the same).
There is no way to get only the itemName property of each child node, so if all you need it the names you're loading more data than needed. If that is a concern, consider creating an additional list with just the value(s) you need. For example:
menuItemNames: {
"J1": "Milk Shake",
"J10": "Name of J10",
...
}
Related
I have a Riverpod Streamprovider that manages how a number of different Firebase documents are presented to the user. The user can then access each document, make some changes and return to the list of their documents. Once they have made, changes the row for that document should have a tick showing. The only wrinkle is that these documents in a different collection, each with their own identifier. So its not as easy as just streaming a whole collection, my function needs to get the identifier for each item and then get a list of documents to send to the user.
I have the code so it 'just works' but what I can't work out is why updating the record works when all the code is inside the provider vs when the provider calls it a external code. For example this StreamProvider works as I want and updated documents are recognised
final outputStreamProvider = StreamProvider.autoDispose((ref) async* {
final List<itemModelTest> itemList = [];
final user = ref.watch(loggedInUserProvider);
final uid = ref.watch(authStateProvider).value!.uid;
for (String ident in user.value!.idents) {
# get each item by its own identifier
final item = FirebaseFirestore.instance
.collection('items')
.where("ident", isEqualTo: ident)
.limit(1)
.snapshots();
final result = await item.first;
final test = result.docs[0];
final itemItem = itemModelTest.fromFirebaseQuery(test, uid);
itemList.add(itemItem);
# Listen for changes in the items
item.listen((event) async {
dev.log('event changed');
for (var change in event.docChanges) {
if (change.type == DocumentChangeType.modified) {
itemModelTest updatedModel =
itemModelTest.fromFirebaseQuery(test, uid);
itemList
.removeWhere((element) => element.title == updatedModel.title);
itemList.add(updatedModel);
}
}
});
}
yield itemList;
});
But as you can see it contains a lot of logic that doesn't belong there and should be with my firebase database class. So I tried to split it so now in my firebase crud class I have almost identical code:
Stream<List<itemModelTest>> itemsToReviewStream(LoggedInUser user, String uid) async*{
final List<itemModelTest> itemList = [];
for (String ident in user.idents) {
final item = FirebaseFirestore.instance
.collection('items')
.where("ident", isEqualTo: ident)
.limit(1)
.snapshots();
final result = await item.first;
final test = result.docs[0];
final itemItem = itemModelTest.fromFirebaseQuery(test, uid);
itemList.add(itemItem);
item.listen((event) async {
dev.log('event changed ${event.docChanges.first.doc}');
for(var change in event.docChanges){
if(change.type == DocumentChangeType.modified){
itemModelTest updatedModel = itemModelTest.fromFirebaseQuery(test, uid);
itemList.removeWhere((element) => element.title == updatedModel.title);
itemList.add(updatedModel);
}
}
});
}yield itemList;
}
and my StreamProvider now looks like this
// Get a list of the currently logged in users papers to review
final testitemStreamProvider = StreamProvider.autoDispose((ref) {
final user = ref.watch(loggedInUserProvider).value;
final uid = ref.watch(authStateProvider).value!.uid;
return DataBase().itemsToReviewStream(user!, uid);
});
The only problem is using this second approach the updates to firebase don't trigger any updates to the ui so when the user returns to their list of documents they cant see which have been processed already. I have been round the houses trying to work out what I am doing wrong but cant see it.
Edit: just a quick edit in case it matters but this is for FlutterWeb not iOS or Android
I am leaving this in case anyone else has the same problem. The real problem with this project was that the database structure was not fit for purpose and a further restriction was to not duplicate data on the database.
The simplest solution (and if you happen to be reading this because you fixing a similar problem) is to make a copy of the documents the user is supposed to have access to in their own collection, this can then be streamed as an entire collection. Checking which documents have and have not been looked at by users was always going to have to be done via an admin account anyway, so it's not as though this would have incurred a penalty.
All the same to manage my particular data repo i ended up
1 make a simple provider to stream a single document
final getSinglePageProvider = StreamProvider.family((ref, String pageId){
return DataBase().getSinglePage(pageId);});
Then once you have a list of all the documents the user has access to make a provider that provides a list of providers above
final simpleOutputsStreamsProvier = StreamProvider((ref) async* {
final user = ref.watch(loggedInUserProvider);
final items = user.value!.items;
yield items.map((e) => ref.watch(getSinglePageProvider(e))).toList();
});
You can then use this in a consumer as normal, but it has to be 'consumed' twice. In my case, I watched the ListProvider in the build method of a ConsumerWidget. That gives you a list of StreamProviders for individual pages. Finally I used ListView to get each StreamProvide (listofProviders[index]) and unwrapped that with provider.when(...
I have no idea how brittle this approach will turn out to be however!
I am trying to get the most recent ID of from my orders table to that I can then use this ID in a path to get the order number and increment it by one.
void _activateListeners() {
final recentOrder = _readRef.child('Orders').limitToLast(1).get();
final readOrder = _readRef
.child('Orders/{$recentOrder}/orderNum')
.onValue
.listen((event) {
final orderNum = event.snapshot.value;
setState(() {
lastOrderNum = orderNum.toString();
intOrderNum = int.parse(lastOrderNum) + 1;
newOrderNum = intOrderNum.toString();
});
});
}
I know that I need to use the "key" word somehow but I have been unable to solve this problem.
my database table looks as follows:
Orders
-N3ZdY6LOL_9Z-6KXHnK
*cartProduct
*dateTime:"2022-06-02 15:41:20.470139"
*orderNum:"6"
*totalAmount:45
-N3ZdgEQIzsjLA5NCu3U
*cartProduct
*dateTime:"2022-06-02 15:41:20.470139"
*orderNum:"7"
*totalAmount:45
Edit: can someone tell me if it is even possible to fetch the unique push ID after it has been made (not directly after the order is added)? I have looked at all the documentation and I have failed to implement anything. I have also found numerous similar questions that are either just different or unanswered.
If I know SQL is SQLite a better alternative?
You can get the key from the snapshot by using the snapshot's key property, like in this line below:
String? eventKey = event.snapshot.key;
Check out the documentation for Flutter's Realtime Database's DataSnapshot class for more information.
I have a question about hive db. is it possible to store List data type? That's what Hive's documentation says: Generic type parameters like Box are unsupported due to Dart limitations. But i need to store chat with room id and messages.
Lets imagine like that
that is, I need to store 2 different data types into the hive object, List (for message) and int (for id). is it possible? if you have any other ideas please share with me
Thanks
I have solved 🙂 this logic worked for me
final data = ChatStorage(
roomId: widget.room!.id,
chat: state.messages,
);
if (chatBox.isEmpty)
chatBox.put(widget.room!.id, data);
else {
List<int>? roomIds = [];
bool available = false;
for (var chat in chatBox.values) {
roomIds.add(chat.roomId!);
}
if (!roomIds.contains(widget.room!.id)) available = true;
if (available) chatBox.put(widget.room!.id, data);
}
}
I think this method will help if someone is facing such a problem 🙂
simple question here but wondering if anyone can offer the right solution.
I want to take in an object of the same type as my current class, and set all my fields to that object's fields, without individually setting each out. Is there a way to do that easily or no?
Eg.,
User({required this.uid}) {
Database().getUser(uid).listen((User user) async {
displayName = user?.displayName;
email = user?.email;
phoneNumber = user?.phoneNumber;
photoURL = user?.photoURL;
notifyListeners();
});
}
The above works but isn't clean.
I want to be able to do the above in 1 line, eg., set
this.user = user
Not sure if this is possible. Thanks!
I am trying to get documents with their own subcollections, from Stream, but I am stuck.
This is where I set up my StreamSubscription:
Future<void> _toggleOrdersHistorySubscription({FirebaseUser user}) async {
_ordersHistorySubscription?.cancel();
if (user != null) {
_ordersHistorySubscription = ordersRepository
.ordersHistoryStream(userId: user.uid)
.listen((ordersSnapshot) {
final List<OrderModel> tmpList = ordersSnapshot.documents.map((e) {
// e.reference.collection("cart").getDocuments().;
return OrderModel.orderFromSnapshot(e);
}).toList();
add(OrdersHistoryUpdated(ordersHistory: tmpList));
});
}
}
The issue is that I can't see a way to get subcollection along with the parent document because getDocuments returns a Future.
Anyone can clear this issue for me?
So, I update the code method a separate method for retrieving data when listener is triggered but it doesn't work fully and I do not understand what's happening and why part of the code is working and part is not.
List<OrderModel> _getOrdersHistory({
#required QuerySnapshot snapshot,
}) {
return snapshot.documents.map((document) {
List<OrderedProductModel> cart = [];
List<AddressModel> addresses = [];
document.reference.collection("cart").getDocuments().then((snapshot) {
snapshot?.documents?.forEach((doc) {
cart.add(OrderedProductModel.fromSnapshot(doc));
});
});
document.reference
.collection("addresses")
.getDocuments()
.then((snapshot) {
snapshot?.documents?.forEach((doc) {
addresses.add(AddressModel.addressFromJson(doc.data));
});
});
final order = OrderModel.orderFromSnapshot(
document,
restaurantCart: cart,
);
return order.copyWith(
orderAddress:
(addresses?.isNotEmpty ?? false) ? addresses.first : null,
sentFromAddress:
(addresses?.isNotEmpty ?? false) ? addresses.last : null,
);
})
.toList() ??
[];
}
As an alternate solution to my original issue is that I made a map entry in Firestore instead of a collection for 2 address documents (which are set above as orderAddress and sentFromAddress) and for the cart I decided to get the data when needed for every cart item.
So the method which I put in the update is not the final one, but I want to understand what is happening up there as I do not understand why:
Why the cart is shown as empty if I do a print(order); right before the return and in the bloc it has data;
Why the orderAddress and sentFromAddress are both empty no matter what I try;
To be short: You'll never be able to get a List synchronously if you get the data async from firebase.
Both questions have the same answer:
Your timeline:
For each document
Create an empty list
Initiate the firebase query - getDocuments()
Subscribe to the returned future with - .then((snapshot){cart.add(...)}).
This lambda will be invoked when the documents arrived.
Another subscribe
Save your empty cart and first/last of empty addresses to an OrderModel
Your List contains the references to your empty lists indirectly
Use your bloc, some time elapsed
Firebase done, your callbacks starts to fill up your lists.
Regarding your comment like stream.listen doesn't like async callbacks:
That's not true, you just have to know how async functions work. They're run synchronously until the first await, then return with an incomplete future. If you do real async things you have to deal with the consequences of the time delay like changed environment or parallel running listeners.
You can deal with parallel running with await for (T v in stream) or you can use subscription.pause() and resume.
If anything returns a future, just do this:
...getDocuments().then((value) => {
value is the item return here. Do something with it....
})
Also, you might want to split your method up a bit to share the responsibility.
If getDocuments is a Future function and you need to wait for it, I think you should add await before it. I also don't see the snapshot status checking in your code pasted here. Maybe you have already checked the snapshot status in other function? Make sure the snapshot is ready when using it.