Using stream inside stream in firestore using bloc - flutter

I have multiple lists from firestore that I get using a stream method and bloc with a subscription, this is working fine. The problem is when I want to add products inside those list. I want that each list has their own products, and I want to get those products with a stream. I tried using a streamBuilder inside those lists to get the products and it worked fine, but I don't think this is a good approach of the bloc pattern (maybe I am wrong and this is a good approach, correct me if I am mistaken please). So I want to use a bloc with a subscription to get the products from each list, the problem is that all the list display the same products that the last list. List example
In this example I can only work with list 3, if I add or delete a product from that list, then that products is showed in the rest of the list, BUT is not created in the collection in firestore, in the collection it creates and deletes properly. My question is why it only displays the last list, is a problem with the bloc stream? Or maybe I am not retrieving the data properly from firestore?
This is the bloc:
on<ProductSubscriptionRequestedEvent>((event, emit) {
String shoppingCartListId = event.shoppingCartListId;
String shoppingListId = event.shoppingListId;
_productsInShoppingListSubscription?.cancel();
_productsInShoppingListSubscription = provider.getProductsFromShoppingList(
shoppingCartListId: shoppingCartListId,
shoppingListId: shoppingListId
)
.listen((products) => add(UpdateProductStreamEvent(products)) );
});
on<UpdateProductStreamEvent>((event, emit){
emit(ProductsStreamState(productList: event.products));
});
This is the way I get the data from firestore:
#override
Stream<Iterable<CloudProducts>> getProductsFromShoppingList({required String
shoppingCartListId, required String shoppingListId}) {
final allProductsShoppingList =
FirebaseFirestore.instance.collection(shoppingListCollectionName)
.doc(shoppingCartListId).collection(shoppingListCartCollectionName)
.doc(shoppingListId).collection(productCollectionName).snapshots().map((event) =>
event.docs
.map((doc) => CloudProducts.fromSnapshot(doc)));
return allProductsShoppingList;
}
And this is how I display the products inside the lists:
return BlocBuilder<ProductBloc, ProductState>(
builder: (context, state) {
if (state is ProductsStreamState) {
final products = state.productList;
return Column(
children: [ListView.builder(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
itemCount: products.length,
itemBuilder:
(BuildContext context, int index) {
final productName =
products.elementAt(index).name;
final productId = products.elementAt(index).productId;
if (productName.isNotEmpty) {
return Slidable(...);
} else {
return SizedBox.shrink();
}
},
),
Thank you in advance.

Related

Create infinite scrolling effect using Firebase Realtime Database data in flutter

I am integrating a chat feature in my mobile application, and decided to use Firebase Realtime Database for the backend instad of Firestore as a cost reduction mechanism. I am running into a problem, however. There seems to be very sparse documentation on how to create infinite scrolling using data from Realtime Database instead of Firestore.
Below is the organization of my chat messages. This is the query I want to use:
FirebaseDatabase.instance
.ref("messages/${widget.placeID}")
.orderByChild("timeStamp")
And this is the widget I want to return for each result:
MessageWidget(
message: message.text,
id: message.uid,
name: message.name,
lastSender: message.lastSender,
date: message.timeStamp,
profilePicture: message.profilePicture,
);
Here is the database structure
The query works, and I have already programmed the MessageWidget from the JSON response of the query. All I need is for the query to be called whenever it reaches the top of its scroll, and load more MessageWdigets. Also note, this is a chat app where users are scrolling up, to load older messages, to be added above the previous.
Thank you!
EDIT: here is the code I currently have:
Flexible(
child: StreamBuilder(
stream: FirebaseDatabase.instance
.ref("messages/${widget.placeID}")
.orderByChild("timeStamp")
.limitToLast(20)
.onValue,
builder:
(context, AsyncSnapshot<DatabaseEvent> snapshot) {
if (!snapshot.hasData) {
return const CircularProgressIndicator();
} else {
Map<dynamic, dynamic> map =
snapshot.data!.snapshot.value as dynamic;
List<dynamic> list = [];
list.clear();
list = map.values.toList();
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 20),
child: ListView.builder(
controller: _scrollController,
// shrinkWrap: true,
itemCount: list.length,
itemBuilder: (context, index) {
final json = list[index]
as Map<dynamic, dynamic>;
final message = Message.fromJson(json);
return MessageWidget(
message: message.text,
id: message.uid,
name: message.name,
lastSender: message.lastSender,
date: message.timeStamp,
profilePicture:
message.profilePicture,
);
}),
),
);
}
},
),
),
My initState
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.atEdge) {
bool isTop = _scrollController.position.pixels == 0;
if (isTop) {
//add more messages
} else {
print('At the bottom');
}
}
});
}
Your code already loads all messages.
If you want to load a maximum number of messages, you'll want to put a limit on the number of messages you load. If you want to load only the newest messages, you'd use limitToLast to do so - as the newest messages are last when you order them by their timeStamp value.
So to load for example only the 10 latest messages, you'd use:
FirebaseDatabase.instance
.ref("messages/${widget.placeID}")
.orderByChild("timeStamp")
.limitToLast(10);
This gives you the limited number of messages to initially show in the app.
Now you need to load the 10 previous messages when the scrolling reaches the top of the screen. To do this, you need to know the timeStamp value and the key of the message that is at the top of the screen - so of the oldest message you're showing so far.
With those two values, you can then load the previous 10 with:
FirebaseDatabase.instance
.ref("messages/${widget.placeID}")
.orderByChild("timeStamp")
.endBefore(timeStampValueOfOldestSeenItem, keyOfOldestSeenItem)
.limitToLast(10);
The database here again orders the nodes on their timeStamp, it then finds the node that is at the top of the screen based on the values you give, and it then returns the 10 nodes right before that.
After several days of testing code, I came up with the following solution
The first step is to declare a ScrollController in your state class.
final ScrollController _scrollController = ScrollController();
You will also need to declare a List to store query results
List list = [];
Next, use the following function to get initial data
getStartData() async {
//replace this with your path
DatabaseReference starCountRef =
FirebaseDatabase.instance.ref('messages/${widget.placeID}');
starCountRef
.orderByChild("timeStamp")
//here, I limit my initial query to 6 results, change this to how many
//you want to load initially
.limitToLast(6)
.onChildAdded
.forEach((element) {
setState(() {
list.add(element.snapshot.value);
list.sort((a, b) => a["timeStamp"].compareTo(b["timeStamp"]));
});
});
}
Run this in initState
void initState() {
super.initState();
FirebaseDatabase.instance.setPersistenceEnabled(true);
getStartData();
}
Now to display the initial data that was generated when the page was loaded, build the results into a ListView
ListView.builder(
itemCount: list.length,
controller: _scrollController,
//here I use a premade widget, replace MessageWidget with
//what you want to load for each result
itemBuilder: (_, index) => MessageWidget(
message: list[index]["text"],
date: list[index]["timeStamp"],
id: list[index]["uid"],
profilePicture: list[index]["profilePicture"],
name: list[index]["name"],
lastSender: list[index]["lastSender"],
),
),
Note that your ListView must be constrained, meaning that you can scroll to the beginning or end of your ListView. Sometimes, the ListView won't have enough data to fill and be scrollable, so you must declare a height with a Container or bound it to its contents.
Now you have the code that fetches data when the page is loaded using getStartData() and initState. The data is stored in list, and a ListView.builder builds a MessageWidget for each item returned by getStartData. Now, you want to load more information when the user scrolls to the top.
getMoreData() async {
var moreSnapshots = await FirebaseDatabase.instance
.ref("messages/${widget.placeID}")
.orderByChild("timeStamp")
.endBefore(list[0]["timeStamp"])
.limitToLast(20)
.once();
var moreMap = moreSnapshots.snapshot.value as dynamic;
setState(() {
list.addAll(moreMap.values.toList());
list.sort((a, b) => a["timeStamp"].compareTo(b["timeStamp"]));
});
}
Then, make the function run when the ListView.builder is scrolled all the way to the top by adding this to the already existing initState.
_scrollController.addListener(() {
if (_scrollController.position.atEdge) {
bool isTop = _scrollController.position.pixels == 0;
if (isTop) {
getMoreData();
}
}
});
Hopefully this helps or gives you a pointer on where to go from here. Thanks to Frank van Puffelen for his help on which query to use based on the previous answer.

filter data using list flutter firestore

I am creating a bookstore where customers who have their favorite books have a favBook list and the website displays the name and images of the book of all their favorite books. The book model looks something like this:
class BookModel
{
String? bookImage;
String? bookName;
String ? bookId;
BookModel({ this.bookImage, this.bookName,this.bookId});
//data from server
factory BookModel.fromMap(map)
{
return BookModel(
bookImage: map['bookImage'],
bookName: map['bookName'],
bookId: map['bookId'],
);
}
// data to server
Map<String, dynamic> toMap(){
return{
'bookImage': bookImage,
'bookName': bookName,
'bookId':bookId,
};
}
}
For now, I have created a list of strings with the bookId on the main page as well as called my BookModel and have a list of books available stored as such:
List<String> favBooks = ["C8n4Tjwof7RhIspC7Hs","Hc3hTsWkg9vHwGN37Jb"];
List<BookModel> bookList = [];
#override
void initState()
{
super.initState();
FirebaseFirestore.instance
.collection("books")
.where("bookId", isEqualTo: favBooks[0])
.get()
.then((value){
if (value != null && value.docs.isNotEmpty) {
bookList = value.docs.map((doc) => BookModel.fromMap(doc.data())).toList();
setState(() {
});
} else {
}
});
}
and then in my build widget im calling the booklist as such:
final books= ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: favBooks.length,
itemBuilder: (BuildContext context,int index)
{
return ListTile(
leading: Ink.image(
image: NetworkImage(bookList[0].bookImage.toString()),
height: 70,
width: 70,
),
title: Text(bookList[0].bookName.toString()),
);
}
);
this is how my database looks like:
[![enter image description here][1]][1]
the issue I'm facing now is that in my where clause when I do favBooks[0] it displays the data but I want to display book the books and now just one book. I tried doing favBooks to be able to get book names and images of the 2 books in the favBook list but it does not work. How do I do it so that when I pass a list of books Id I get the name and image of those lists? (edit: I converted the button to a list view and added image of database)
instead of
.where("bookId", isEqualTo: favBooks[0])
I used
.where("bookId", whereIn: favBooks)
and it worked
I am not sure if I understand the question correctly, but if you are asking how to get collection of all books that have ID's of those favorite books (e.g. your favBooks list) i don't think that would be possible by single query like that. My suggestion would be to create additional field in your FS database in your User. (lets say you have collection Users) in this collection where you store users as documents with unique ID's, for each user you create additional field FavBooks, which would be a list of references to the books. Every time user marks a book as his favourite, you would add a reference to that book to this filed in a user document. After which you can simply create a query to select all books references from this list.

Flutter Streambuilder map to List object

I need to display a listview in Flutter with data from firestore. Then I want the user to be able to filter the listview by typing his query in a textfield in the appbar. This is the code I came up with for the listview:
_buildAllAds() {
return StreamBuilder(
stream: Firestore.instance.collection("Classificados")
.orderBy('title').snapshots().map((snap) async {
allAds.clear();
snap.documents.forEach((d) {
allAds.add(ClassificadoData(d.documentID,
d.data["title"], d.data["description"], d.data["price"], d.data["images"] ));
});
}),
builder: (context, snapshot) {
// if (!snapshot.hasData) {
// return Center(child: CircularProgressIndicator());
// }
//else{
//}
if (snapshot.hasError) {
print("err:${snapshot.error}");
}
return ListView.builder(
itemCount: allAds.length,
itemBuilder: (context, index) {
ClassificadoData ad = allAds[index];
return ClassificadosTile(ad);
});
});
}
The reason I save the stream data in the List allAds of type ClassificadoData (data items are ads) is because I can then copy it to another List filteredAds on which the user can perform filtering. And the reason I need a stream for allAds is because I want users to be able to see additions/updates in real time.
So this code "works" but it feels a bit awkward and I also can't do nothing with the builder since snaphot remains null all the way (can't show loader during initial data fetch, for example).
Was wondering if there's maybe a more solid way for doing what I want and if it's possible to get a reference to the snapshots down to the builder.
You seem to be mixing two different concepts of using Streams and Stream related Widgets. Ideally you would either use a StreamBuilder and use the data you get from the stream directly on the Widget, or listen to the data and update a variable that is then used to populate your ListView. I've build the latter as an example from your code:
#override
initState(){
_listenToData();
super.initState();
}
_listenToData(){
Firestore.instance.collection("Classificados")
.orderBy('title').snapshots().listen((snap){
allAds.clear();
setState(() {
snap.documents.forEach((d) {
allAds.add(ClassificadoData(d.documentID,
d.data["title"], d.data["description"], d.data["price"], d.data["images"] ));
});
});
});
}
_buildAllAds() {
return ListView.builder(
itemCount: allAds.length,
itemBuilder: (context, index) {
ClassificadoData ad = allAds[index];
return ClassificadosTile(ad);
}
);
}

Filter Stream to List

I have the following stream builder:
streamCard() {
return StreamBuilder(
stream: cardsRef
.orderBy("timestamp", descending: true)
.limit(10)
.snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return circularProgress();
}
List<CustomCard> cards = [];
snapshot.data.documents.forEach((doc) {
cards.add(CustomCard.fromDocument(doc));
});
...
return Stack(
alignment: Alignment.center,
children: cards,
);
I need to filter certain cards from being added to the stream/displayed when:
I'm the owner of the card ('ownerId' == currentUserId)
I've liked the card ('liked.' contains = currentUserId)
OwnerId is a field inside each document and Liked is an array with Id's who have liked it.
I've tried to remove the cards from being added to the cards List<> with .where and .contains, but couldn't properly 'discard' them. I was thinking another option could be to modify the Stack directly, in
children: cards
with cards.removeWhere/.skip, or something like that.
To follow the bloc pattern, business logic should happen in your bloc class. Which mean that you should do all the sorting or filtering in the bloc. When you add a new object into sink, the streambuilder will rebuild.
class BlaBloc {
final BehaviorSubject<List<String>> _results =
BehaviorSubject<List<String>>();
getResults() {
List<String> yourresults = yourapi.getResults();
_results.sink.add(yourresults);
};
getResultsLikedByMe() {
List<String> yourresults = yourapi.getResultsLikedByMe();
_results.sink.add(yourresults);
}
getResultsOwnerIsMe() {
List<String> yourresults = yourapi.getResultsOwnerIsMe();
_results.sink.add(yourresults);
}
BehaviorSubject<List<String>> get results => _results;
}
final blaBloc = BlaBloc();
When you build your streambuilder, point to your Bloc, for example as below:
body: StreamBuilder<List<String>>(
stream: blaBloc.results,
builder: (context, AsyncSnapshot<RecipesResponse> snapshot) {
// build your list here...
}
)
To understand more about Bloc pattern, this is a very useful tutorial you could follow here: RxDart Bloc Pattern

AsyncSnapshot state is always connectionState.waiting

I'm trying to have a ListView dynamically update depending on the contents of a TextField (a search bar).
The ListView is inside a "ScenariosList" widget, and contains a list of "Scenarios", which is a custom widget containing a title, content and other bits of data (not really relevant but helpful for context). It's content is fetched from a database via a "ScenariosBloc".
The TextField is contained within a "SearchBar" widget.
The goal is to have the contents of the ListView change whenever a change to the TextField is detected.
I'm currently using two individual blocs. ScenariosBloc fetches all the scenarios from the database and FilterScenariosBloc makes the List render a widget to show the scenario if it's title contains the string in the TextView within the SearchBar.
I'm using nested StreamBuilders to do this (see code below).
ScenariosList.dart
// build individual scenario cards
Widget _buildScenarioListItem(Scenario scen, String filter) {
if (!(filter == null || filter == "")) {
print("null filter");
if (!(scen.title.contains(filter))) {
print("match!");
return ScenarioCard(scen);
}
}
return Container();
}
Widget _buildScenarioList(BuildContext context) {
return StreamBuilder(
stream: scenarioBloc.scenarios,
builder: (BuildContext context,
AsyncSnapshot<List<Scenario>> scenariosSnapshot) {
if (!scenariosSnapshot.hasData) {
return CircularProgressIndicator();
}
return StreamBuilder(
stream: filterScenariosBloc.filterQuery,
initialData: "",
builder: (BuildContext context, AsyncSnapshot filterSnapshot) {
if(!filterSnapshot.hasData) return CircularProgressIndicator();
print("Scenarios Snapshot: ${scenariosSnapshot.toString()}");
print("Filter Snapshot: ${filterSnapshot.toString()}");
return ListView.builder(
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.all(0),
shrinkWrap: true,
itemCount: scenariosSnapshot.data.length,
itemBuilder: (BuildContext context, int index) {
Scenario scen = scenariosSnapshot.data[index];
return _buildScenarioListItem(scen, filterSnapshot.data);
},
);
});
});
}
}
SearchBar.dart
the onChanged method of the Textfield contains:
// function to filter the scenarios depending on the users input.
void filterSearchResults(String query) {
_filterScenariosBloc.doFilter(query);
}
FilterScenariosBloc.dart
class FilterScenariosBloc {
// stream - only need one listener
final _searchController = StreamController<String>.broadcast();
// output stream
get filterQuery => _searchController.stream;
FilterScenariosBloc() {
doFilter(" ");
}
doFilter(String query) {
_searchController.sink.add(query);
}
}
The user input is sent to the FilterScenariosBloc all fine, but the status of the filterSnapshot is always connectionState.waiting.
Any ideas on how I can resolve this?
I had the same issue. The problem was that my firestore DB rules did not allow read or write for the collection in question. Please see if that solves your prob
I had the same issue, always having the connectionState.waiting, and thus the snapshot.data
was null. This means that for whatever reason, the data could not be fetched.
I then ran the app into the debug mode and got an error like "Cannot fit requested classes into a single dex file". Then I just followed this answer and it solved the issue for me.