ScrollController attached to multiple scroll views. flutter - 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.

Related

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) {

Flutter - How to reload the entire page(reload the widget with its initstate again) on the call of an action button?

I have a Stateful Widget containing a custom tab view.
At the initialisation of the widget, category data(All, Science, Trending, Health & Fitness here) is fetched from the firestore and accordingly corresponding widgets are added to the tab view.
To add a new category, it can be selected from the last tab('+' here). Inside the corresponding widget, one can select the categories of his interest.
On the tap of "Add Category" button I am adding the selected category to firestore but after that I want to reload the widget and load its initstate(because the category data is fetched in initstate).
Can somone please explain that how can i achieve the same here?
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_advanced_networkimage/provider.dart';
import 'package:flutter_advanced_networkimage/transition.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:journey_togather/pages/home.dart';
import 'package:journey_togather/pages/invite_friends.dart';
import 'package:journey_togather/pages/journey/create_journey.dart';
import 'package:journey_togather/pages/journey/create_paid_journey.dart';
import 'package:journey_togather/pages/journey/journey_home.dart';
import 'package:journey_togather/pages/locator.dart';
import 'package:journey_togather/services/analytics.dart';
import 'package:journey_togather/settings/appbuilder.dart';
import 'package:journey_togather/settings/sizeconfig.dart';
import 'package:journey_togather/settings/textstyle.dart';
import 'package:journey_togather/widgets/all_user_journey.dart';
import 'package:journey_togather/widgets/pillButton.dart';
import 'package:journey_togather/widgets/progress.dart';
import 'package:uuid/uuid.dart';
class ExploreFeed extends StatefulWidget {
#override
_ExploreFeedState createState() => _ExploreFeedState();
}
class _ExploreFeedState extends State<ExploreFeed>
with TickerProviderStateMixin {
bool isLoading = false;
final AnalyticsService _analyticsService = locator<AnalyticsService>();
TabController tabController;
final _scaffoldKey = GlobalKey<ScaffoldState>();
// this will control the button clicks and tab changing
TabController _controller;
// this will control the animation when a button changes from an off state to an on state
AnimationController _animationControllerOn;
// this will control the animation when a button changes from an on state to an off state
AnimationController _animationControllerOff;
// this will give the background color values of a button when it changes to an on state
Animation _colorTweenBackgroundOn;
Animation _colorTweenBackgroundOff;
// this will give the foreground color values of a button when it changes to an on state
Animation _colorTweenForegroundOn;
Animation _colorTweenForegroundOff;
// when swiping, the _controller.index value only changes after the animation, therefore, we need this to trigger the animations and save the current index
int _currentIndex = 0;
// saves the previous active tab
int _prevControllerIndex = 0;
// saves the value of the tab animation. For example, if one is between the 1st and the 2nd tab, this value will be 0.5
double _aniValue = 0.0;
// saves the previous value of the tab animation. It's used to figure the direction of the animation
double _prevAniValue = 0.0;
List<dynamic> _userInterestCategory = ['All'];
List<dynamic> _userInterestCategoryId = ['All'];
List<Widget> _interestFeedBuild = [];
//saves the newly added interest data of a user from add category tab
List _interestDataId = [];
List _interestDataName = [];
// store the pill buttons for categories in add category page
List<Widget> buttons;
Color _foregroundOn = Colors.white;
Color _foregroundOff = Colors.grey;
// active button's background color
Color _backgroundOn = Colors.green;
// Color _backgroundOff = Colors.black;
Color _backgroundOff = Colors.grey.withOpacity(0.1);
// scroll controller for the TabBar
ScrollController _tabScrollController = new ScrollController();
// this will save the keys for each Tab in the Tab Bar, so we can retrieve their position and size for the scroll controller
List _keys = [];
bool _buttonTap = false;
// ScrollController _scrollController = ScrollController();
#override
void initState() {
setState(() {
isLoading = true;
});
_initialiseData();
// WidgetsBinding.instance.addPostFrameCallback(_initialiseData());
super.initState();
}
_initialiseData()async{
_getInterestData().whenComplete(() {
for (int index = 0; index < _userInterestCategory.length; index++) {
// create a GlobalKey for each Tab
_keys.add(new GlobalKey());
}
// this creates the controller with 6 tabs (in our case)
_controller =
TabController(vsync: this, length: _userInterestCategory.length);
// this will execute the function every time there's a swipe animation
_controller.animation.addListener(_handleTabAnimation);
// this will execute the function every time the _controller.index value changes
_controller.addListener(_handleTabChange);
_animationControllerOff = AnimationController(
vsync: this, duration: Duration(milliseconds: 75));
// so the inactive buttons start in their "final" state (color)
_animationControllerOff.value = 1.0;
_colorTweenBackgroundOff =
ColorTween(begin: _backgroundOn, end: _backgroundOff)
.animate(_animationControllerOff);
_colorTweenForegroundOff =
ColorTween(begin: _foregroundOn, end: _foregroundOff)
.animate(_animationControllerOff);
_animationControllerOn = AnimationController(
vsync: this, duration: Duration(milliseconds: 150));
// so the inactive buttons start in their "final" state (color)
_animationControllerOn.value = 1.0;
_colorTweenBackgroundOn =
ColorTween(begin: _backgroundOff, end: _backgroundOn)
.animate(_animationControllerOn);
_colorTweenForegroundOn =
ColorTween(begin: _foregroundOff, end: _foregroundOn)
.animate(_animationControllerOn);
_userInterestCategoryId.forEach((element) {
if (element != '+') {
_interestFeedBuild.add(
BuildInterestFeed(
categoryId: element,
),
);
} else {
buildButton();
_interestFeedBuild.add(
_buildAddInterest(),
);
}
});
setState(() {
isLoading = false;
});
});
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
_getInterestData() async {
DocumentSnapshot _doc =
await userInterestsRef.document(currentUser.id).get();
List _interestsFetched = _doc.data['categories'];
_interestsFetched = _interestsFetched.toSet().toList();
Map _userInterestData = {};
for (var val in _interestsFetched) {
Map _filteredData =
globalInterest.firstWhere((element) => element['categoryId'] == val);
_userInterestData[_filteredData['categoryId']] =
(_filteredData['categoryName']);
}
setState(() {
_userInterestCategory =
_userInterestCategory + _userInterestData.values.toList() + ['+'];
_userInterestCategoryId =
_userInterestCategoryId + _userInterestData.keys.toList() + ['+'];
});
}
//tab bar functions
// runs during the switching tabs animation
_handleTabAnimation() {
// gets the value of the animation. For example, if one is between the 1st and the 2nd tab, this value will be 0.5
_aniValue = _controller.animation.value;
// if the button wasn't pressed, which means the user is swiping, and the amount swipped is less than 1 (this means that we're swiping through neighbor Tab Views)
if (!_buttonTap && ((_aniValue - _prevAniValue).abs() < 1)) {
// set the current tab index
_setCurrentIndex(_aniValue.round());
}
// save the previous Animation Value
_prevAniValue = _aniValue;
}
// runs when the displayed tab changes
_handleTabChange() {
// if a button was tapped, change the current index
if (_buttonTap) _setCurrentIndex(_controller.index);
// this resets the button tap
if ((_controller.index == _prevControllerIndex) ||
(_controller.index == _aniValue.round())) _buttonTap = false;
// save the previous controller index
_prevControllerIndex = _controller.index;
}
_setCurrentIndex(int index) {
// if we're actually changing the index
if (index != _currentIndex) {
setState(() {
// change the index
_currentIndex = index;
});
// trigger the button animation
_triggerAnimation();
// scroll the TabBar to the correct position (if we have a scrollable bar)
_scrollTo(index);
}
}
_triggerAnimation() {
// reset the animations so they're ready to go
_animationControllerOn.reset();
_animationControllerOff.reset();
// run the animations!
_animationControllerOn.forward();
_animationControllerOff.forward();
}
_scrollTo(int index) {
// get the screen width. This is used to check if we have an element off screen
double screenWidth = MediaQuery.of(context).size.width;
// get the button we want to scroll to
RenderBox renderBox = _keys[index].currentContext.findRenderObject();
// get its size
double size = renderBox.size.width;
// and position
double position = renderBox.localToGlobal(Offset.zero).dx;
// this is how much the button is away from the center of the screen and how much we must scroll to get it into place
double offset = (position + size / 2) - screenWidth / 2;
// if the button is to the left of the middle
if (offset < 0) {
// get the first button
renderBox = index-1 < 0 ? _keys[0].currentContext.findRenderObject() : _keys[index - 1].currentContext.findRenderObject();
// get the position of the first button of the TabBar
position = renderBox.localToGlobal(Offset.zero).dx;
// if the offset pulls the first button away from the left side, we limit that movement so the first button is stuck to the left side
if (position > offset) offset = position;
} else {
// if the button is to the right of the middle
// get the last button
renderBox = index+1 == _userInterestCategory.length ? _keys[_userInterestCategory.length-1]
.currentContext
.findRenderObject(): _keys[index + 1]
.currentContext
.findRenderObject();
// get its position
position = renderBox.localToGlobal(Offset.zero).dx;
// and size
size = renderBox.size.width;
// if the last button doesn't reach the right side, use it's right side as the limit of the screen for the TabBar
if (position + size < screenWidth) screenWidth = position + size;
// if the offset pulls the last button away from the right side limit, we reduce that movement so the last button is stuck to the right side limit
if (position + size - offset < screenWidth) {
offset = position + size - screenWidth;
}
}
// scroll the calculated amount
_tabScrollController.animateTo(offset + _tabScrollController.offset,
duration: new Duration(milliseconds: 150), curve: Curves.easeInOut);
}
_getBackgroundColor(int index) {
if (index == _currentIndex) {
// if it's active button
return _colorTweenBackgroundOn.value;
} else if (index == _prevControllerIndex) {
// if it's the previous active button
return _colorTweenBackgroundOff.value;
} else {
// if the button is inactive
return _backgroundOff;
}
}
_getForegroundColor(int index) {
// the same as the above
if (index == _currentIndex) {
return _colorTweenForegroundOn.value;
} else if (index == _prevControllerIndex) {
return _colorTweenForegroundOff.value;
} else {
return _foregroundOff;
}
}
callback(Map data) {
if (data['method'] == 'delete') {
setState(() {
_interestDataId.removeWhere((element) => element == data['id']);
_interestDataName.removeWhere((element) => element == data['name']);
});
} else {
setState(() {
_interestDataId.add(data['id']);
_interestDataName.add(data['name']);
});
}
}
addInterest(context) async {
if(_interestDataId.length > 0 && _interestDataName.length > 0) {
await userInterestsRef
.document(currentUser.id)
.updateData({'categories': FieldValue.arrayUnion(_interestDataId)});
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) {
return Home();
},
),
);
}
else{
SnackBar snackbar = SnackBar(
content: Text("Please select atleast 1 interest before submitting"),
);
_scaffoldKey.currentState.showSnackBar(snackbar);
}
}
buildButton() {
List<Widget> _buttons = [];
globalInterest.forEach((element) {
if (_userInterestCategoryId.contains(element['categoryId'])) {
} else {
_buttons.add(PillButton(
interestId: element['categoryId'],
interestTitle: element['categoryName'],
callback: callback,
));
}
});
setState(() {
buttons = _buttons;
});
}
Widget _buildAddInterest() {
return Scaffold(
bottomNavigationBar: buttons.length != 0 ?
Container(
margin: EdgeInsets.only(bottom: 16.0),
padding: EdgeInsets.symmetric(horizontal: 16.0),
height: 40,
child: FlatButton(
onPressed: () => addInterest(context),
color: Colors.black,
child: Text(
'Add category',
style: Theme.of(context)
.textTheme
.subtitle2
.copyWith(color: Theme.of(context).primaryColor),
),
),
) : Container(),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: buttons.length != 0
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Wrap(
spacing: 12.0,
children: buttons,
),
],
)
: Center(
child: Text(
'Voila! You\'ve selected all the available interests.',
style: Theme.of(context).textTheme.subhead.copyWith(
color:
Theme.of(context).primaryColorDark.withOpacity(0.40),),
// Colors.black,)
),
),
),
);
}
#override
Widget build(BuildContext context) {
return isLoading
? circularProgress(context)
: AppBuilder(
builder: (context){
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
automaticallyImplyLeading: false,
backgroundColor: Theme.of(context).primaryColor,
elevation: 0,
title: IconButton(
iconSize: SizeConfig.safeBlockHorizontal * 21,
icon: Image.asset(
Theme.of(context).brightness == Brightness.light
? 'assets/images/logo.png'
: 'assets/images/journey_exploreFeed_dark.png',
),
),
titleSpacing: 8.0,
actions: <Widget>[
IconButton(
padding: EdgeInsets.only(right: 24),
icon: new Image.asset(
Theme.of(context).brightness == Brightness.light
? 'assets/icons/create_journey_black.png'
: 'assets/icons/create_journey_white.png'),
onPressed: () {
_analyticsService.logCreateJourneyExploreFeed();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CreateJourney()),
);
},
),
IconButton(
padding: EdgeInsets.only(right: 16),
icon:
new Image.asset('assets/icons/invite_friends_icon.png'),
onPressed: () {
_analyticsService.logShareApp();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => InviteFriends()),
);
},
),
]),
backgroundColor: Colors.transparent,
body: Column(children: <Widget>[
// this is the TabBar
Container(
height: 52.0,
color: Theme.of(context).primaryColor,
// this generates our tabs buttons
child: ListView.builder(
// this gives the TabBar a bounce effect when scrolling farther than it's size
physics: BouncingScrollPhysics(),
controller: _tabScrollController,
// make the list horizontal
scrollDirection: Axis.horizontal,
// number of tabs
itemCount: _userInterestCategory.length,
itemBuilder: (BuildContext context, int index) {
return Padding(
// each button's key
key: _keys[index],
// padding for the buttons
padding: EdgeInsets.fromLTRB(8.0, 4.0, 0.0, 8.0),
child: ButtonTheme(
child: AnimatedBuilder(
animation: _colorTweenBackgroundOn,
builder: (context, child) => FlatButton(
// get the color of the button's background (dependent of its state)
color: _getBackgroundColor(index),
padding:
EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0),
// make the button a rectangle with round corners
shape: RoundedRectangleBorder(
borderRadius:
new BorderRadius.circular(24.0)),
onPressed: () {
setState(() {
_buttonTap = true;
// trigger the controller to change between Tab Views
_controller.animateTo(index);
// set the current index
_setCurrentIndex(index);
// scroll to the tapped button (needed if we tap the active button and it's not on its position)
_scrollTo(index);
});
},
child: index !=
_userInterestCategory.length - 1
? Text(
// get the icon
_userInterestCategory[index],
// get the color of the icon (dependent of its state)
style: (TextStyle(
color: _getForegroundColor(index),
)),
)
: Icon(
Icons.add,
color: _getForegroundColor(index),
)),
)));
})),
Flexible(
// this will host our Tab Views
child: TabBarView(
// and it is controlled by the controller
controller: _controller,
children: _interestFeedBuild,
)),
]));
},
);
}
}
class BuildInterestFeed extends StatefulWidget {
final String categoryId;
BuildInterestFeed({this.categoryId});
#override
_BuildInterestFeedState createState() => _BuildInterestFeedState();
}
class _BuildInterestFeedState extends State<BuildInterestFeed>
with AutomaticKeepAliveClientMixin {
bool isLoading = false;
List<Journey> journeys = [];
final AnalyticsService _analyticsService = locator<AnalyticsService>();
String docId = Uuid().v4();
List<DocumentSnapshot> journeysFetched = []; // stores fetched products
bool hasMore = true; // flag for more products available or not
int documentLimit = 10; // documents to be fetched per request
DocumentSnapshot
lastDocument; // flag for last document from where next 10 records to be fetched
ScrollController _verticalScrollController = ScrollController();
#override
void initState() {
super.initState();
if (widget.categoryId != '+') {
_getExploreFeedData();
_verticalScrollController.addListener(() {
double maxScroll = _verticalScrollController.position.maxScrollExtent;
double currentScroll = _verticalScrollController.position.pixels;
double delta = MediaQuery.of(context).size.height * 0.10;
if (maxScroll - currentScroll <= delta) {
_getExploreFeedData();
}
});
}
}
_getExploreFeedData() async {
if (!hasMore) {
print('No More Journeys');
return;
}
if (isLoading) {
return;
}
setState(() {
isLoading = true;
});
if (widget.categoryId == 'All') {
await buildAllDataFeed();
} else {
QuerySnapshot querySnapshot;
if (lastDocument == null) {
querySnapshot = await journeyRef
.orderBy('createdAt', descending: true)
.where('category', arrayContains: widget.categoryId)
.limit(documentLimit)
.getDocuments();
} else {
querySnapshot = await journeyRef
.orderBy('createdAt', descending: true)
.where('category', arrayContains: widget.categoryId)
.startAfterDocument(lastDocument)
.limit(documentLimit)
.getDocuments();
}
if (querySnapshot.documents.length != 0) {
if (querySnapshot.documents.length < documentLimit) {
hasMore = false;
}
if (querySnapshot.documents.length != 0) {
lastDocument =
querySnapshot.documents[querySnapshot.documents.length - 1];
} else {
lastDocument = null;
}
journeysFetched.addAll(querySnapshot.documents);
}
}
setState(() {
isLoading = false;
});
}
buildAllDataFeed() async {
QuerySnapshot querySnapshot;
if (lastDocument == null) {
querySnapshot = await journeyRef
.orderBy('createdAt', descending: true)
.limit(documentLimit)
.getDocuments();
} else {
querySnapshot = await journeyRef
.orderBy('createdAt', descending: true)
.startAfterDocument(lastDocument)
.limit(documentLimit)
.getDocuments();
}
if (querySnapshot.documents.length < documentLimit) {
hasMore = false;
}
if (querySnapshot.documents.length != 0) {
lastDocument =
querySnapshot.documents[querySnapshot.documents.length - 1];
} else {
lastDocument = null;
}
// lastDocument = querySnapshot.documents[querySnapshot.documents.length - 1];
journeysFetched.addAll(querySnapshot.documents);
}
addInterestData({tags}) async{
userInterestsRef.document(currentUser.id).updateData({
'tags': FieldValue.arrayUnion(tags),
});
}
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return
RefreshIndicator(
onRefresh: () => _getExploreFeedData(),
child:
Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(children: [
Expanded(
child: journeysFetched.length == 0
? Center(
child: Text(
'There\'s nothing new here right now',
style: Theme.of(context).textTheme.subhead.copyWith(
color: Theme.of(context)
.primaryColorDark
.withOpacity(0.40)),
),
)
: StaggeredGridView.countBuilder(
controller: _verticalScrollController,
crossAxisCount: 2,
itemCount: journeysFetched.length,
itemBuilder: (BuildContext context, int index) {
Map journeyData = journeysFetched.elementAt(index).data;
String journeyId =
journeysFetched.elementAt(index).documentID;
return GestureDetector(
child: Container(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TransitionToImage(
transitionType: TransitionType.fade,
borderRadius: BorderRadius.circular(8.0),
loadingWidget: Container(
height: 242,
decoration: (BoxDecoration(
borderRadius:
BorderRadius.circular(8.0),
color: Theme.of(context)
.primaryColorLight
.withOpacity(0.05),
))),
image: AdvancedNetworkImage(
journeyData['coverImage'],
useDiskCache: true,
cacheRule: CacheRule(
maxAge: Duration(hours: 9999))),
),
Padding(
padding: EdgeInsets.only(
top: SizeConfig.safeBlockVertical * 0.5,
left:
SizeConfig.safeBlockVertical * 0.5),
child: Text(
journeyData['title'],
style: Theme.of(context)
.textTheme
.bodyText2
.copyWith(
fontWeight: FontWeight.w500),
))
],
),
),
onTap: () {
addInterestData(
tags: journeyData['community']['hashtags']);
Navigator.push(
context,
MaterialPageRoute(
settings:
RouteSettings(name: 'journey_home'),
builder: (context) =>
JourneyHome(journeyId: journeyId)));
});
},
staggeredTileBuilder: (int index) => StaggeredTile.fit(1),
mainAxisSpacing: SizeConfig.safeBlockVertical * 2,
crossAxisSpacing: 8.0,
),
),
isLoading ? circularProgress(context) : Container()
]),
),
),
);
}
}
There is also a pub package named states_rebuilder
https://pub.dev/packages/states_rebuilder
or
our Widget should have a setState() method, this method is called, the widget is redrawn

Flutter: Drag Draggable Stack item inside a Draggable Stack Item

I have a Draggable on a DragTarget as part of a Stack. Inside is another Stack with Draggables, again on DragTargets and so on... (Stack over Stack over Stack etc.).
The Draggable is a Positioned with a Listener telling where to be placed.
homeView.dart
body: Stack(children: [
DraggableWidget(parentKey, Offset(0, 0)),
]),
draggableWidget.dart
class DraggableWidget extends StatefulWidget {
final Key itemKey;
final Offset itemPosition;
DraggableWidget(this.itemKey, this.itemPosition);
#override
_DraggableWidgetState createState() => _DraggableWidgetState();
}
class _DraggableWidgetState extends State<DraggableWidget> {
Offset tempDelta = Offset(0, 0);
Window<List<Key>> item;
List<DraggableWidget> childList = [];
Map<Key, Window<List>> structureMap;
initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
structureMap = Provider.of<Data>(context).structureMap;
if (structureMap[widget.itemKey] != null) {
structureMap[widget.itemKey].childKeys.forEach(
(k) => childList.add(
DraggableWidget(k, item.position),
),
);
} else {
structureMap[widget.itemKey] = Window<List<Key>>(
title: 'App',
key: widget.itemKey,
size: Size(MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height),
position: Offset(0, 0),
color: Colors.blue,
childKeys: []);
}
item = Provider.of<Data>(context).structureMap[widget.itemKey];
return Positioned(
top: item.position.dx,
left: item.position.dy,
child: DragTarget(
builder:
(buildContext, List<Window<List<Key>>> candidateData, rejectData) {
return Listener(
onPointerDown: (PointerDownEvent event) {},
onPointerUp: (PointerUpEvent event) {
setState(() {
item.position = Offset(item.position.dx + tempDelta.dx,
item.position.dy + tempDelta.dy);
tempDelta = Offset(0, 0);
});
},
onPointerMove: (PointerMoveEvent event) {
tempDelta = Offset((event.delta.dy + tempDelta.dx),
(event.delta.dx + tempDelta.dy));
},
child: Draggable(
childWhenDragging: Container(),
feedback: Container(
color: item.color,
height: item.size.height,
width: item.size.width,
),
child: Column(children: [
Text(item.title),
Container(
color: item.color,
height: item.size.height,
width: item.size.width,
child: ItemStackBuilder(widget.itemKey, item.position),
),
]),
data: item),
);
},
),
);
}
}
itemStackBuilder.dart
class ItemStackBuilder extends StatelessWidget {
final Key itemKey;
final Offset itemPosition;
ItemStackBuilder(this.itemKey, this.itemPosition);
#override
Widget build(BuildContext context) {
Map<Key, Window<List<Key>>> structureMap =
Provider.of<Data>(context).structureMap;
if (structureMap[itemKey] == null) {
structureMap[itemKey] = Window(size: Size(20, 20), childKeys: []);
}
return Stack(overflow: Overflow.visible, children: [
...stackItems(context),
Container(
height: structureMap[itemKey].size.height,
width: structureMap[itemKey].size.width,
color: Colors.transparent),
]);
}
List<Widget> stackItems(BuildContext context) {
List<Key> childKeyList =
Provider.of<Data>(context).structureMap[itemKey].childKeys;
var stackItemDraggable;
List<Widget> stackItemsList = [];
if (childKeyList == null || childKeyList.length < 1) {
stackItemsList = [Container()];
} else {
for (int i = 0; i < childKeyList.length; i++) {
stackItemDraggable = DraggableWidget(childKeyList[i], itemPosition);
stackItemsList.add(stackItemDraggable);
}
}
return stackItemsList;
}
}
When I want to move the Draggable item on top, the underlying Stack moves.
I tried it with a Listener widget and was able to detect all RenderBoxes inside the Stack.
But how can I select the specific Draggable and/or disable all the other layers? Is it a better idea to forget about Draggables and do it all with Positioned and GestureDetector?
Ok, it was my mistake not of the framework:
on itemStackBuilder.dart I used an additional Container to size the Stack. I was not able to recognise, because color was transparent:
Container(
height: structureMap[itemKey].size.height,
width: structureMap[itemKey].size.width,
color: Colors.transparent),
]);
}
After deleting this part, all works fine for now.

Check / Uncheck ListTiles in ListView.builder() - Flutter

I have a ListView.builder(); in showModalBottomSheet();
Need to select / deselect multiple items on tap everything is well but need to close the modal and show it again to apply changes, another thing is the ListTiles sometimes duplicated more than once, function emptyList doesn't work well.
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:async';
import 'package:flutter/material.dart';
import 'book_details.dart' show BookDetails;
class Explore extends StatefulWidget {
#override
_ExploreState createState() => _ExploreState();
}
var _books,
_categories,
_arranges,
_currentCategory,
_selected,
_primeColor,
_currentFilter,
_isThereIsFilters,
_booksContainer,
_booksWithFilters,
_isLoading,
_noBooks,
_itemIcon;
final GlobalKey<ScaffoldState> _scaffoldKeyExplore =
new GlobalKey<ScaffoldState>();
List<String> _getCats = new List();
List<String> _getArrs = new List();
void _insertCategories() {
for (int i = 0; i < _categories.length; i++) {
_getCats.add(_categories[i]);
}
_getCats.sort();
}
void _insertArranges() {
for (int i = 0; i < _arranges.length; i++) {
_getArrs.add(_arranges[i]);
}
}
class _ExploreState extends State<Explore> with TickerProviderStateMixin {
onCatChange(String category) {
setState(() {
_currentCategory = category;
});
}
#override
void initState() {
super.initState();
_primeColor = Color.fromRGBO(239, 89, 39, 1.0);
_categories = ["أول", "ثاني", "ثالث", "رابع", "خامس"];
_arranges = ["أول", "ثاني", "ثالث", "رابع", "خامس"];
_currentFilter = _arranges[0];
_selected = [];
_isThereIsFilters = false;
}
void emptyList(List list) {
for (var i = 0; i < list.length; i++) {
list.remove(list[i]);
}
}
_showSheet(String type) {
switch (type) {
case "filters":
showModalBottomSheet(
context: _scaffoldKeyExplore.currentContext,
builder: (BuildContext context) {
return Directionality(
textDirection: TextDirection.rtl,
child: Container(
child: Column(children: <Widget>[
Expanded(
child: new ListView.builder(
itemCount: _getArrs[0] != null ? _getArrs.length : 0,
itemBuilder: (BuildContext context, int i) {
return new RadioListTile(
title: Text(_getArrs[i]),
value: _getArrs[i],
groupValue: _currentFilter,
onChanged: (val) {
setState(() {
_currentFilter = val;
});
});
}),
)
])),
);
});
break;
case "categories":
default:
showModalBottomSheet(
context: _scaffoldKeyExplore.currentContext,
builder: (BuildContext context) {
return Directionality(
textDirection: TextDirection.rtl,
child: Container(
child: Column(children: <Widget>[
Container(
color: _primeColor,
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
IconButton(
icon: Icon(Icons.close, color: Colors.white),
onPressed: () {
emptyList(_selected);
//Navigator.pop(context);
//_showSheet(type);
}),
IconButton(
icon:
Icon(Icons.done_all, color: Colors.white),
onPressed: () {
if (_selected.length > 0) {
_getFilteredBooks(_selected);
setState(() {
_isThereIsFilters = true;
});
} else {
setState(() {
_isThereIsFilters = false;
});
}
Navigator.pop(context);
})
]),
),
Expanded(
child: new ListView.builder(
itemCount: _getCats != null ? _getCats.length : 0,
itemBuilder: (BuildContext context, int i) {
final _isSelected = _selected.contains(_getCats[i]);
return new ListTile(
leading: Icon(Icons.category),
trailing: _isSelected ? Icon(Icons.done) : null,
title: Text(_getCats[i]),
onTap: () {
setState(() {
_isSelected
? _selected.remove(_getCats[i])
: _selected.add(_getCats[i]);
});
//Navigator.pop(context);
//_showSheet(type);
});
}),
)
])),
);
});
break;
}
}
#override
Widget build(BuildContext context) {
return new Directionality(
textDirection: TextDirection.rtl,
child: new Scaffold(
key: _scaffoldKeyExplore,
appBar:
AppBar(title: Text("استكشاف"), elevation: 0.0, actions: <Widget>[
IconButton(
icon: Icon(Icons.category, color: _primeColor),
onPressed: () => _showSheet("categories")),
IconButton(
icon: Icon(Icons.filter_list, color: _primeColor),
onPressed: () => _showSheet("filters"))
]),
body: Center(child: Text("Nothing..."));
));
}
}
Thank you
need to close the modal and show it again to apply changes
This happens because the showModalBottomSheet's builder needs to be called again to reflect the changes.
In Flutter, StatefulWidgets should be able to rebuild any time the state changes - which is not the case here, because of the bottom sheet being shown.
Why did I run into this issue (on a meta level)?
Storing the state in StatefulWidgets is useful for saving UI state, but you quickly outgrow this technique if you want to store some "app state" or "data state" that is independent of the screen it's on.
It is finally time to fundamentally rethink your state management and settle on a full-fledged state management pattern that decouples the state from the widgets. Luckily, there are a few to choose from:
Making everything global, like you did above. This is generally not a good idea, as you break the contract of setState (state can be modified without the widgets being notified). Also, you break hot restart and stuff like that.
Using an InheritedWidget, where widgets below a root widget can access the same state.
Using a ScopedModel, which builds on top of that.
Using the infamous BLoC pattern, which also builds on top of the InheritedWidget, but adds some Stream-y stuff to make everything more reactive.
Probably many more.
Here is a great Youtube video about state management from Google I/O, where several patterns are being presented.
Anyways, are bottom sheets the right widget for the task ahead?
According to the Material Design spec, the modal bottom sheet is "an alternative to inline menus or simple dialogs on mobile, providing room for additional items, longer descriptions, and iconography".
More concrete, the showModalBottomSheet function is designed to show a widget that doesn't affect the parent over time, but rather - if at all - at a single point in time. That's why it returns a Future<T>, not a Stream<T>.
Be aware that you are trying to use the bottom sheet in a way that it's not intended to be used.
In your case, I'd recommend just using a new screen.

Flutter ListView lazy loading

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