Efficient storage fetching for ListView.builder, GridView.builder - flutter

In order to fetch data from the API with the builder function of ListView.builder or GridView.builder you have to create a list of Widgets that is being filled when you scroll.
The actual reason for using the Builder function is, to deal with large Lists so that only widgets are being rendered/build when needed:
List<Widget> _mediaList = [];
int currentPage = 0;
int? lastPage;
#override
void initState() {
super.initState();
_fetchNewMedia();
}
_handleScrollEvent(ScrollNotification scroll) {
if (scroll.metrics.pixels / scroll.metrics.maxScrollExtent > 0.33) {
if (currentPage != lastPage) {
_fetchNewMedia();
}
}
}
_fetchNewMedia() async {
lastPage = currentPage;
setState(() {
_mediaList.add(
Text("Some Widget"),
);
currentPage++;
});
}
#override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scroll) {
_handleScrollEvent(scroll);
return false;
},
child: GridView.builder(
controller: widget.scrollCtr,
itemCount: _mediaList.length,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
itemBuilder: (BuildContext context, int index) {
return _mediaList[index];
}),
);
}
The problem that I see is, that if you have an endless list (like a Post feed), that the list would store every Data source and the list would eventually jam up the RAM.
I would imagine that you either have to clear the list after scrolling for a long time or you would need to only store String-IDs and load the data according to them.
Is that concern appropriate or does the builder also optimize the storage in that case?

You should store String-IDs and build the list items using FutureBuilder(). Because although ListView.builder() renders only those items which are visible on the device screen but in case of post feed the post data may consume so much storage which can cause efficiency problem.

Related

Data not updating to another widget flutter

I am trying to show the price of items in the cart but the total value should be shown in TextField. I am saving data to SQLite and retrieving then show to a widget, but when I try to access total_price to another widget it's not updating, but When I press hot reload again the data shows but not first time when I am opening the page
return FutureBuilder<List<CartModel>>(
future: fetchCartFromDatabase(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data.length > 0) {
cartCount = snapshot.data.length;
for(int i = 0;i<snapshot.data.length;i++){
var price = snapshot.data[i].product_price.split("₹");
total_price+=double.parse(price[1]);
}
} else if (snapshot.hasData && snapshot.data.length == 0) {
return new Text("No Data found");
}
else
{
return new Container(
alignment: AlignmentDirectional.center,
child: new CircularProgressIndicator(),
);
}
);
value initialized
int cartCount = 0;
double total_price=0.0;
The FutureBuilder updates only its children. To update the value of another widget you must use setState.
The best way would be putting FutureBuilder in an upper level or using some sort of state manager, like provider.
To use setState you need to initialize you fetch from an initState of a stetefullWidget (or to call it from a function). This way you will not need a FutureBuilder and must refactor your code:
class YourWidget extends StatefulWidget {
#override
_YourWidgetState createState() => _YourWidgetState();
}
class _YourWidgetState extends State<YourWidget> {
double total_price = 0;
#override
void initState() {
super.initState();
fetchCartFromDatabase().then((value){
setState((){
for(int i = 0;i<value.length;i++){
var price = value[i].product_price.split("₹");
total_price+=double.parse(price[1]);
}
});
});
}
}
The addPostFrameCallback is not a good solution, since it updates the value only in the next frame. When the app grows it leads to lags.
To continue using the FutureBuilder, move your widget tree that needs to be updated to be inside of the FutureBuilder.

Flutter Firestore Convert Stream To Future

I have a firestore stream in flutter that I would instead like to be a future so that I can do pagination of requests. Currently I periodically increase the limit variable in the code below and reload the whole original stream plus new data. This is very irritating because every time the limit variable increases the widget (a listview) scrolls to the top. I would like to ask how to convert the stream below into a future and how to place its contents into a list. My purpose of doing so being that the contents of all the future calls will be accumulated in an list and my listview will be generated off of that array, hopefully without scrolling to the top every time.
My other reason for doing so is to save memory on the client device. When a user scrolls down I would like to remove items from the front of the list to save memory and reload them only if the user scrolls back up. My project is a social-media application so I foresee users scrolling down indefinitely and using up all their phone memory. I am new to flutter so I would also like to ask if this memory usage is a valid concern.
Stream<List<Memo>> getFeed(int limit) async* {
yield* Firestore.instance
.collection('memos')
.where('followers', arrayContains: userid)
.orderBy("date")
.limit(limit) // TODO: add pagination of request
// .startAfterDocument(null)
.snapshots()
.map(_snapshotToMemoList);
}
My Streamsubscription and listview builder code is as follows:
Widget build(BuildContext context) {
return StreamBuilder<List<Memo>>(
stream: dbService( user: widget.user ).getFeed( streamLimit ),
builder: (BuildContext context, AsyncSnapshot<List<Memo>> snapshot) {
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Text('Loading...');
default:
if (snapshot.data.isEmpty) {
return Text('EMPTY');
}
// streamSub.cancel();
// return Text(snapshot.data[1].body);
return ListView.builder(
controller: _scrollController,
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return Container(
height: 600,
child: Text(snapshot.data[index].body)
);
}
);
}
},
);
Lastly, my limit increasing function is
void initState() {
if (lastScrollPosition != null) _scrollController.jumpTo(lastScrollPosition);
_scrollController.addListener(() {
final maxScroll = _scrollController.position.maxScrollExtent;
// print(maxScroll);
final currentScroll = _scrollController.position.pixels;
// print(currentScroll);
if (maxScroll - currentScroll <= _scrollThreshold) {
setState(() {
lastScrollPosition = currentScroll;
streamLimit += 1;
print('increasing');
});
}
});
}

List view item builder calls multiple times

I have an infinite list like below. But item builder works multiple times after loading data, i don't understand when it happen, sometimes when scrolling happen, sometimes after initial data load . So it call FlashNewsCard build function multiple times as well as buildNewsCard function. Is any way to restrict this behaviour.
#override
void initState() {
super.initState();
newsBloc = NewsBloc();
newsBloc.newsEventSink.add(MoreNewsEvent());
widget.scrollController.addListener(_onScroll);
}
===============================
ListView.builder(
physics: BouncingScrollPhysics(),
shrinkWrap: true,
itemCount: newsBloc.hasReachedmax
? snapshot.data.length + 1
: snapshot.data.length + 2,
itemBuilder: (context, index) {
if (index == 0) {
return FlashNewsCard();
}
index -= 1;
return Padding(
padding: const EdgeInsets.only(
bottom: 4.0, left: 6.0, right: 5.0),
child: buildNewsCard(index, snapshot));
})
Bloc class
class NewsBloc {
final _newsStateController =StreamController<List<News>>();
StreamSink<List<News>> get
_inNewsList =>_newsStateController.sink;
Stream<List<News>> get
newsList =>_newsStateController.stream;
final _newsEventController =StreamController<NewsEvent>();
StreamSink<NewsEvent> get
newsEventSink =>_newsEventController.sink;
NewsBloc() {
_newsEventController.stream.listen(_mapStateToEvent);
}
void _mapStateToEvent(NewsEvent newsEvent) async {
final newses =await fetchNewFromApi ();
_inNewsList.add(newses)
}
}
I checked the stream controller and it serve data correctly. Also i want to build FlashNewsCard widget to build only at initial data load.
UPDATE : The stream builder for the listview is inside the refreshindicator widget.
Found a similar question StreamBuilder inside RefreshIndicator render child widget many times, how to avoid it? but couldn't find a solution
StreamBuilder rebuilds its children when changes in Stream has been detected. If you like to only load the Stream once, you can initialize the Stream on initState(). This should prevent rebuilding the children widget.
late Stream _stream;
#override
initState() {
super.initState();
...
_stream = yourStream();
}
#override
Widget build() {
return StreamBuilder(stream: _stream, ...);
}

GridView.count() not updating when values changed in cron

What I'm trying to do
What I'm trying to do is retrieve data from an API call and pass the data in the response to a GridView.count() widget every minute because the data could change.
I did this using a FutureBuilder widget and the Cron functionality from the cron/cron.dart if that helps.
Here is the code:
FutureBuilder<dynamic>(
future: Api.getFoods(),
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
List<Widget> slots = [];
if (snapshot.data == null) {
return Text('');
}
var data = snapshot.data;
cron.schedule(new Schedule.parse('*/1 * * * *'), () async {
setState(() {
data = snapshot.data;
});
slots = [];
});
for (int i = 0; i < snapshot.data.length; i++) {
slots.add(new FoodSlot(
snapshot.data[i]['name'],
snapshot.data[i]['created_at'],
snapshot.data[i]['qty'].toString()
));
}
return new GridView.count(
crossAxisCount: 2,
scrollDirection: Axis.vertical,
children: slots
);
})
The FoodSlot widget creates a Card and displays the value passed in the arguments on the card.
What I tried
After debugging, I saw that the cron job works fine, but the GridView widgets just won't update.
I tried using a Text widget instead of the GridView and return the values returned by the API call and the widget is updated automatically every 1 minute as expected.
Why is GridView.count() acting like this and how can I fix this?
UPDATE
When the changes in the database are made, the GridView does update, but only when the application is restarted (using R not r).
Solved!
Turns out I had to add the cron job in the FoodSlot widget and update the widget itself periodically.
Ended up adding the following in the initState() function for FoodSlot:
#override
void initState() {
super.initState();
colorSettings = HelperFunctions.setStateColour(expiry);
cron.schedule(new Schedule.parse('*/1 * * * *'), () async {
setState(() {
Api.getFood(id).then((res) {
expiry = res['created_at'];
name = res['name'];
qty = res['qty'].toString();
colorSettings = HelperFunctions.setStateColour(expiry);
});
});
});
}
Where id is the id of the database entry referenced by the FoodSlot widget.
Hope this helps someone :)

Flutter, ListView, How to add several items on top of the ListView and make it not scroll to the top

When I add some items on top of the ListView it scrolls to the top item on the 0 index. But I need it to stay in the same position as before adding items.
For example, chat history pagination on the top of the list of messages, if I open any messenger (Telegram, WhatsApp, etc.) open chat with a long history and scroll down downloading the history. History will be added to the top of the list (from the server ~20 messages at a time) but the list will stay on the same position (while scrolling).
Flutter ListView behaves like that if you add to the bottom, but if you add to the top it jumps to the first added item. I want it to stay.
Screenshot:
Since you didn't share any code, I just created a simple demo to show how you can achieve the effect like a messaging app. The code is very simple to understand, I have used comments almost everywhere.
Code (Null safe):
class _MyPageState extends State<MyPage> {
// Say you have total 100 messages, and you want to load 20 messages on each scroll.
final int _totalMessages = 100, _loadInterval = 20;
final double _loadingOffset = 20;
late final List<String> _messages;
bool _loading = false;
final ScrollController _controller = ScrollController();
#override
void initState() {
super.initState();
// Initially, load only 20 messages.
_messages = List.generate(20, (i) => 'Message #${_totalMessages - i}');
_controller.addListener(_scrollListener);
}
void _scrollListener() async {
var max = _controller.position.maxScrollExtent;
var offset = _controller.offset;
// Reached at the top of the list, we should make _loading = true
if (max - offset < _loadingOffset && !_loading) {
_loading = true;
// Load 20 more items (_loadInterval = 20) after a delay of 2 seconds
await Future.delayed(Duration(seconds: 2));
int lastItem = _totalMessages - _messages.length;
for (int i = 1; i <= _loadInterval; i++) {
int itemToAdd = lastItem - i;
if (itemToAdd >= 0) _messages.add('Message #$itemToAdd');
}
// Items are loaded successfully, make _loading = false
setState(() {
_loading = false;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Messages')),
body: ListView.builder(
controller: _controller,
reverse: true,
itemCount: _messages.length + 1, // +1 to show progress indicator.
itemBuilder: (context, index) {
// All messages fetched.
if (index == _totalMessages) return Container();
// Reached top of the list, show a progress indicator.
if (index == _messages.length) return Align(child: CircularProgressIndicator());
// Show messages.
return ListTile(title: Text('${_messages[index]}'));
},
),
);
}
}
The solution proposed by #CopsOnRoad works if your feed can expand in one direction only.
This solution works for appending items to the top and the bottom of the list. The idea of the solution is the following. You create two SliverLists and put them inside CustomScrollView.
CustomScrollView(
center: centerKey,
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
// Here we render elements from the upper group
child: top[index]
)
}
),
SliverList(
// Key parameter makes this list grow bottom
key: centerKey,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
// Here we render elements from the bottom group
child: bottom[index]
)
}
),
)
The first list scrolls upwards while the second list scrolls downwards. Their offset zero points are fixed at the same point and never move. If you need to prepend an item you push it to the top list, otherwise, you push it to the bottom list. That way their offsets don't change and your scroll view does not jump.
You can find the solution prototype in the following dartpad example.