Count number of user viewed the posts in ListView FLutter - flutter

I am loading posts from firebase and want to call a function when user scroll to each new post. Basically I want to store user's id in 'Post' collection who view the post. But I am unable to get post ID on scrolling so that I update record on firebase.

There is no such easy way to do this, but you can use VisibilityDetector from visibility_detector package:
You can get the index of the last list item that is completely visible on screen by wrapping each list item with a VisibilityDetector.
_visibleItem = 0;
itemBuilder: (context, index) {
return VisibilityDetector(
key: Key(index.toString()),
onVisibilityChanged: (VisibilityInfo visibilty) {
if (visibilty.visibleFraction == 1)
setState(() {
log(_visibleItem);
_visibleItem = index;
});
},
child: ListTile(title: Text("Index $index"))
);
},

Related

Flutter - what is the best way to load 10,000 more of the list of sentences from json file

I have a json file which have more than 10,000 list of sentences. I want to know what is the effective way when I click a button, it will redirect to a page and load the list of 10,000 sentences without slowing the performance or crash the memory.
Below are the sample of my code:
class _PageLoadState extends State<PageLoadState> {
Future<BookModel> getSentencesList() async {
final response = await rootBundle.loadString('assets/books.json');
var data = jsonDecode(response);
return BookModel.fromJson(data);
}
// This is the body section
Expanded(
child: FutureBuilder<BookModel>(
future: getSentencesList(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, index) => Wrap(
children: [
Padding(
padding: EdgeInsets.all(18.0),
child: Text(
snapshot.data[index].sentence.toString(),
),
)
],
)
);
}
You can use lazy loading. Ex. You will just load 100 sentences and when the user scrolls and hits the bottom of the screen you will increment it by +100 and so on.
you can check this:
https://pub.dev/packages/lazy_load_scrollview
you can use the pagination for this. that easy to load a huge amount of list.
using pagination you can load the list as per requirements for example: first, you need to load only 100 items of a list, and when the user scrolls over the 100 items you can load more ,100 items.
and for that, you can use:- loadmore package
https://pub.dev/packages/loadmore
you can see the following example as a reference.

Can I Change the data of a single particular item of a Listview.builder, Without losing the previous data for the other items In Flutter

Let me Explain,
Suppose I have a Listview.builder with 3 item
and I Want to change the data of item no.2 without changing the other items data(1 and 3).
And I have two data sources for updating on those 3 items
according to button press.
Please Help me if you have any idea to solve this problem.
Thank You
Welcome #Rahul Choudhury!
ListView.builder has the property itemBuilder who accepts the index argument.
You can use that one!
final items = List<String>.generate(3, (i) => "Item $i");
const int specialIndex = 1;
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
if (specialIndex == index){
//Use here your custom amazing widget
return const ListTile(
title: Text("Flutter is awesome"),
);
}else {
return ListTile(
title: Text(items[index]),
);
}
},
);
Obviously, I suggest you to refactor this code as you like, but I wanted to give you an idea ;)

Flutter + Firestore chat .. Listview rebuilds all items

I'm trying to add chat functionality to my app (kind of similar to WhatsApp functionalities), using flutter and Firestore. The main structure on Firestore is to have 2 collections (I want the unread message count as well):
users: and each user will have a subcollection "chats" that will include all CHATS_IDs. This will be the main place to build home chat page (shows a history list of all chats) by getting the user chat list.
chats: a list of all chats and each chat document has a subcollection of messages.
My main issue is in building the home page (where a list of all user previous chats should appear). I get/subscribe the user chat subcollection, and for each chat ID listed in there I also subscribe for the chat itself in the chat collection (using the ID).
Here are screenshots of it in principle:
users collection:
chats coleection:
and here is the main screen of interest (principle from whatsapp screen):
What I'm doing is that I retrieve user's chat subcollection (and register a listener to it using StreamBuilder), and also for number of unread messages/last message and last message time, I subscribe to listen for each of these chats (and want to use each user last message time, status and his last presence in that chat doc to calculate the unread count) .
The problem is that Listview.builder rebuilds all items (initially and on scroll) instead of just the viewed ones. here is my code:
Stream<QuerySnapshot> getCurrentUserChats(userId) {
return FirebaseFirestore.instance
.collection(AppConstants.USERS_COLLECTION)
.doc('$userId')
.collection(AppConstants.USER_CHATS_SUBCOLLECTION)
.orderBy('lastMsgTS', descending: true)
.snapshots()
.distinct();
}
Widget getRecentChats(userId) {
return StreamBuilder<QuerySnapshot>(
stream: getCurrentUserChats(userId),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data.docs.isNotEmpty) {
print('snapshot of user chats subcoll has changed');
List<QueryDocumentSnapshot> retrievedDocs = snapshot.data.docs;
return Container(
height: 400,
child: ListView.builder(
//childrenDelegate: SliverChildBuilderDelegate(
itemCount: snapshot.data.size,
itemBuilder: (context, index) {
String chatId = retrievedDocs[index].id;
print('building index: $index, chatId: $chatId');
return StreamBuilder(
stream: FirebaseFirestore.instance
.collection(AppConstants.CHATS_COLLECTION)
.doc('$chatId')
.snapshots()
.distinct(),
builder:
(context, AsyncSnapshot<DocumentSnapshot> snapshot) {
if (snapshot.hasData) {
print('${snapshot.data?.id}, isExist: ${snapshot.data?.exists}');
if (snapshot.data.exists) {
return KeyProxy(
key: ValueKey(chatId),
child: ListTile(
leading: CircleAvatar(
child: Container(
//to be replaced with user image
color: Colors.red,
),
),
title: Text('$chatId'),
subtitle: Text(
"Last Message received on: ${DateTimeUtils.getDateViewFromDT(snapshot.data.data()['ts']?.toDate())}"),
),
);
}
}
return SizedBox.shrink();
},
);
},
/*childCount: snapshot.data.size,
findChildIndexCallback: (Key key) {
print('calling findChildIndexCallback');
final ValueKey valKey = key;
final String docId = valKey.value;
int idx = retrievedDocs.indexOf(retrievedDocs
.where((element) => element.id == docId)
.toList()[0]);
print('docId: $docId, idx: $idx');
return idx;
}*/
),
);
}
return Center(child: UIWidgetUtils.loader());
});
}
After searching, I found these related suggestions (but both didn't work):
A github issue suggested thesince the stream is reordarable (github: [https://github.com/flutter/flutter/issues/58917]), but even with using ListView.custom with a delegate and a findChildIndexCallback, the same problem remained.
to use distinct.
But removing the inner streambuilder and just returning the tiles without a subscription, makes the ListView.builder work as expected (only builds the viewed ones). So my questions are:
Why having nested stream builders causing all items to be rebuil.
is there a better structure to implement the above features (all chats with unread count and last message/time in real-time). Especially that I haven't added lazy loading yet. And also with this design, I have to update multiple documents for each message (in chats collection, and each user's subcollection).
Your help will be much appreciated (I have checked some other SO threads and medium articles, but couldn't find one that combines these features in one place with and preferably with optimized design for scalability/price using Firestore and Flutter).
I think that You can do this:
Widget build(ctx) {
return ListView.builder(
itemCount: snapshot.data.size,
itemBuilder: (index, ctx) =>_catche[index],
)
}
and for _catche:
List<Widget> _catche = [/*...*/];
// initialize on load

How to get the index number on scroll for listview in flutter?

I am using below code for the listview builder in flutter application I need to get the index of the item in the list upon scrolling. Just Like function of onPageChanged:(){} while using PageView.Builder
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final item = posts[index];
return Post(index: index, title: 'Test', imageUrl: 'https://www.google.com',);
},
);
How can I get that?
There is no such easy way to do this, but you can use VisibilityDetector from visibility_detector package:
You can get the index of the last list item that is completely visible on screen by wrapping each list item with a VisibilityDetector.
itemBuilder: (context, index) {
return VisibilityDetector(
key: Key(index.toString()),
onVisibilityChanged: (VisibilityInfo info) {
if (info.visibleFraction == 1)
setState(() {
_currentItem = index;
print(_currentItem);
});
},
child: Post(index: index, title: 'Test', imageUrl: 'https://www.google.com',)
);
},
_currentItem is a variable in the main widget's state that stores the index of the last visible item:
int _currentItem = 0;
Note: This is based on scroll direction, i.e., the last visible item's index equals to the index of the last item that is visible after the scroll; if the scroll is in forward direction, it is the index of the item on bottom of the screen, but if the scroll is in reverse direction, it is the index of the item on top of the screen. This logic can be easily changed though (e.g., using a queue to store the total visible items).

Flutter Animated List: Conditionally add ListView item

I am have an animated list in my flutter project.
For every element in that list I have a grid of buttons that are placed dynamically from a Firestore stream. Sometimes that will come back with 10 items in the grid but other times that will comeback with 0 items.
When a button on the grid in a list element is pushed it will search firestore and create a new grid of buttons in the next list element below.
The problem that I have is when it comes back with 0 grid buttons I don't want it to create a new list element (an empty list element with no grid buttons). I tried returning a container with 0 size as a work around but animated list still gives it some height so you can see there is a problem. I also understand that this would be bad practice as you have non visible empty list elements in the list.
I start with a list of foods as strings:
List foodListGrids = ['breads','drinks']
I then have an animated list:
AnimatedList(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
key: _FoodandDrinkKey,
initialItemCount: foodListGrids.length,
itemBuilder: (context, index, animation) {
return SizeTransition(
sizeFactor: animation,
child: buildButtonGridItemsMenu(index),
);
},
),
I set the AnimatedList size to the length of the foods list.
I set the child of the Animated List to a class that searches firebase and returns a card with the grid of buttons on it like this:
StreamBuilder(
stream: Firestore.instance
.collection(widget.categoryType)
.where(widget.relationship, isEqualTo: widget.searchString)
.snapshots(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (!snapshot.hasData) {
return Container(width: 0, height: 0,);
} else if (snapshot.hasData) {
List<Widget> widgetList = [];
List<DocumentSnapshot> documentList = snapshot.data.documents;
if (documentList.length > 0) {
for (int i = 0; i < documentList.length; i++) {
widgetList.add(ButtonTheme(
minWidth: 16,
height: 30,
child: GridButton(snapshot, i, widget.listIndex),
));
}
return Container(
width: double.infinity,
child: Wrap(
children: widgetList,
alignment: WrapAlignment.center,
));
} else{
return Text('NO DATA BECAUSE NUMBER OF GRID ITEMS IS 0');
}
} else {
return Text('NO DATA BECAUSE GRID ITEMS CALL IS NULL');
}
},
),
then in the on pressed method for each grid button I add a new list element like this:
void _insertCategoryGridItem(String id, int index) {
if (!foodListGrids.contains(id)) {
foodListGrids.add(id);
_FoodandDrinkKey.currentState.insertItem(index + 1);
}
}
The problem is a chicken or the egg problem I will try to show below:
List item is generated from the index 0 in the food list and all stream data is if from food list index 0 firebase results.
On pressed for a grid item in the first list row is pressed to add a new list row with a new set of grid items. This will then update the food list array and the call for the list to add new row of grid buttons. The issue is because this is in the onpressed for the first rows grid there is no knowledge of what will be returned for the next row so there is no way of knowing if it will return a grid of size 0 in the next list row in the current way it is setup.
I have tried returning null, container of 0 width and height but have had no luck. I am not sure what I can do to fix it.
Thanks for your help
I'm not sure if I get you right but seems that I faced the same problem with AnimatedList and stream of data from the Firestore. The problem is in initialItemCount: property of the AnimatedList.
In my case I wanted to change AnimtedList in two ways:
I wanted to manually add an item and to show it with animation.
I want that if the list is changed due to a new portion of data from the stream - I want the list to be updated without animation of inserting and without errors (out of range).
To solve this I did a dirty hack: when there is an update from the stream I reinit the key of the list, in your case it's _FoodandDrinkKey. So BEFORE you build the AnmatedList just reinit your key _listKeyUserNotes = GlobalKey(); that's how the List will "forget" about it's initialItemCount and will render a new data without any out-of-range errors.
When you want to add a new item manually with animation - use insert().
key: _FoodandDrinkKey,
initialItemCount: foodListGrids.length,
Hope this makes sense.