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 :)
Related
First of all I don't know what i am facing, but I'll do my best to explain the situation.
I'm trying to build chat app and i have two sections on same page. These two different sections are rendering inside same ListView. Only thing that changing is the data which i am using to feed the ListView. I need to get the status of user in real time so i am putting tagged controllers for each tile which is rendering inside list view. Here comes the problem. The tiles rendered at the same index are not showing the true states of themselves until some state changes on that tile for example position of any Stack item.
Here is the code.
In this part I'm rendering ListView
ListView.builder(
itemCount: chatController.currentChats!.length,
itemBuilder: (context, index) {
return GetBuilder<UserOnlineController>(
global: false,
init: Get.find<UserOnlineController>(tag: chatController.currentUserID == chatController.currentChats![index].user1 ? chatController.currentChats![index].user2 : chatController.currentChats![index].user1),
builder: (userController) {
return Stack(
children: [
Positioned(
child: Container(
color: Colors.black,
width: Get.width,
height: Dimensions.h100,
child: Center(
child: Text(
"${userController.user!.name!}",
style: TextStyle(
color: Colors.white
),
),
),
)
)
],
);
}
);
}),
This is the part that I'm putting controllers and listening chats in real time.
void listenChats() async {
var chatController = Get.find<ChatController>();
var messagesController = Get.find<MessagesController>();
String userID = Get.find<SharedPreferenceService>().getUserID();
var currentUserDoc = (await firestoreService.getCollection('users').where('userID', isEqualTo: userID).get()).docs[0];
Stream<DocumentSnapshot> userStream = firestoreService.getCollection('users').doc(currentUserDoc.id).snapshots();
Stream<QuerySnapshot> chatStream = firestoreService.getCollection('chats').snapshots();
await for(var user in userStream){
var userObject = UserModel.fromJson(user.data() as Map<String,dynamic>);
await for(var chats in chatStream) {
List<Chat> activeChats = [];
List<Chat> unActiveChats = [];
List<Chat> newMatches = [];
List<Chat> allChats = [];
var filteredChats = chats.docs.where((chat) => userObject.chat!.active_chats!.contains(chat['chatID'])).toList();
filteredChats.forEach((chatDoc) {
var currentChat = Chat.fromJson(chatDoc.data() as Map<String,dynamic>);
if(currentChat.user1 == userID){
Get.put(
UserOnlineController(firestoreService: firestoreService, userID: currentChat.user2!),
tag: currentChat.user2!,
);
}
else{
Get.put(
UserOnlineController(firestoreService: firestoreService, userID: currentChat.user1!),
tag: currentChat.user1!
);
}
allChats.add(currentChat);
if(currentChat.isActive!){
if(currentChat.isStarted!){
activeChats.add(currentChat);
}
else{
newMatches.add(currentChat);
}
}
else{
unActiveChats.add(currentChat);
}
});
messagesController.generatePositions(activeChats.length, true);
messagesController.generatePositions(unActiveChats.length, false);
chatController.setAllChats(allChats);
chatController.setCurrentChats();
chatController.setChats(activeChats, unActiveChats, newMatches);
}
}
}
And this is the part that I'm using to control the UI state
void setAllChats(List<Chat> allChats) {
_allChats = allChats;
}
void setCurrentChats() {
_currentChats = _allChats!.where((chat) => chat.isActive! == isActiveMessages).toList();
update();
}
void setIsActiveMessages(bool state){
_isActiveMessages = state;
_currentChats = _allChats!.where((chat) => chat.isActive! == state).toList();
update();
}
In the above pictures all of these users are different but only true one is the third one at second screen shot.
Hello again this question basically explains all the details.
Multiple Instance with GetX tag not working in flutter
Basically you need to add key parameter.
GetBuilder<UserChatController>(
key: Key(currentUserControllerTag),
tag: currentUserControllerTag,
global: false,
init: Get.find<UserChatController>(tag: currentUserControllerTag),
builder: (controller) {
return controller.user != null ? Container(
width: Get.width,
height: Dimensions.h100,
child: Stack(
children: [
Positioned(
left: 0,
right: 0,
child: Container(
height: Dimensions.h100,
width: double.maxFinite,
color: Colors.black,
child:Center(
child: Text(
controller.user != null ? controller.user!.name! : "",
style: TextStyle(
color: Colors.white
),
),
)
))
],
),
) : Container();
},
)
I have a chatbot screen that I built with a listview and there is a strange behavior that I don't understand the origin of.
From time to time, some of the elements overlap each other, then if I scroll or change screen and come back they are back to normal. I can't seem to find the problem...
import 'package:async_redux/async_redux.dart';
import 'package:flutter/material.dart';
import '../../../app_theme.dart';
import '../../../data/models/models.dart';
import '../../../helpers.dart';
import 'action_message.dart';
import 'conversation_bar.dart';
import 'image_message.dart';
import 'text_message.dart';
import 'typing_indicator.dart';
class ConversationFlow extends StatelessWidget {
// Max number of messages to show
final int maxMessages = 50;
Widget _buildConversationMessage(BuildContext context, message, next) {
// Detects which message type to build as child
Widget child = Container();
if (message.text.isEmpty) {
return child;
}
if (message.attachment == null) {
child = TextMessage(message: message);
} else if (message.attachment.type == 'image') {
child = ImageMessage(message: message);
}
// General structure of each message
var topSpacing = App.size(15, context);
// Between same speaker messages
if (next != null && next.speaker == message.speaker) {
topSpacing = App.size(1.5, context);
}
// Last one from conversation
if (next == null) {
topSpacing = App.size(10, context);
}
return Row(
mainAxisAlignment: (message.speaker == 'coach') ? MainAxisAlignment.start : MainAxisAlignment.end,
children: [
Container(
constraints: BoxConstraints(
maxWidth: App.isLandscape(context)
? MediaQuery.of(context).size.width * 0.42
: MediaQuery.of(context).size.width * 0.7,
),
padding: EdgeInsets.symmetric(
vertical: App.size(8, context),
horizontal: App.size(10, context),
),
margin: EdgeInsets.only(
left: (message.speaker == 'coach') ? App.margin(context) : 0,
top: topSpacing,
right: (message.speaker == 'coach') ? 0 : App.margin(context),
),
decoration: BoxDecoration(
color: (message.speaker == 'coach') ? AppTheme.colorGrey[100] : AppTheme.colorAccent,
borderRadius: BorderRadius.circular(App.radius()),
),
child: child,
),
(message.speaker == 'coach' && message.action != null) ? ActionMessage(action: message.action) : Container(),
],
);
}
#override
Widget build(BuildContext context) {
return StoreConnector(
converter: (Store<AppState> store) => store.state.bot,
builder: (BuildContext context, Bot bot) {
int itemsCount = bot.conversation.isNotEmpty
? bot.conversation.length > maxMessages
? maxMessages
: bot.conversation.length + 2
: 1;
return Align(
alignment: Alignment.bottomCenter,
child: ListView.builder(
shrinkWrap: true,
reverse: true,
scrollDirection: Axis.vertical,
physics: BouncingScrollPhysics(),
itemCount: itemsCount,
itemBuilder: (BuildContext context, int index) {
if (index == 0) {
return ConversationBar();
}
// Typing indicator Row
if (index == 1) {
return bot.typing ? TypingIndicator() : Container();
}
// Correct Typing index shift
index = index - 2;
// Reduces opacity as conversation fades away like memories
double opacity = 1.0;
(index > maxMessages - 10) ? opacity = 1.0 - ((index - maxMessages + 10) * 0.1) : 0.0;
if (opacity < 0.0) {
opacity = 0.0;
} else if (opacity > 1.0) {
opacity = 1.0;
}
if (index < maxMessages) {
// Conversation bubbles
return Opacity(
opacity: opacity,
child: _buildConversationMessage(
context,
bot.conversation[index],
bot.conversation.asMap().containsKey(index + 1) ? bot.conversation[index + 1] : null,
),
);
}
return Container();
},
),
);
},
);
}
}
Any idea of what could cause this?
apply itemExtent: value into Listview.builder
Use readmore package if you have different size and wrap your text into ReadMoreText
ReadMoreText(
'your message',
trimLines: 2,
trimMode: TrimMode.Line,
trimCollapsedText: 'Read more',
trimExpandedText: 'Read less',
),
use trimLines for line of text you want to show
I'm trying to build a sort function in order to sort JSON data.
For this, I have a button that opens a "showModalBottomSheet".
Within it I can choose the following data of the school class numbers.
So in my data I have 6 classrooms when loading in my constructor.
My filter is represented by buttons which are active or not if the filter contains the number of the classroom. My code works pretty much, my problem is that when I select a filter button in order to activate or not the filter, the button is deleted instead of staying but changing color
My notifier :
class TablesNotifier with ChangeNotifier {
// Services
// ---------------------------------------------------------------------------
final jsonSelectorService = locator<JsonSelectorService>();
// Variables
// ---------------------------------------------------------------------------
//all data from my classerooms in JSON
List<ClassroomModel> classrooms;
// Data that I will display and reconstruct based on my filter parameters
List<ClassroomModel> classroomsFiltered;
List<int> numberOfClassrooms = List();
// Model which will store the parameters of my filters and as a function I will load the data to display
FilterClassroomsModel filterClassroomsModel = FilterClassroomsModel();
// Constructor
// ---------------------------------------------------------------------------
TablesNotifier(){
_initialise();
}
// Initialisation
// ---------------------------------------------------------------------------
Future _initialise() async{
classrooms = await jsonSelectorService.classrooms('data');
classroomsFiltered = classrooms ;
// I install the number of existing classrooms
// Here the result is [1,2,3,4,5,6]
classrooms.forEach((element) {
if(!numberOfClassrooms.contains(element.type)){
numberOfClassrooms.add(element.type);
}
});
// I install the number of classrooms activated by default in my filter
// As I decide to display all my classrooms by default
// My filter on the classrooms must contain all the loaded classrooms
filterClassroomsModel.classrooms = numberOfClassrooms;
notifyListeners();
}
// Functions public
// ---------------------------------------------------------------------------
void saveClassroomsSelected(int index)
{
// Here my classroom model also contains the numbers of the classrooms that I want to filter
if(filterClassroomsModel.classrooms.contains(index)){
filterClassroomsModel.classrooms.remove(index);
}else{
filterClassroomsModel.classrooms.add(index);
}
notifyListeners();
}
}
I have identified that in my function initialize () if I change my code by this it works :
filterClassroomsModel.classrooms= numberOfClassrooms; // this
filterClassroomsModel.classrooms= [1,2,3,4,5,6]; // By this
I am losing the dynamic side of my classroom calculation and that does not suit me. But I don't understand this behavior.
My view :
class TableScreen extends StatelessWidget {
final String title;
TableScreen({Key key, #required this.title}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: MenuDrawerComponent.builder(context),
appBar: AppBar(
backgroundColor: AppColors.backgroundDark,
elevation: 0,
centerTitle: true,
title: Text(title),
),
floatingActionButton: FloatingActionButton.extended(
icon: Icon(Icons.sort),
label: Text('Filter'),
onPressed: () async{
slideSheet(context);
},
backgroundColor: AppColors.contrastPrimary,
),
body: _buildBody(context),
);
}
Widget _buildBody(BuildContext context)
{
var _tableProvider = Provider.of<TablesNotifier>(context);
if(_tableProvider.chargesFiltered == null){
return Center(
child: CircularProgressIndicator(
backgroundColor: AppColors.colorShadowLight,
),
);
}else{
return Column(
children: <Widget>[
Expanded(
child: Container(
padding: EdgeInsets.only(top: 10, right : 20, left : 20),
child: ListView.builder(
itemCount: _tableProvider.classroomsFiltered.length,
itemBuilder: (context, index){
return Container(
child: Column(
children: [
Row(
// Some classrooms data
),
],
),
);
},
),
)
),
],
);
}
}
void slideSheet(BuildContext context) {
var _tableProvider = Provider.of<TablesNotifier>(context, listen:false);
showModalBottomSheet(
context: context,
isScrollControlled: true,
isDismissible: true,
builder: (context) {
return Wrap(
children: [
Container(
color: Color(0xFF737373),
child: Container(
child: Column(
children: <Widget>[
// Some filters ...
// Here I want to rebuild the list of button for show the changes
ChangeNotifierProvider.value(
value: _tableProvider,
child: Consumer<TablesNotifier>(
builder: (context, model, child){
return _listOfClassrooms(context);
}
),
),
],
),
),
),
]
);
});
}
Widget _listOfClassrooms(BuildContext context){
var _tableProvider = Provider.of<TablesNotifier>(context);
List<Widget> list = List<Widget>();
var listClassrooms = _tableProvider.numberOfClassrooms;
var filterClassrooms = _tableProvider.filterClassroomsModel.classrooms;
for (var i = 0; i < listClassrooms.length; i++) {
int selectIndex = 0;
if(filterClassrooms.contains(listClassrooms[i])){
selectIndex = listClassrooms[i];
}
list.add(
RadioComponent(
text: "${listClassrooms[i]}",
index: listClassrooms[i],
width: (MediaQuery.of(context).size.width - 56) /3,
selectedIndex: selectIndex,
onPressed: _tableProvider.saveChargesSelected,
),
);
}
return Wrap(
spacing: 8.0, // gap between adjacent chips
runSpacing: 8.0, // gap between lines
children: list
);
}
}
My FilterClassroomsModel :
class FilterClassroomsModel {
int order;
int sort;
List<int> classrooms;
FilterClassroomsModel ({
this.order = 0,
this.sort = 0,
this.classrooms = const[],
});
#override
String toString() {
return '{ '
'${this.order}, '
'${this.sort}, '
'${this.classrooms}, '
'}';
}
}
EDIT : Resolved topic. Thanks to Javachipper.
In the notifier I replace that :
filterClassroomsModel.classrooms = numberOfClassrooms;
By that :
filter.classrooms = List<int>();
filter.classrooms.addAll(numberOfClassrooms);
change this:
filterClassroomsModel.classrooms = numberOfClassrooms;
to:
filterClassroomsModel.classrooms.addAll(numberOfClassrooms);
Update (you can also do it like this):
filterClassroomsModel.classrooms= new List<int>();
filterClassroomsModel.classrooms.addAll(numberOfClassrooms);
i am working on a project where i am using PageView.builder which basically scroll vertically. i am loading video list using PageView.builder where user can see the videos (one video at a time) i want to add load more functionality. currently i am fetching 10 videos at a time from the server and loading it in PageView.builder, when user reaches last video i.e 10th video i want to load more 10 videos from server. moreover i want to refresh the list by pulling it down if it's first video. below is the code which i am using. Does anyone know how to achieve the desired
function? thanks in advance.
Stack createListView(BuildContext context, AsyncSnapshot snapshot)
{
videoDataList =snapshot.data;
_videoListController.init(
_pageController,
videoDataList,
);
tkController.addListener(
() {
if (tkController.value == TikTokPagePositon.middle) {
_videoListController.currentPlayer.start();
} else {
_videoListController.currentPlayer.pause();
}
},
);
return Stack(
// index: currentPage == null ? 0 : 1,
children: <Widget>[
RefreshIndicator(
child: PageView.builder(
key: Key('home'),
controller: _pageController,
pageSnapping: true,
physics: ClampingScrollPhysics(),
scrollDirection: Axis.vertical,
itemCount: _videoListController.videoCount,
itemBuilder: (context, i) {
// Put together a video component
var data = videoDataList[i];
bool isF = SafeMap(favoriteMap)[i].boolean ?? false;
var player = _videoListController.playerOfIndex(i);
//Right button bar
Widget buttons = TikTokButtonColumn(
profilePic: Glob.ITEM_BASE_URL+data.user_profile,
isFavorite: isF,
videoModel: data,
onAvatar: () {
tkController.animateToPage(TikTokPagePositon.right);
},
onFavorite: () {
setState(() {
favoriteMap[i] = !isF;
});
// showAboutDialog(context: context);
},
onComment: () {
_getCommentData(data.post_id);
},
onShare: () {},
);
// video
Widget currentVideo = Center(
child: FijkView(
player: player,
color: Colors.black,
panelBuilder: (_, __, ___, ____, _____) => Container(),
),
);
currentVideo = TikTokVideoPage(
hidePauseIcon: player.state != FijkState.paused,
aspectRatio: 9 / 16.0,
key: Key(Glob.ITEM_BASE_URL+data.post_video + '$i'),
tag: Glob.ITEM_BASE_URL+data.post_video,
bottomPadding: hasBottomPadding ? 16.0 : 16.0,
userInfoWidget: VideoUserInfo(
viewers: data.post_view_count,
desc: data.post_description,
userName: data.user_name,
fullname: data.full_name,
bottomPadding: hasBottomPadding ? 16.0 : 50.0,
// onGoodGift: () => showDialog(
// context: context,
// builder: (_) => FreeGiftDialog(),
// ),
),
onSingleTap: () async {
if (player.state == FijkState.started) {
await player.pause();
} else {
await player.start();
}
setState(() {});
},
onAddFavorite: () {
setState(() {
favoriteMap[i] = true;
});
},
rightButtonColumn: buttons,
video: currentVideo,
);
return currentVideo;
},
),
onRefresh: _getData,
),
Opacity(
opacity: 1,
child: currentPage ?? Container(),
),
// Center(
// child: Text(_currentIndex.toString()),
// )
],
);
}
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.
},