List view item builder calls multiple times - flutter

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, ...);
}

Related

Efficient storage fetching for ListView.builder, GridView.builder

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.

setState not causing UI to refresh

Using a stateful widget, I have this build function:
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Row(
children: [
Icon(MdiIcons.atom),
Text("Message Channel")
],
)
),
body: compileWidget(context),
bottomSheet: Row(
children: [
Expanded(
child: DefaultTextFormField(true, null, hintText: "Message ...", controller: messageField)
),
IconButton(
icon: Icon(Icons.send),
onPressed: onSendPressed
)
],
),
);
}
As you can see in the body, there is a compileWidget function defined as:
FutureBuilder<Widget> compileWidget(BuildContext context) {
return FutureBuilder(
future: createWidget(context),
builder: (context, snapshot) {
if (snapshot.hasData) {
print("DONE loading widget ");
return snapshot.data;
} else {
return Container(
child: Center(
child: CircularProgressIndicator()
),
);
}
}
);
}
In turn, the createWidget function looks like this:
List<Widget> bubbles;
Future<Widget> createWidget(BuildContext context) async {
await updateChatList();
// when working, return listview w/ children of bubbles
return Padding(
padding: EdgeInsets.all(10),
child: ListView(
children: this.bubbles,
),
);
}
// update chat list below
Future<void> updateChatList({Message message}) async {
if (this.bubbles != null) {
if (message != null) {
print("Pushing new notification into chat ...");
this.bubbles.add(bubbleFrom(message));
} else {
print("Update called, but nothing to do");
}
} else {
var initMessages = await Message.getMessagesBetween(this.widget.implicatedCnac.implicatedCid, this.widget.peerNac.peerCid);
print("Loaded ${initMessages.length} messages between ${this.widget.implicatedCnac.implicatedCid} and ${this.widget.peerNac.peerCid}");
// create init bubble list
this.bubbles = initMessages.map((e) {
print("Adding bubble $e");
return bubbleFrom(e);
}).toList();
print("Done loading bubbles ...");
}
}
The chat bubbles populate the screen in good order. However, once a new message comes-in and is received by:
StreamSubscription<Message> listener;
#override
void initState() {
super.initState();
this.listener = Utils.broadcaster.stream.stream.listen((message) async {
print("{${this.widget.implicatedCnac.implicatedCid} <=> ${this.widget.peerNac.peerCid} streamer received possible message ...");
if (this.widget.implicatedCnac.implicatedCid == message.implicatedCid && this.widget.peerNac.peerCid == message.peerCid) {
print("Message meant for this screen!");
await this.updateChatList(message: message);
setState(() {});
}
});
}
The "message meant for this screen!" prints, then within updateChatList, "Pushing new notification into chat ..." prints, implying that the bubble gets added to the array. Finally, setState is called, yet, the additional bubble doesn't get rendered. What might be going wrong here?
First of all, let me explain how setState works according to this link.
Whenever you change the internal state of a State object, make the change in a function that you pass to setState. Calling setState notifies the framework that the internal state of this object has changed in a way that might impact the user interface in this subtree, which causes the framework to schedule a build for this State object.
In your case, I would say it's just a convention. You can define a message object in your stateful class and update it inside the listener and your setstate like this:
setState(() { this.message = message; });
Caution:
Before any changes, you need to check this question
Usage of FutureBuilder with setState
Because using setstete with FutureBuilder can make an infinite loop in StatefulWidget which reduces performance and flexibility. Probably, you should change your approach to design your screen because FutureBuilder automatically updates the state each time tries to fetch data.
Update:
There is a better solution for your problem. Because you are using a stream to read messages, you can use StreamBuilder instead of the listener and FutureBuilder. It brings less implementation and more reliability for services that provide a stream of data. Every time the StreamBuilder receives a message from the Stream, it will display whatever you want with the snapshot of data.
You need to ensure that the ListView has a UniqueKey:
ScrollController _scrollController = ScrollController();
Stream<Widget> messageStream;
#override
void initState() {
super.initState();
// use RxDart to merge 2 streams into one stream
this.messageStream = Rx.merge<Message>([Utils.broadcaster.stream.stream.where((message) => this.widget.implicatedCnac.implicatedCid == message.implicatedCid && this.widget.peerNac.peerCid == message.peerCid), this.initStream()]).map((message) {
print("[MERGE] stream recv $message");
this.widget.bubbles.add(bubbleFrom(message));
// this line below to ensure that, as new items get added, the listview scrolls to the bottom
this._scrollController = _scrollController.hasClients ? ScrollController(initialScrollOffset: _scrollController.position.maxScrollExtent) : ScrollController();
return ListView(
key: UniqueKey(),
controller: this._scrollController,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
padding: EdgeInsets.only(top: 10, left: 10, right: 10, bottom: 50),
children: this.widget.bubbles,
);
});
}
Note: As per Erfan's directions, the context is best handled with a StreamBuilder. As such, I changed to a StreamBuilder, thus reducing the amount of code needed

BlocBuilder partially update ListView

Project
Hi, I'm trying to use a bloc pattern to create a list view that can be filtered by a TextField
Here is my code
bloc:
class HomeBloc extends Bloc<HomeEvent, HomeState> {
List<Book> displayList = [];
....
#override
HomeState get initialState => UnfilteredState();
#override
Stream<HomeState> mapEventToState(
HomeEvent event,
) async* {
....
//handle filter by input
if(event is FilterListByTextEvent) {
displayList = displayList.where((book){
return book.title.toLowerCase().contains(event.filterString.toLowerCase());
}).toList();
yield FilteredState();
}
}
}
view
class BookList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
final HomeBloc bloc = Provider.of<HomeBloc>(context);
print(bloc.displayList);
return ListView.builder(
shrinkWrap: true,
itemCount: bloc.displayList.length,
itemBuilder: (context, index) {
return Dismissible(
key: UniqueKey(),
background: Container(
color: selectedColor,
),
child: Container(
height: 120,
padding: const EdgeInsets.fromLTRB(20, 4, 20, 4),
child: BookCard(
book: bloc.displayList[index],
),
),
onDismissed: (DismissDirection direction) {
},
);
},
);
},
);
}
}
Problem
I've read some other discussion about bloc pattern and List view but the issue I'm facing here is different.
Every time my Filter event is called, bloc correctly generate a new displayList but, when BlocBuilder rebuild my UI, listview is not correctly updated.
The new filtered list is rendered but old results do not disappear. It seems like the new list is simply stacked on top of the old one.
In order to understand what was happening I tried printing the list that has to be rendered, inside the BlocBuilder before the build method is called.
The printed result was correct. In the console I see only the new filtered elements while in the UI I see both the new one and the old one, one below the other.
What am I missing here?
Thanks
Keep an intermediate event, eg. a ListInit for which you will display a CircularProgressIndicator. BlocBuilder will be stuck on previous state unless you update it over again.
So in your bloc, first yield the ListInit state and then perform filtering operations, and then yield your FilteredState.
Make new state for loading and yield before displayList.
if(event is FilterListByTextEvent) {
yield LoadFilterList();
displayList = displayList.where((book)
{
return
book.title.toLowerCase().contains(event.filterString.toLowerCase());
}).toList();
yield FilteredState();
}

Flutter: how to make a list that always scrolls to the bottom? [duplicate]

This question already has answers here:
Programmatically scrolling to the end of a ListView
(17 answers)
Closed 4 months ago.
I am trying to make a list that is basically a log screen. That said, I need the list to keep scrolling to bottom all the time.
How this can be done?
Set reverse: true on the ListView widget and reverse the children list.
ListView(
// Start scrolled to the bottom by default and stay there.
reverse: true,
children: widget.children.reversed.toList(growable: false),
),
If you have a very long list and reversing is expensive, you can reverse the index rather than the whole list using ListView.builder
ListView.builder(
reverse: true,
itemCount: items.length,
itemBuilder: (context, index) {
final reversedIndex = items.length - 1 - index;
final item = items[reversedIndex];
return MyWidget(item);
}
)
I got this from Günter's comment above. Thanks!
I found WidgetsBinding.instance.addPostFrameCallback extremely useful, especially in situations like this where you need some post build processing. In this case it solved the issue as follows:
Define the ScrollController
final ScrollController _sc = ScrollController();
In the ListView builder add the statement:
WidgetsBinding.instance.addPostFrameCallback((_) => {_sc.jumpTo(_sc.position.maxScrollExtent)});
Add the controller to ListView parameters:
controller: _sc,
I was also building a logger viewer. Works like a charm.
I could make this work by using a Timer but probably should have a better way to do this.
My workaround was:
Define a ScrollController() and attach it to the listView:
ListView.builder(
controller: _scrollController,
itemCount: _logLines.values.length,
itemBuilder: (context, index) => _getLogLine(index),
)
Override your page initState method and set a Timer inside it like:
#override
void initState() {
super.initState();
Timer.periodic(Duration(milliseconds: 100), (timer) {
if (mounted) {
_scrollToBottom();
} else {
timer.cancel();
}
});
}
Define a method _scrollToBottom() that calls:
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);

Keep scroll position in ListView when adding new item on top

I am using a ListView.builder to render a chat.
The scenario is the following: A user types in a new chat message, when that happens i am storing the new message in state and therefore the itemCount is increased by one.
If i am at the end of the list, I see the new item directly at the top, everything is OK.
However, if I am in the middle of the chat and an item is added, the view scrolls a little bit. How can I disable this behaviour? I guess it kinda makes sense because the scroll offset is still the same double in the ScrollControllers state, but if there is one more item, it looks like it scrolls...
Is there a good way to do this? I guess the manual approach would be to measure the height of the new item and manually set the correct ScrollController offset again.. but meeeh
In fact, you should not add new chat items to the list. And you have to cache them and then inject the cache into the list after the list has reached the top or bottom. I hope this solution works for you.
This is a solution for items with static/constant height (height: 100) added on top of the list.
When I detect that list length has changed I change offset position by hardcoded item height by calling _scrollController.jumpTo(newOffect).
It works well. The list stays at the current position when a new item is added to the top, with no jumps visible.
class FeedWidget extends StatefulWidget {
FeedWidget({
Key key,
}) : super(key: key);
#override
State<StatefulWidget> createState() => _FeedWidgetState();
}
class _FeedWidgetState extends State<FeedWidget> {
var _scrollController = ScrollController();
var _itemsLength = 0;
#override
Widget build(BuildContext context) {
return StreamBuilder<List<FeedItem>>(
stream: Provider.of<FeedRepository>(context).getAll(),
builder: (BuildContext context, AsyncSnapshot<List<FeedItem>> items) {
print('Loaded feed data $items');
switch (items.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
default:
final newItemsLength = items.data.length;
if (_itemsLength != 0 && _itemsLength != newItemsLength) {
final newItemsCount = newItemsLength - _itemsLength;
print('New items detected: $newItemsCount');
final newOffset = _scrollController.offset +
100 * newItemsCount; //item height is 100
print(
'Setting new offest ${_scrollController.offset} -> $newOffset');
_scrollController.jumpTo(newOffset);
}
print('Items length new = $newItemsLength old = $_itemsLength}');
_itemsLength = newItemsLength;
return Flexible(
child: ListView(
key: PageStorageKey('feed'), //keeps scroll position
controller: _scrollController,
padding: EdgeInsets.only(top: 8, bottom: 32),
children: items.data
.map((element) => FeedItemCard(item: element)) //item height is 100
.toList()));
}
},
);
}
}
You can use the flutter_scrollview_observer lib to implement your desired functionality without invasivity.
We only need three steps to implement the chat session page effect.
1、All chat data are displayed at the top of the listView when there is less than one screen of chat data.
2、When inserting a chat data
If the latest message is close to the bottom of the list, the listView will be pushed up.
Otherwise, the listview will be fixed to the current chat location.
Step 1: Initialize the necessary ListObserverController and ChatScrollObserver.
/// Initialize ListObserverController
observerController = ListObserverController(controller: scrollController)
..cacheJumpIndexOffset = false;
/// Initialize ChatScrollObserver
chatObserver = ChatScrollObserver(observerController)
..toRebuildScrollViewCallback = () {
// Here you can use other way to rebuild the specified listView instead of [setState]
setState(() {});
};
Step 2: Configure ListView as follows and wrap it with ListViewObserver.
Widget _buildListView() {
Widget resultWidget = ListView.builder(
physics: ChatObserverClampinScrollPhysics(observer: chatObserver),
shrinkWrap: chatObserver.isShrinkWrap,
reverse: true,
controller: scrollController,
...
);
resultWidget = ListViewObserver(
controller: observerController,
child: resultWidget,
);
return resultWidget;
}
Step 3: Call the [standby] method of ChatScrollObserver before inserting or removing chat data.
onPressed: () {
chatObserver.standby();
setState(() {
chatModels.insert(0, ChatDataHelper.createChatModel());
});
},
...
onRemove: () {
chatObserver.standby(isRemove: true);
setState(() {
chatModels.removeAt(index);
});
},