Flutter ListView lazy loading - flutter

How can I realize items lazy loading for endless listview? I want to load more items by network when user scroll to the end of listview.

You can listen to a ScrollController.
ScrollController has some useful information, such as the scrolloffset and a list of ScrollPosition.
In your case the interesting part is in controller.position which is the currently visible ScrollPosition. Which represents a segment of the scrollable.
ScrollPosition contains informations about it's position inside the scrollable. Such as extentBefore and extentAfter. Or it's size, with extentInside.
Considering this, you could trigger a server call based on extentAfter which represents the remaining scroll space available.
Here's an basic example using what I said.
class MyHome extends StatefulWidget {
#override
_MyHomeState createState() => _MyHomeState();
}
class _MyHomeState extends State<MyHome> {
ScrollController controller;
List<String> items = List.generate(100, (index) => 'Hello $index');
#override
void initState() {
super.initState();
controller = ScrollController()..addListener(_scrollListener);
}
#override
void dispose() {
controller.removeListener(_scrollListener);
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Scrollbar(
child: ListView.builder(
controller: controller,
itemBuilder: (context, index) {
return Text(items[index]);
},
itemCount: items.length,
),
),
);
}
void _scrollListener() {
print(controller.position.extentAfter);
if (controller.position.extentAfter < 500) {
setState(() {
items.addAll(List.generate(42, (index) => 'Inserted $index'));
});
}
}
}
You can clearly see that when reaching the end of the scroll, it scrollbar expends due to having loaded more items.

Thanks for Rémi Rousselet's approach, but it does not solve all the problem. Especially when the ListView has scrolled to the bottom, it still calls the scrollListener a couple of times. The improved approach is to combine Notification Listener with Remi's approach. Here is my solution:
bool _handleScrollNotification(ScrollNotification notification) {
if (notification is ScrollEndNotification) {
if (_controller.position.extentAfter == 0) {
loadMore();
}
}
return false;
}
#override
Widget build(BuildContext context) {
final Widget gridWithScrollNotification = NotificationListener<
ScrollNotification>(
onNotification: _handleScrollNotification,
child: GridView.count(
controller: _controller,
padding: EdgeInsets.all(4.0),
// Create a grid with 2 columns. If you change the scrollDirection to
// horizontal, this would produce 2 rows.
crossAxisCount: 2,
crossAxisSpacing: 2.0,
mainAxisSpacing: 2.0,
// Generate 100 Widgets that display their index in the List
children: _documents.map((doc) {
return GridPhotoItem(
doc: doc,
);
}).toList()));
return new Scaffold(
key: _scaffoldKey,
body: RefreshIndicator(
onRefresh: _handleRefresh, child: gridWithScrollNotification));
}

The solution use ScrollController and I saw comments mentioned about page.
I would like to share my finding about package incrementally_loading_listview
https://github.com/MaikuB/incrementally_loading_listview.
As packaged said : This could be used to load paginated data received from API requests.
Basically, when ListView build last item and that means user has scrolled down to the bottom.
Hope it can help someone who have similar questions.
For purpose of demo, I have changed example to let a page only include one item
and add an CircularProgressIndicator.
...
bool _loadingMore;
bool _hasMoreItems;
int _maxItems = 30;
int _numItemsPage = 1;
...
_hasMoreItems = items.length < _maxItems;
...
return IncrementallyLoadingListView(
hasMore: () => _hasMoreItems,
itemCount: () => items.length,
loadMore: () async {
// can shorten to "loadMore: _loadMoreItems" but this syntax is used to demonstrate that
// functions with parameters can also be invoked if needed
await _loadMoreItems();
},
onLoadMore: () {
setState(() {
_loadingMore = true;
});
},
onLoadMoreFinished: () {
setState(() {
_loadingMore = false;
});
},
loadMoreOffsetFromBottom: 0,
itemBuilder: (context, index) {
final item = items[index];
if ((_loadingMore ?? false) && index == items.length - 1) {
return Column(
children: <Widget>[
ItemCard(item: item),
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
Container(
width: 60.0,
height: 60.0,
color: Colors.grey,
),
Padding(
padding: const EdgeInsets.fromLTRB(
8.0, 0.0, 0.0, 0.0),
child: Container(
color: Colors.grey,
child: Text(
item.name,
style: TextStyle(
color: Colors.transparent),
),
),
)
],
),
Padding(
padding: const EdgeInsets.fromLTRB(
0.0, 8.0, 0.0, 0.0),
child: Container(
color: Colors.grey,
child: Text(
item.message,
style: TextStyle(
color: Colors.transparent),
),
),
)
],
),
),
),
Center(child: CircularProgressIndicator())
],
);
}
return ItemCard(item: item);
},
);
full example https://github.com/MaikuB/incrementally_loading_listview/blob/master/example/lib/main.dart
Package use ListView index = last item and loadMoreOffsetFromBottom to detect when to load more.
itemBuilder: (itemBuilderContext, index) {
if (!_loadingMore &&
index ==
widget.itemCount() -
widget.loadMoreOffsetFromBottom -
1 &&
widget.hasMore()) {
_loadingMore = true;
_loadingMoreSubject.add(true);
}

here is my solution for find end of listView
_scrollController.addListener(scrollListenerMilli);
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
getMoreData();
}
If you want to load more data when 1/2 or 3/4 of a list view size, then use this way.
if (_scrollController.position.pixels == (_scrollController.position.maxScrollExtent * .75)) {//.5
getMoreData();
}
Additional -> Make sure you called getMore API only one time when reaching to the bottom. You can solve this in many ways, This is one of the ways to solve this by boolean variable.
bool loadMore = false;
if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent && !loadMore) {
loadMore = true;
getMoreData().then(() => loadMore = false);
}

here is my approach which is inspired by answers above,
NotificationListener(onNotification: _onScrollNotification, child: GridView.builder())
bool _onScrollNotification(ScrollNotification notification) {
if (notification is ScrollEndNotification) {
final before = notification.metrics.extentBefore;
final max = notification.metrics.maxScrollExtent;
if (before == max) {
// load next page
// code here will be called only if scrolled to the very bottom
}
}
return false;
}

Use lazy_load_scrollview: 1.0.0 package that use same concept behind the scenes that panda world answered here. The package make it easier to implement.

The solutions posted don't solve the issue if you want to achieve lazy loading in up AND down direction. The scrolling would jump here, see this thread.
If you want to do lazy loading in up and down direction, the library bidirectional_listview could help.
Example (Source):
static const double kItemHeight = 30.0;
BidirectionalScrollController controller;
double oldScrollPosition = 0.0;
#override
void initState() {
super.initState();
for (int i = -10; i <= 10; i++) {
items[i] = "Item " + i.toString();
}
controller = new BidirectionalScrollController()
..addListener(_scrollListener);
}
#override
void dispose() {
controller.removeListener(_scrollListener);
super.dispose();
}
#override
void build() {
// ...
List<int> keys = items.keys.toList();
keys.sort();
return new BidirectionalListView.builder(
controller: controller,
physics: AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
return Container(
child: Text(items[index]),
height: kItemHeight,
},
itemCount: keys.first,
negativeItemCount: keys.last.abs(),
);
// ...
}
// Reload new items in up and down direction and update scroll boundaries
void _scrollListener() {
bool scrollingDown = oldScrollPosition < controller.position.pixels;
List<int> keys = items.keys.toList();
keys.sort();
int negativeItemCount = keys.first.abs();
int itemCount = keys.last;
double positiveReloadBorder = (itemCount * kItemHeight - 3 * kItemHeight);
double negativeReloadBorder =
(-(negativeItemCount * kItemHeight - 3 * kItemHeight));
// reload items
bool rebuildNecessary = false;
if (scrollingDown && controller.position.pixels > positiveReloadBorder)
{
for (int i = itemCount + 1; i <= itemCount + 20; i++) {
items[i] = "Item " + i.toString();
}
rebuildNecessary = true;
} else if (!scrollingDown &&
controller.position.pixels < negativeReloadBorder) {
for (int i = -negativeItemCount - 20; i < -negativeItemCount; i++) {
items[i] = "Item " + i.toString();
}
rebuildNecessary = true;
}
// set new scroll boundaries
try {
BidirectionalScrollPosition pos = controller.position;
pos.setMinMaxExtent(
-negativeItemCount * kItemHeight, itemCount * kItemHeight);
} catch (error) {
print(error.toString());
}
if (rebuildNecessary) {
setState(({});
}
oldScrollPosition = controller.position.pixels;
}
I hope that this helps a few people :-)

The accepted answer is correct but you can also do as follows,
Timer _timer;
Widget chatMessages() {
_timer = new Timer(const Duration(milliseconds: 300), () {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 300),
);
});
return StreamBuilder(
stream: chats,
builder: (context, snapshot) {
return snapshot.hasData
? ListView.builder(
// physics: NeverScrollableScrollPhysics(),
controller: _scrollController,
shrinkWrap: true,
reverse: false,
itemCount: snapshot.data.documents.length,
itemBuilder: (context, index) {
return MessageTile(
message: snapshot.data.documents[index].data["message"],
sendByMe: widget.sendByid ==
snapshot.data.documents[index].data["sendBy"],
);
})
: Container();
},
);
}

There is also this package, taking away the boilerplate: https://pub.dev/packages/lazy_load_scrollview

There is a much simpler solution than working with Scroll Controllers and Notifications. Just use the built in lazy loading feature of ListView Builders:
I suggest (and tested) to just wrap two FutureBuilders within each other and let them handle everything for you. Alternatively, the outer FutureBuilder can be replaced by loading the values in the initState.
Create FutureBuilder to retrieve the most compact version of your data. Best a url or an id of the data items to be displayed
Create a ListView.builder, which according to the flutter doc Flutter Lists Codebook, already takes care of the lazy loading part
The standard ListView constructor works well for small lists. To work with lists that contain a large number of items, it’s best to
use the ListView.builder constructor.
In contrast to the default ListView constructor, which requires creating all items at once, the ListView.builder() constructor
creates items as they’re scrolled onto the screen.
Within the ListView builder, add another FutureBuilder, which fetches the individual content.
You're done
Have a look at this example code.
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: <get a short list of ids to fetch from the web>,
builder: (BuildContext context, AsyncSnapshot<List<int>> snapshot) {
if (snapshot.hasData) {
return Expanded(
child: ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (BuildContext context, final int index) {
final int recordId = snapshot.data![index];
return FutureBuilder(
future: <get the record content from the web>,
builder: (BuildContext context,
AsyncSnapshot<Issue?> snapshot) {
if (snapshot.hasData) {
final Record? record = snapshot.data;
if (issue != null) {
return ListTile(
isThreeLine: true,
horizontalTitleGap: 0,
title: <build record widget>,
);
}
}
return ListTile(
isThreeLine: true,
horizontalTitleGap: 0,
title: const Text("Loading data..."));
});
}),
);
}
return const Text("Loading data...",
style: TextStyle(color: Colors.orange));
});
Let me know what you think. Performance was great when I've tried it, I'm wondering what you experienced with this. Sure, this needs some clean up, I know :D

This is an old question and the current answer is to use the ListView.builder method.
Same is true for the GridView.builder, please refer to the example below.
GridView.builder(
// ask GridView to cache and avoid redundant callings of Futures
cacheExtent: 100,
shrinkWrap: true,
itemCount: c.thumbnails.length,
// Define this as you like
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 0.0,
crossAxisSpacing: 0.0,
childAspectRatio: 1.0,
),
itemBuilder: (BuildContext context, int index) {
return FutureBuilder<Image>(builder: (ctx, snap) {
if (!snap.hasData) {
return const SizedBox.expand(); // show nothing
}
if (snap.hasError) {
return Text('An error occured ${snap.error}');
}
return snap.data!;
},
future: <YOUR THUMBNAIL FUTURE>,
);
}
);

You can handle it by knowing the current page and the last page
By using listview builder
itemBuilder: (context, index) {
if(list.length - 1 == index && currentPage! < lastPage!){
currentPage = currentPage! + 1;
/// Call your api here to update the list
return Progress();
}
return ///element widget here.
},

Related

ScrollController attached to multiple scroll views. flutter

I try to rebuild instagram stories by using GetX. I always receive this issue.
Can anyone help me solve this problem?
ScrollController attached to multiple scroll views.
'package:flutter/src/widgets/scroll_controller.dart':
Failed assertion: line 109 pos 12: '_positions.length == 1'
I try to rebuild instagram stories by using GetX. I always receive this issue.
Can anyone help me solve this problem?
ScrollController attached to multiple scroll views.
'package:flutter/src/widgets/scroll_controller.dart':
Failed assertion: line 109 pos 12: '_positions.length == 1'
import 'package:flamingo/Business_Logic/GetXControllers/Pages_Controllers/Stories_Controller.dart';
import 'package:flamingo/Data/DataProviders/StoriesList.dart';
import 'package:flamingo/Data/Models/All_Models.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class Stories extends StatelessWidget {
#override
Widget build(BuildContext context) {
return GetBuilder<StoriesController>(
init: StoriesController(),
builder: (storiesCtrl) {
return Scaffold(
backgroundColor: Colors.black,
body: GestureDetector(
onTapDown: (details) => storiesCtrl.onTapDown(
details, story[storiesCtrl.currentIndex.value]),
child: Stack(
children: <Widget>[
PageView.builder(
controller: storiesCtrl.pageC,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, i) {
final StoryModel s = story[i];
switch (s.media) {
case MediaType.image:
return Image.network(
s.image,
fit: BoxFit.cover,
);
}
return const SizedBox.shrink();
},
),
Positioned(
top: 15.0,
left: 10.0,
right: 10.0,
child: Row(
children: story
.asMap()
.map((i, e) {
return MapEntry(
i,
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 1.5),
child: LayoutBuilder(
builder: (context, constraints) {
return Stack(
children: <Widget>[
_buildContainer(
double.infinity,
i < storiesCtrl.currentIndex.value
? Colors.white
: Colors.white.withOpacity(0.5),
),
i == storiesCtrl.currentIndex.value
? AnimatedBuilder(
animation: storiesCtrl.animC,
builder: (context, child) {
return _buildContainer(
constraints.maxWidth *
storiesCtrl.animC.value,
Colors.white,
);
},
)
: const SizedBox.shrink(),
],
);
}),
),
));
})
.values
.toList(),
),
),
],
),
),
);
},
);
}
Container _buildContainer(double width, Color color) {
return Container(
height: 5.0,
width: width,
decoration: BoxDecoration(
color: color,
border: Border.all(
color: Colors.black26,
width: 0.8,
),
borderRadius: BorderRadius.circular(3.0),
),
);
}
}
import 'package:flamingo/Data/Models/StoryModel.dart';
import 'package:flamingo/Utils/AllUtils.dart';
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
import 'package:flamingo/Data/DataProviders/StoriesList.dart';
import 'package:get/get.dart';
class StoriesController extends GetxController
with SingleGetTickerProviderMixin {
List<StoryModel> stories;
AnimationController animC;
var pageC;
var currentIndex = 0.obs;
#override
void onInit() {
stories = story;
super.onInit();
pageC = PageController();
animC = AnimationController(vsync: this);
final StoryModel firstStory = stories.first;
loadStory(story: firstStory, animateToPage: false);
animC.addStatusListener((status) {
if (status == AnimationStatus.completed) {
animC.stop();
animC.reset();
if (currentIndex.value + 1 < stories.length) {
currentIndex.value += 1;
loadStory(story: stories[currentIndex.value]);
update();
} else {
currentIndex.value = 0;
loadStory(story: stories[currentIndex.value]);
update();
}
}
});
}
void onTapDown(TapDownDetails details, StoryModel s) {
final double dx = details.globalPosition.dx;
if (dx < GlobalSize.screenWidth / 3) {
if (currentIndex.value - 1 >= 0) {
currentIndex.value -= 1;
loadStory(story: story[currentIndex.value]);
update();
}
} else if (dx > 2 * GlobalSize.screenWidth / 3) {
if (currentIndex.value + 1 < story.length) {
currentIndex.value += 1;
loadStory(story: story[currentIndex.value]);
} else {
currentIndex.value = 0;
loadStory(story: story[currentIndex.value]);
}
}
}
void loadStory({StoryModel story, bool animateToPage = true}) {
animC.stop();
animC.reset();
switch (story.media) {
case MediaType.image:
animC.duration = story.duration;
animC.forward();
break;
}
if (animateToPage) {
pageC.animateToPage(
currentIndex.value,
duration: const Duration(microseconds: 1),
curve: Curves.easeInOut,
);
}
}
#override
void onClose() {
pageC.value.dispose();
animC.dispose();
super.onClose();
}
}
Okay so I had this same issue but for me it was not actually related to the code where I used my controller. Typically this issue shows up if you access the same controller from multiple pages. However, it also occurs if you have multiple instances of the same page on the router stack. In my case I was using Get.to() elsewhere in my application to navigate back to my home page which created another instance of my home page on the stack, hence the multiple controllers error.
So to solve it, I changed Get.to() to Get.offAll(). If you are using named routes, switch Get.toNamed() to Get.offAllNamed().
If you get errors after doing this that say your controllers are no longer found, in your bindings, set permanent: true when using Get.put() to instantiate your controller: Get.put(Home(), permanent: true);
Looking at your code, it does not look like the error is coming from using the same controller twice and may instead occur when you route back to this page from somewhere in your application. Took me forever to figure out but essentially Flutter makes another instance of the "shared" controller when you add the same page onto the stack so you have to remove the original page from the stack when navigating.
If anyone gets this error from navigating and is not using GetX for state management, use Navigation.pushNamedAndRemoveUntil() to navigate back to the page with the controller.

How to use ValueNotifier inside ValueListenableBuilder in flutter?

I'm using CustomScrollView and I need to search data inside 3 SliverList and 2 SliverGrid. All of them listen their data with ValueListenableBuilder and also I have an info box which shows if any data found or not. There are 5 ValueNotifier<bool> variables that helps me show/hide the info box but the problem is when the builders called again I need to call ValueNotifier.value inside this builders but it gives an error:
setState() or markNeedsBuild() called during build.
I know why this error happen, but I can't find any other solution, because I need to keep track of the status of the lists and grids (Some of their items can be deleted and meanwhile I need to check again if new list or grid has a data)
One of the SliverList:
Widget _taskWidgets() {
return SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 22, 16, 16),
sliver: ValueListenableBuilder(
valueListenable: _taskService.listenable,
builder: (_, __, child) {
var tasks = _taskService.search(_keyword.value);
_tasksEmpty.value = tasks.isEmpty; // this line gives error
if (!_showCompleted.value) {
tasks = tasks.where((e) => !e.completed).toList();
}
final listIDS = tasks.map((e) => e.listID).toSet();
return SliverList(
delegate: SliverChildBuilderDelegate((_, index) {
if (index % 2 == 0) {
final widget = HierarchyCard(tasks, index ~/ 2, true);
if (index == 0) {
return Stack(
clipBehavior: Clip.none,
children: [
widget,
_text(_appLocalizations.searchTasks),
],
);
}
return widget;
}
return const SizedBox(height: 30);
}, childCount: 2 * listIDS.length - 1),
);
}
),
);
}
This is because you are rebuilding a widget which is already being build
Try
WidgetsBinding.instance.addPostFrameCallback((_){
_tasksEmpty.value = tasks.isEmpty;
});

NotificationListener in TabController for infinite scroll

I have 4 tabs and I want to add lazy load or infinite scroll option in them. Earlier I tried with Scroll Controller but when it reaches to the end. Event firing more than once. Hence there are multiple Future http request to API.
I read some question on SO and found I might need to use NotificationListener. I am not sure I need to define it once or for all the tabs. I have no idea how to use NotificationListener.
class _Searchstate extends State<Search> with SingleTickerProviderStateMixin{
ScrollController _scrollController = new ScrollController();
final _scaffoldKey = GlobalKey<ScaffoldState>();
TabController _controller;
TabBarView(
controller: _controller,
children: [
// Text("TAB ONE CONTENT"),
RefreshIndicator(
onRefresh: refreshData,
child:Container(
decoration: BoxDecoration(
color: Colors.black87,
),
padding: EdgeInsets.only(top: 10, bottom: 5),
height: MediaQuery.of(context).size.height,
width: double.infinity,
child: ListView.builder(
controller: _scrollcontroller,
itemCount: (recommended) ? lists.length : searchlists.length,
itemBuilder: (BuildContext context, int index) {
return buildList1(context, index);
}),
),
),
//Text("TAB TWO CONTENT"),
RefreshIndicator(
onRefresh: refreshData1,
child:Container(
decoration: BoxDecoration(
color: Colors.black54,
),
padding: EdgeInsets.only(top: 10, bottom: 5),
height: MediaQuery.of(context).size.height,
width: double.infinity,
child: ListView.builder(
controller: _scrollcontroller,
itemCount: (nearme) ? lists1.length : searchlists1.length,
itemBuilder: (BuildContext context, int index) {
return buildList2(context, index);
}),
),
),
Below is the buildList where I am using the Listview.Builder to show the data which is coming from database. I tried to use ScrollController in this too like below.
Widget buildList1(BuildContext context, int index) {
_scrollController.addListener((){
print(_scrollController.position.pixels);
print(_scrollController.position.maxScrollExtent);
if(_scrollController.position.pixels == _scrollController.position.maxScrollExtent){
print(recommended);
if(recommended){
//getData();
print('getData()1;');
}
// getData();
}
});
I have added some relevant codes only in this question as full code is very long.
Edit
I tried using Notification listener and if I define it once around Scaffold then it is working at least I can see the scroll events but I have 4 tabs and I am not sure how can I implement it for all. Because it would be quite hard to set condition for all those 4 tabs.
#override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
child:Scaffold(
---- -- - - - - - More Codes -----
onNotification: (notificationInfo) {
if (notificationInfo is ScrollEndNotification) {
print("scroll");
print("detail:"+notificationInfo.dragDetails.toString());
/// your code
}
return true;
},
);
Same code I tried to put inside the tabs but it is not detecting the scroll event.
I managed to get it work but not sure this is the efficient way or not. May be it will others who may have faced similar issue.
Below code will help to identify the active tab.
void initState(){
_controller = TabController(vsync: this, length: 4);
currentTab = (_controller.index);
_controller.addListener(() {
if(_controller.indexIsChanging) {
print("tab is animating. from active (getting the index) to inactive(getting the index) ");
}else {
//tab is finished animating you get the current index
print(_controller.index);
currentTab = (_controller.index);
_handleTabSelection();
}
});
and below code i have added in NotificationListner.
onNotification: (notificationInfo) {
if (notificationInfo is ScrollEndNotification && notificationInfo.metrics.pixels == notificationInfo.metrics.maxScrollExtent) {
print("scroll");
if(currentTab == 0){
if(recommended == true && tab0 == 'Suggested' ){
// getData();
print('fire the 1 event');
}else{
print('Name()1;');
}
}
if(currentTab == 1){
if(nearme == true && tab1 == 'Near Me'){
//getData();
print('fire the 2nd event ');
}else{
}
}
if(currentTab == 2){
if(byRating == true && tab2 == 'By Rating'){
//getData();
print('fire the 3rd event');
}else{
}
}
if(currentTab == 3){
if(byprice == true && tab3 == 'Active'){
//getData();
print('fire the 4 event');
}else{
}
}
/// your code
}
return true;
},
);
Edit: As i have multiple taps so, above code is firing on left and right scroll too. To prevent that i have changed the code to as below.
if (notificationInfo is ScrollEndNotification && notificationInfo.metrics.axisDirection == AxisDirection.down && notificationInfo.metrics.pixels == notificationInfo.metrics.maxScrollExtent) {

`A dismissed Dismissible widget is still part of the tree` error occurred suddenly (FutureBuilder, ListView)

A dismissed Dismissible widget is still part of the tree.
Make sure to implement the onDismissed handler and to immediately remove the Dismissible widget from the application once that handler has fired.
I already know there are many questions about this issue in stackoverflow, and also read almost of them. But I have no idea why this happens because I have no setState issue, also dismissible key is correct. Could you find problem that I missed?? Someone said Dismissible should not be in ListView widget, but it has worked very well until yesterday even in the ListView. I tried key: UniqueKey() but it didn't work. Please let me know if you know any solutions. Thanks in advance.
Widget vocaBuilder() {
return FutureBuilder(
future: loadTodayVoca(),
builder: (context, snap) {
if (snap.data.length == 0 || snap.data.isEmpty) {
return Container();
} else {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: snap.data.length,
scrollDirection: Axis.vertical,
itemBuilder: (context, index) {
Voca voca = snap.data[index];
return GestureDetector(
onTap: () {
editPage(voca.id);
},
child: Dismissible(
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
padding: EdgeInsets.only(top: 23, right: 30)),
key: Key(snap.data[index].toString()),
...
onDismissed: (direction) {
setState((){
deleteVoca(voca.id);
snap.data.removeAt(index);
}); }));
Future<void> deleteVoca(String id) async {
DBHelper sd = DBHelper();
await sd.deleteVoca(id);
}
Future<List<Voca>> loadTodayVoca() async {
DBHelper sd = DBHelper();
var list = await sd.vocas();
return list
.where((list) =>
list.createTime ==
DateFormat('yyyy-MM-dd')
.format(DateTime(now.year, now.month, now.day)))
.toList();
}
Hai,I have the same problem,and I solve it like this:modify the value of the key to become a unique value。
For you like this:key:Key(snap.data[index].id.toString()), or if voca.id is a string,you should key:Key(snap.data[index].id)

How to compare two list data in flutter (dart)

I have a grid data on my page. I want to compare these data to another list like (id 1,2,3,4,5).
GridView.builder(
itemCount: course == null ? 0 : course.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: (MediaQuery.of(context).orientation == Orientation.portrait) ? 3 : 4),
itemBuilder: (BuildContext context, int index) {
return Card(
child:InkWell(
onTap: () {
setState(() {
courseid = course[index]['id'];
coursename=course[index]['name'];
});
addCourse(course[index]['about']);
},
child:Column(
children: <Widget>[
Text((coursedata['courseid'] == course[index]['id']) ? "Added" : ""),
IconButton(
icon: index.isEven ? Icon(Icons.school) : Icon(Icons.book),
iconSize:MediaQuery.of(context).orientation == Orientation.portrait ?30 : 30,
color: index.isOdd ? Colors.amber[800]: Colors.green[800],
onPressed: (){
getcourseData();
},
),
Text(course[index]['name']),
],
),
),
);
},
),
This is another list data gets from a firebase database.
getcourseData() async {
databaseReference.collection(user.email).getDocuments().then((querySnapshot) {
querySnapshot.documents.forEach((result) {
coursedata=result.data;
});
});
}
Both the above lists are compared using data ids.
coursedata['courseid'] == course[index]['id']) ? "Added" : ""
Kindly help on how to compare data in Gride view builder. Currently, only one data show "Added" though there are other data too not showing "Added" in view.
I have created a demo. Change as per your requirement.
import 'package:flutter/material.dart';
void main() =>
runApp(MaterialApp(home: GridViewDemo()));
class GridViewDemo extends StatefulWidget {
#override
_GridViewDemoState createState() => _GridViewDemoState();
}
class _GridViewDemoState extends State<GridViewDemo> {
// already added indices numbers
List<int> alreadyAddedIndices = [3,4,5,6,7];
var courseid = 0;
var coursename = "default";
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(("GridView Demo")),
),
body: SingleChildScrollView(
child: Column(
children: [
GridView.builder(
itemCount: 5,
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: (MediaQuery.of(context).orientation == Orientation.portrait) ? 3 : 4),
itemBuilder: (BuildContext context, int index) {
return Card(
child:InkWell(
onTap: () {
setState(() {
// change as per your code
courseid = index;
coursename=index.toString();
// add index in list if not available
// tapping again, remove index from list
alreadyAddedIndices.contains(index)?alreadyAddedIndices.remove(index):alreadyAddedIndices.add(index);
});
},
child:Column(
children: <Widget>[
Text((alreadyAddedIndices.contains(index)) ? "Added" : ""),
Icon(index.isEven ? Icons.school : Icons.book,
size:MediaQuery.of(context).orientation == Orientation.portrait ?30 : 30,
color: index.isOdd ? Colors.amber[800]: Colors.green[800],),
// course name text
const Text("course Name"),
],
),
),
);
},
),
],
),
),
);
}
}
Output:
Note:
This is a demo code. You can get all the added course ids in alreadyAddedIndices list. Change code as per the need.
The line,
Text((coursedata['courseid'] == course[index]['id']) ? "Added" : ""),
is only comparing one value as you are iterating over only one list. Try calling a method that returns a boolean value or a Text widget by iterating over both the lists. If the function returns false only then do you add to the list. Here is an example of a sudo code that return a Text widget:
_ComparingLists(int id) {
bool temp = false;
for (int i = 0; i < coursedata['courseid'].length; i++) {
if ((coursedata['courseid'][i] == id)) {
temp = true;
break;
} else {
temp = false;
}
}
// student is already enrolled
if (temp == true) {
return Text("Student is enrolled ...");
}
// student is not enrolled
else {
// do your operations like adding to the list here ....
return Text("No match");
}
}
You can call the method by :
_ComparingLists(course[index]['id'])
Hope that helps :)