How to implement listview lazyload inside a NestedScrollView? - flutter

I have an app, it has a page that act as an entry point and showing a TabView containing 3 or more pages on it. It uses NestedScrollView and SliverAppBar to give some animation when user scroll the view.
I want to implement lazy load of a paginated list but since it does not allows me to use a controller inside the CustomScrollView as mentioned in the docs in this line:
builder: (BuildContext context) {
return CustomScrollView(
// The "controller" and "primary" members should be left
// unset, so that the NestedScrollView can control this
// inner scroll view.
// If the "controller" property is set, then this scroll
// view will not be associated with the NestedScrollView.
// The PageStorageKey should be unique to this ScrollView;
// it allows the list to remember its scroll position when
// the tab view is not on the screen.
key: PageStorageKey<String>(name),
slivers: <Widget>[
I cannot make use of ScrollController in the child page to get the scroll value to trigger the loadMore function. Fortunately, there is a similar widget to listen the scroll event called ScrollNotification. But I don't know which property is holding the value of the maximum scroll limit.
Tried to compare the available properties by this:
bool _onScrollNotification(ScrollNotification notification) {
if (notification is! ScrollEndNotification) return false;
print('extentBefore: ${notification.metrics.extentBefore}');
print('extentAfter: ${notification.metrics.extentAfter}');
print('maxScrollExtent: ${notification.metrics.maxScrollExtent}');
return true;
}
But its seems like they doesn't hold any fixed value as I need. It always changed its value independently.
I also cannot use the ScrollController on the parent page (the tabview_holder) since each page in each tabs has independent bloc, events, data & fetching algorithm. With that in mind, how can I achieve this requirement?
Please have a look at my script:
tabview_holder.dart (not a real file name, just to illustrate it)
class EventPage extends StatefulWidget {
EventPage({Key key}) : super(key: key);
#override
_EventPageState createState() => _EventPageState();
}
class _EventPageState extends State<EventPage>
with SingleTickerProviderStateMixin {
final ScrollController _scrollController = ScrollController();
final List<Widget> _tabs = [
Tab(text: 'My Events'),
Tab(text: "Private Events"),
Tab(text: "Division Events"),
Tab(text: "Department Events"),
Tab(text: "Public Events"),
];
double _bottomNavigatorPosition = 0.0;
double _gradientStop = 0.2;
TabController _tabController;
#override
void initState() {
super.initState();
_scrollController.addListener(_scrollListener);
_tabController = TabController(
initialIndex: 0,
length: _tabs.length,
vsync: this,
);
}
#override
void dispose() {
_scrollController.dispose();
_tabController.dispose();
super.dispose();
}
void _scrollListener() {
ScrollDirection direction = _scrollController.position.userScrollDirection;
switch (direction) {
case ScrollDirection.reverse:
setState(() {
_gradientStop = 0.0;
_bottomNavigatorPosition = -100.0;
});
return;
break;
case ScrollDirection.forward:
case ScrollDirection.idle:
setState(() {
_gradientStop = 0.2;
_bottomNavigatorPosition = 0.0;
});
break;
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Stack(
children: [
NestedScrollView(
controller: _scrollController,
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverOverlapAbsorber(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
context),
sliver: SliverAppBar(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
automaticallyImplyLeading: false,
floating: true,
expandedHeight: 100,
flexibleSpace: FlexibleSpaceBar(
background: Container(
child: Stack(
children: [
Positioned(
left: 30.0,
bottom: 10,
child: PageHeader(title: 'Events'),
),
],
),
),
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: _SliverAppBarDelegate(
TabBar(
controller: _tabController,
isScrollable: true,
indicator: BubbleTabIndicator(
indicatorHeight: 35.0,
indicatorColor: Theme.of(context).primaryColor,
tabBarIndicatorSize: TabBarIndicatorSize.tab,
),
tabs: _tabs,
),
),
),
];
},
body: TabBarView(
controller: _tabController,
children: [
MyEventsPage(),
PrivateEventsPage(),
MyEventsPage(),
MyEventsPage(),
MyEventsPage(),
],
),
),
_buildBottomGradient(),
_buildBottomNavigator(),
],
),
),
);
}
Widget _buildBottomGradient() {
return IgnorePointer(
child: AnimatedContainer(
duration: Duration(milliseconds: 200),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
stops: [_gradientStop / 2, _gradientStop],
colors: [
Color(0xFF121212),
Colors.transparent,
],
),
),
),
);
}
Widget _buildBottomNavigator() {
return AnimatedPositioned(
duration: Duration(milliseconds: 200),
left: 0.0,
right: 0.0,
bottom: _bottomNavigatorPosition,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: PageNavigator(
primaryButtonText: 'Create new event',
onPressedPrimaryButton: () {
Navigator.of(context).pushNamed(Routes.EVENT_CREATE);
},
),
),
);
}
}
tabview_item.dart
class MyEventsPage extends StatefulWidget {
MyEventsPage({Key key}) : super(key: key);
#override
_MyEventsPageState createState() => _MyEventsPageState();
}
class _MyEventsPageState extends State<MyEventsPage>
with AutomaticKeepAliveClientMixin<MyEventsPage> {
Completer<void> _refreshCompleter;
PaginatedEvent _paginated;
MyEventsBloc _myEventsBloc;
bool _isFetchingMoreInBackground;
#override
void initState() {
super.initState();
_myEventsBloc = BlocProvider.of<MyEventsBloc>(context);
_myEventsBloc.add(MyEventsPageInitialized());
_refreshCompleter = Completer<void>();
_isFetchingMoreInBackground = false;
}
void _set(PaginatedEvent paginated) {
setState(() {
_paginated = paginated;
});
_refreshCompleter?.complete();
_refreshCompleter = Completer();
}
void _add(Event data) {
setState(() {
_paginated.data.add(data);
});
}
void _update(Event data) {
final int index = _paginated.data.indexWhere((leave) {
return leave.id == data.id;
});
setState(() {
_paginated.data[index] = data;
});
}
void _destroy(Event data) {
final int index = _paginated.data.indexWhere((leave) {
return leave.id == data.id;
});
setState(() {
_paginated.data.removeAt(index);
});
}
void _append(PaginatedEvent paginated) {
setState(() {
_paginated.currentPage = paginated.currentPage;
_paginated.data.addAll(paginated.data);
});
}
bool _onScrollNotification(ScrollNotification notification) {
if (notification is! ScrollEndNotification) return false;
print('extentBefore: ${notification.metrics.extentBefore}');
print('extentAfter: ${notification.metrics.extentAfter}');
print('maxScrollExtent: ${notification.metrics.maxScrollExtent}');
return true;
}
#override
Widget build(BuildContext context) {
super.build(context);
return RefreshIndicator(
onRefresh: () {
_myEventsBloc.add(MyEventsRefreshRequested());
return _refreshCompleter.future;
},
child: NotificationListener<ScrollNotification>(
onNotification: _onScrollNotification,
child: CustomScrollView(
slivers: [
SliverOverlapInjector(
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
),
SliverToBoxAdapter(
child: BlocConsumer<MyEventsBloc, MyEventsState>(
listener: (context, state) {
if (state is MyEventsLoadSuccess) {
_set(state.data);
}
if (state is MyEventsCreateSuccess) {
_add(state.data);
}
if (state is MyEventsUpdateSuccess) {
_update(state.data);
}
if (state is MyEventsDestroySuccess) {
_destroy(state.data);
}
if (state is MyEventsLoadMoreSuccess) {
_append(state.data);
}
},
builder: (context, state) {
if (state is MyEventsLoadSuccess) {
return EventList(data: _paginated.data);
}
return ListLoading();
},
),
),
],
),
),
);
}
#override
bool get wantKeepAlive => true;
}

Finally found the answer by my own after doing some research. Not a perfect solution but it works.
bool _onScrollNotification(UserScrollNotification notification) {
/// Make sure it listening to the nearest depth of scrollable views
/// and ignore notifications if scroll axis is not vertical.
if (notification.depth == 0 && notification.metrics.axis == Axis.vertical) {
ScrollDirection direction = notification.direction;
if (direction == ScrollDirection.reverse && !_isFetchingMoreData) {
/// Check if the user is scrolling the list downward to prevent
/// function call on upward. Also check if there is any fetch
/// queues, if it still fetching, skip this step and do nothing.
/// It was necessary to prevent the notification to bubble up
/// the widget with `_loadMoreData()` call.
if (_paginated.currentPage < _paginated.lastPage)
/// If the conditions above are passed, we are safe to load more.
return _loadMoreData();
}
}
return true;
}

Related

How to apply Scroll controller for multiple SilverList or ListView items?

I have found the internet a way to hide arrow when scrolling horizontally through FlutterLogo with CustomSrollView. The functionality works and i want to place it inside a ListView.builder or SilverList so i have multilple widgets with the scrollfunctionality but once i put CustomScrollView inside a list weither its ListView.builder or SilverList i get error:
ScrollController attached to multiple scroll views.
'package:flutter/src/widgets/scroll_controller.dart':
Failed assertion: line 108 pos 12: '_positions.length == 1'
This is the full code:
class AppView2 extends StatefulWidget {
const AppView2({
Key? key,
}) : super(key: key);
#override
_AppViewState createState() => _AppViewState();
}
class _AppViewState extends State<AppView2> {
late ScrollController _hideButtonController;
var _isVisible;
#override
void initState() {
_isVisible = true;
_hideButtonController = ScrollController();
_hideButtonController.addListener(() {
if (_hideButtonController.position.userScrollDirection ==
ScrollDirection.reverse) {
if (_isVisible == true) {
/* only set when the previous state is false
* Less widget rebuilds
*/
print("**** $_isVisible up"); //Move IO away from setState
setState(() {
_isVisible = false;
});
}
} else {
if (_hideButtonController.position.userScrollDirection ==
ScrollDirection.forward) {
if (_isVisible == false) {
setState(() {
_isVisible = true;
});
}
}
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text("Example app bar"),
),
body: CustomScrollView(slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return SizedBox(
height: 500,
child: Stack(
children: [
CustomScrollView(
controller: _hideButtonController,
scrollDirection: Axis.horizontal,
shrinkWrap: true,
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.all(20.0),
sliver: SliverList(
delegate: SliverChildListDelegate(
<Widget>[
FlutterLogo(
size: 600,
)
],
),
),
),
],
),
Visibility(
visible: _isVisible,
child: Align(
alignment: Alignment.centerRight,
child: FloatingActionButton(
child: const Icon(Icons.arrow_forward_ios),
onPressed: () {}),
),
)
],
),
);
}, childCount: 10),
)
])));
}
}
How can i make sure that _hideButtonController works with multiple widget inside SilverList or ListView?
It is not very clear what you are trying to accomplish, but here is the reason why the error is thrown, and maybe some suggestions to help.
The error is thrown because you use the position getter of the ScrollController inside your listener code, while having multiple positions attached. Here is a quote from the documentation:
Calling this is only valid when only a single position is attached.
https://api.flutter.dev/flutter/widgets/ScrollController/position.html
You could use positions in your listener and check your conditions for any of the attached positions, although that is probably not what you want.
_hideButtonController.addListener(() {
if (_hideButtonController.positions.any((pos) => pos.userScrollDirection ==
ScrollDirection.reverse)) {
if (_isVisible == true) {
/* only set when the previous state is false
* Less widget rebuilds
*/
print("**** $_isVisible up"); //Move IO away from setState
setState(() {
_isVisible = false;
});
}
} else {
if (_hideButtonController.positions.any((pos) => pos.userScrollDirection ==
ScrollDirection.forward)) {
if (_isVisible == false) {
setState(() {
_isVisible = true;
});
}
}
}
});
If you only want to share the code for hiding the button when a condition is met for a singular horizontal scroller, your best bet is probably to write your own widget which holds its own ScrollController with the same listener code you already have. This allows every child of the vertical list to have its own ScrollController for the horizontal list and thus allows you to only hide the button for the affected controller:
class LogoHidingScrollView extends StatefulWidget {
const LogoHidingScrollView({
Key? key,
}) : super(key: key);
_LogoHidingScrollViewState createState() => _LogoHidingScrollViewState();
}
class _LogoHidingScrollViewState extends State<LogoHidingScrollView> {
final ScrollController _scrollController = ScrollController();
bool _isVisible = true;
void initState() {
_isVisible = true;
_scrollController.addListener(() {
if (_scrollController.position.userScrollDirection ==
ScrollDirection.reverse) {
if (_isVisible == true) {
/* only set when the previous state is false
* Less widget rebuilds
*/
print("**** $_isVisible up"); //Move IO away from setState
setState(() {
_isVisible = false;
});
}
} else {
if (_scrollController.position.userScrollDirection ==
ScrollDirection.forward) {
if (_isVisible == false) {
setState(() {
_isVisible = true;
});
}
}
}
});
super.initState();
}
Widget build(BuildContext context) {
return SizedBox(
height: 500,
child: Stack(
children: [
CustomScrollView(
controller: _scrollController,
scrollDirection: Axis.horizontal,
shrinkWrap: true,
slivers: <Widget>[
SliverPadding(
padding: const EdgeInsets.all(20.0),
sliver: SliverList(
delegate: SliverChildListDelegate(
<Widget>[
FlutterLogo(
size: 600,
)
],
),
),
),
],
),
Visibility(
visible: _isVisible,
child: Align(
alignment: Alignment.centerRight,
child: FloatingActionButton(
child: const Icon(Icons.arrow_forward_ios), onPressed: () {}),
),
)
],
),
);
}
}
If you truly want to synchronize the scrolling behaviour between all those horizontal scroll views you could have a look at: https://pub.dev/packages/linked_scroll_controller

Disable scrolling of CustomScrollView while scrolling without setState Flutter

I have multiple widget and lists within CustomScrollView and I would like to stop CustomScrollView to scroll while scrolling on some pixels bound condition.
I can use NeverScrollPhysics() to stop it but I don't want to use setState() function here because the CustomScrollview content with lists is big enough to make the screen laggy while reloading on scroll.
Also tried with Provider but the builder is providing only a child widget which is not working with sliver list.
Here is the code using setState() :
NotificationListener(
onNotification: (ScrollNotification notif) {
if(notif is ScrollUpdateNotification) {
if (canScroll && notif.metrics.pixels > 100) {
canScroll = false;
setState(() {});
}
}
if(notif is ScrollEndNotification) {
if(!canScroll) {
canScroll = true;
setState(() {});
}
}
return true;
},
child: CustomScrollView(
shrinkWrap: true,
physics: canScroll ? BouncingScrollPhysics() : NeverScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(),
List(),
List(),
],
),
),
Is there a way to reload only the CustomScrollView without its child ? Otherwise any workaround to prevent scrolling in this case ?
Thanks for help
When the build method is called, all widgets in that build method will be rebuild except for const widgets, but const widget cannot accept dynamic arguments (only a constant values).
Riverpod provides a very good solution in this case,
With ProviderScope you can pass arguments by inherited widget instead of widget constructor (as when passing arguments using navigation) so the contractor can be const.
Example :
Data module
TLDR you need to use Freezed package or override the == operator and the hashCode almost always because of dart issue.
class DataClass {
final int age;
final String name;
const DataClass(this.age, this.name);
#override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is DataClass && other.age == age && other.name == name;
}
#override
int get hashCode => age.hashCode ^ name.hashCode;
}
setting our ScopedProvider as a global variable
final dataClassScope = ScopedProvider<DataClass>(null);
The widget we use in our list
class MyChildWidget extends ConsumerWidget {
const MyChildWidget();
#override
Widget build(BuildContext context, ScopedReader watch) {
final data = watch(dataClassScope);
// Note for better optimaization
// in case you are sure the data you are passing to this widget wouldn't change
// you can just use StatelessWidget and set the data as:
// final data = context.read(dataClassScope);
// use ConsumerWidget (or Consumer down in this child widget tree) if the data has to change
print('widget with name\n${data.name} rebuild');
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 20),
child: Text(
'Name : ${data.name}\nAge ${data.age}',
textAlign: TextAlign.center,
),
),
);
}
}
finally the main CustomScrollView widget
class MyMainWidget extends StatefulWidget {
const MyMainWidget();
#override
State<MyMainWidget> createState() => _MyMainWidgetState();
}
class _MyMainWidgetState extends State<MyMainWidget> {
bool canScroll = true;
void changeCanScrollState() {
setState(() => canScroll = !canScroll);
print('canScroll $canScroll');
}
final dataList = List.generate(
20,
(index) => DataClass(10 * index, '$index'),
);
#override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
changeCanScrollState();
},
child: CustomScrollView(
shrinkWrap: true,
physics: canScroll
? BouncingScrollPhysics()
: NeverScrollableScrollPhysics(),
slivers: [
for (int i = 0; i < dataList.length; i++)
ProviderScope(
overrides: [
dataClassScope.overrideWithValue(dataList[i]),
],
child: const MyChildWidget(),
),
],
),
),
);
}
}
Don't forget to wrap the MaterialApp with ProviderScope.
runApp(
ProviderScope(
child: MyApp(),
),
);
Try this solution use const constructor for child widget so it won't rebuild unless widget changed
class MyHomePage extends StatelessWidget {
ValueNotifier<ScrollPhysics> canScroll =
ValueNotifier(const BouncingScrollPhysics());
MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: NotificationListener(
onNotification: (ScrollNotification notif) {
if (notif is ScrollUpdateNotification) {
if (canScroll.value.runtimeType == BouncingScrollPhysics &&
notif.metrics.pixels > 100) {
canScroll.value = const NeverScrollableScrollPhysics();
debugPrint("End false");
}
}
if (notif is ScrollEndNotification) {
if (canScroll.value.runtimeType == NeverScrollableScrollPhysics) {
debugPrint("End");
Future.delayed(const Duration(milliseconds: 300),
() => canScroll.value = const BouncingScrollPhysics());
debugPrint("End1");
}
}
return true;
},
child: ValueListenableBuilder(
valueListenable: canScroll,
builder:
(BuildContext context, ScrollPhysics scrollType, Widget? child) =>
CustomScrollView(
physics: scrollType,
slivers: [
SliverToBoxAdapter(
child: Container(
height: 200,
color: Colors.black,
),
),
SliverToBoxAdapter(
child: Column(
children: [
Container(
height: 100,
color: Colors.blue,
),
Container(
height: 200,
color: Colors.grey,
),
Container(
height: 200,
color: Colors.blue,
),
Container(
height: 200,
color: Colors.grey,
),
Container(
height: 200,
color: Colors.blue,
),
],
),
),
],
),
),
),
);
}
}
Are you just need to stop the user from scrolling it? I think you can try to controller the list to a fixed position by using jumoTo.
...
final _controller = ScrollController();
#override
Widget build(BuildContext context) {
return NotificationListener(
onNotification: (ScrollNotification notif) {
if (notif is ScrollUpdateNotification) {
if (notif.metrics.pixels > 100) {
_controller.jumpTo(100)
}
}
return true;
},
child: CustomScrollView(
controller: _controller,
...

Flutter appear The method 'findRenderObject' was called on null

I have a need recently,I need to measure the distance between the child elements in the sliver and the top, but always prompt that findrendereobject is empty。
I can't even try widgetsbinding.instance.addpostframecallback
Console error:
════════ Exception caught by scheduler library
═════════════════════════════════════════════════════
The following
NoSuchMethodError was thrown during a scheduler callback: The method
'findRenderObject' was called on null. Receiver: null Tried calling:
findRenderObject()
Please Everybody look what happened,why can't measuring height~thank you very much!!!
My Code:
class GoodsDetailPage extends StatefulWidget {
final IndexVOS bean;
GoodsDetailPage(this.bean);
#override
_GoodsDetailPageState createState() => _GoodsDetailPageState();
}
class _GoodsDetailPageState extends State<GoodsDetailPage> with SingleTickerProviderStateMixin{
int productId;
int areaId;
String serviceTime;
ProductInfoBean productInfoBean;
String _selectName;
Norms norms;
double screenWidth;
bool _isShow = false;
TabController _tabController;
List<String> tabList;
ScrollController _scrollController = ScrollController();
var globalKeyOne = GlobalKey();
var globalKeyTwo = GlobalKey();
var globalKeyThree = GlobalKey();
var oneY = 0.0;
var twoY = 0.0;
var threeY = 0.0;
ProductModel model = ProductModel();
GoodsSpecModel _specModel = GoodsSpecModel();
#override
void initState() {
productId = widget.bean.productId;
areaId = widget.bean.areaId;
serviceTime = widget.bean.serviceTimeBegin;
tabList = [
"商品",
"评价",
"详情",
];
_tabController = TabController(
length: tabList.length,
vsync: this,
);
_scrollController.addListener(() {
var of = _scrollController.offset;
setState(() {
_isShow = of >= screenWidth-50;
});
if (of > threeY - oneY) {
_tabController.animateTo(2);
}else if (of > twoY - oneY) {
_tabController.animateTo(1);
} else {
_tabController.animateTo(0);
}
print("滚动了$of one=${twoY - oneY}=two=${threeY - oneY}");
});
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
super.initState();
}
//等待界面渲染完成
void _afterLayout(_){
oneY = getY(globalKeyOne.currentContext);
twoY = getY(globalKeyTwo.currentContext);
threeY = getY(globalKeyThree.currentContext);
}
static double getY(BuildContext buildContext) {
final RenderBox box = buildContext.findRenderObject();
//final size = box.size;
final topLeftPosition = box.localToGlobal(Offset.zero);
return topLeftPosition.dy;
}
Future _request() async{
model.requestGoodsDetail(productId: productId,areaId: areaId,serviceTime: serviceTime);
}
#override
Widget build(BuildContext context) {
screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
body: Container(
width: double.infinity,
height: double.infinity,
child: ProviderWidget<ProductModel>(
model: model,
onModelReady: (model) => model.requestGoodsDetail(productId: productId,areaId: areaId,serviceTime: serviceTime),
builder: (context, model, child) {
if (model.busy) {
return ViewStateBusyWidget();
}
if (model.error) {
return ViewStateErrorWidget(error: model.viewStateError, onPressed: _request);
}
return Column(
children: <Widget>[
Expanded(child: _body()),
_bottom()
],
);
}
),
)
);
}
Widget _body(){
var _normalBack = Image.asset(ImagePath.normalBack,height: dp40,width: dp40);
var _detailBack = Image.asset(ImagePath.detailBack,height: dp60,width: dp60);
var _normalShare = Image.asset(ImagePath.detailShareSmall,height: dp40,width: dp60);
var _detailShare = Image.asset(ImagePath.detailShare,height: dp60,width: dp140);
var _normalDot = Image.asset(ImagePath.threeDot,height: dp40,width: dp40);
var _detailDot = Image.asset(ImagePath.detailDot,height: dp60,width: dp60);
return CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverAppBar(
key: globalKeyOne,
title: _isShow?_topTabTitle():Container(),
centerTitle: true,
expandedHeight: screenWidth,
floating: false,
pinned: true,
snap: false,
elevation: 0.5,
leading: IconButton(
icon: _isShow?_normalBack:_detailBack,
onPressed: () => pop(),
),
actions: <Widget>[
GestureDetector(
child: _isShow?_normalShare:_detailShare,
onTap: (){
print("分享");
},
),
SizedBox(width: dp24),
GestureDetector(
child: _isShow?_normalDot:_detailDot,
onTap: _showPopup,
),
SizedBox(width: dp30),
],
flexibleSpace: FlexibleSpaceBar(
background: GoodsDetailBannerWidget(model),
),
),
SliverList(
delegate: SliverChildListDelegate([
_activityImage(),
GoodsDetailInfoWidget(model),
gap20,
GoodsDetailOtherWidget(model,serviceTime),
GoodsDetailCategoryWidget(click: _clickSpec,name: _selectName),
gap20,
Column(
key: globalKeyTwo,
children: <Widget>[
GoodsDetailCommentWidget(model),
gap20,
],
),
Container(
key: globalKeyThree,
child: GoodsDetailDescWidget(model),
)
]),
),
],
);
}
Widget _bottom(){
return GoodsDetailBottomWidget(
clickBuy: (){
if(_selectName == null){
_clickSpec();
return;
}
routePush(ConfirmOrderPage(productInfoBean: model.productInfoBean,norms: norms,count: _specModel.countNumber));
},
);
}
Widget _activityImage(){
return Container(
width: double.infinity,
height: dp80,
child: Image.asset(ImagePath.goodsActivity),
);
}
Widget _topTabTitle(){
return Container(
height: dp60,
child: TabBar(
controller: _tabController,
labelColor: ColorConfig.themeGreen,
unselectedLabelColor: ColorConfig.C09,
labelStyle: StyleConfig.green_36Style,
unselectedLabelStyle: StyleConfig.normalTitle36Style,
indicatorColor: ColorConfig.themeGreen,
indicatorSize: TabBarIndicatorSize.label,
indicatorWeight: 2,
isScrollable: true,
labelPadding: EdgeInsets.symmetric(horizontal: dp20),
tabs: tabList.map((item) {
return Tab(
text: item,
);
}).toList(),
onTap: (index){
switch(index){
case 0:
_scrollController.jumpTo(0);
_tabController.animateTo(0);
break;
case 1:
_scrollController.jumpTo(twoY - oneY);
_tabController.animateTo(1);
break;
case 2:
_scrollController.jumpTo(threeY - oneY);
_tabController.animateTo(2);
break;
}
},
),
);
}
void _showPopup(){
showPopupWindow(
context,
gravity: KumiPopupGravity.rightTop,
underStatusBar: true,
underAppBar: true,
offsetX: -dp30,
offsetY: -dp20,
childFun: (pop){
return Container(
key: GlobalKey(),
child: GoodsDetailPopupMenuWidget(),
);
}
);
}
void _clickSpec(){
_specModel.initData(model.normInfoBean.norms);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return GoodsSpecSelectDialog(
model: _specModel,
onSelected: (bean, count) {
norms = bean;
setState(() {
_selectName = "已选:"+bean.normName + ",数量:" + count.toString();
});
},
);
},
);
}
#override
void dispose() {
_specModel.dispose();
_scrollController.dispose();
_tabController.dispose();
super.dispose();
}
}
Your global keys are attached to the widgets in _body() method but this method is just called when model.busy = false and model.error = false. It means globalKeyOne.currentContext will be null when model.busy = true || model.error = true. Your _afterLayout(...) is called in all the cases, that's why it fails with NPE.
use widgetsbinding.instance.addpostframecallback
if you put the method getPosition in the widgetsbinding..... then remember to add this in the parameter of getPosition(_)
for some reason this works
Here is the Sample Code
import 'package:flutter/material.dart';
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
GlobalKey _akey = GlobalKey();
var top;
var left;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
height: 200,
width: 300,
key: _akey,
color: Colors.blue,
),
),
);
}
_afterLayout(_) {
_getPosition();
}
_getPosition() async {
final RenderBox rb = _akey.currentContext.findRenderObject();
var position = rb.localToGlobal(Offset.zero);
top = position.dy;
left = position.dx;
print('$left , $top');
}
}
try putting it in build Function not initState()

Listen for Start ScrollController on CustomScrollView

In my project I have a project where I only want to set the visibility of an action button if the screen is scrolled all the way up.
The code that I have right now works fine but it will trigger the code as soon as the screen begins to scroll up - and it is really not what I want.
So how can I fix this ?
Below is my code as on demand
class AccountPage extends StatefulWidget with NavigationStates {
#override
_AccountPage createState() => _AccountPage();
}
class _AccountPage extends State<AccountPage> {
String searchQuery = '';
ScrollController _controller;
bool sliverActionsHidden = true;
// Tracking search query.
void _updateSearchQuery(String query) {
setState(
() {
searchQuery = query;
},
);
}
// Listening for user scroll on screen.
void _scrollListener() {
if (_controller.offset <= _controller.position.minScrollExtent &&
!_controller.position.outOfRange) {
setState(() {
sliverActionsHidden = false;
print('Reach the top');
});
}
else {
setState(() {
sliverActionsHidden = true;
print('Reach the bottom');
});
}
}
#override
void initState() {
_controller = ScrollController();
_controller.addListener(_scrollListener);
super.initState();
}
#override
Widget build(BuildContext context) {
/* Action IconButtons ----------------------------------------------- */
final actionButtons = <Widget>[
IconButton(
icon: Icon(Icons.filter_list),
onPressed: () {
print('Filter');
},
),
IconButton(
icon: Icon(Icons.more_vert),
onPressed: () {
print('More Settings');
},
),
];
/* Material Appbar ----------------------------------------------- */
final PreferredSizeWidget appBar = SearchBar(
canSearch: true,
query: searchQuery,
searchQuery: (value) {
_updateSearchQuery(value);
print("Listening for search query : $value");
},
// Action Icons
actions: actionButtons,
);
/* Sliver Appbar ----------------------------------------------- */
final sliverAppBar = SliverAppBar(
pinned: true,
expandedHeight: MediaQuery.of(context).size.height * 0.2,
backgroundColor: Theme.of(context).primaryColor,
flexibleSpace: FlexibleSpaceBar(
// Searchbar
background: Column(
children: <Widget>[
appBar,
],
),
// Page Title
title: Text(
AppStrings.accountTitle,
style: Theme.of(context).textTheme.title.copyWith(
color: CustomColors().novaWhite,
),
),
),
// Action Buttons
actions: sliverActionsHidden ? actionButtons : <Widget>[],
);
/* Page Content ----------------------------------------------- */
final bodyContent = SafeArea(
bottom: false,
child: CustomScrollView(
controller: _controller,
slivers: <Widget>[
sliverAppBar,
SliverToBoxAdapter(
child: Container(
height: MediaQuery.of(context).size.height,
padding: EdgeInsets.all(Constants.paddingXY_32),
decoration: BoxDecoration(
color: Theme.of(context).scaffoldBackgroundColor,
),
// Page content begins here...
child: Text(searchQuery.toString()),
),
),
],
),
);
/* Scaffold ----------------------------------------------- */
return Scaffold(
body: bodyContent,
backgroundColor: Theme.of(context).primaryColor,
);
}
}
You can check if the maxScrollExtent value of the scroll position is reached :
void _scrollListener() {
if (_controller.offset == _controller.position.maxScrollExtent) {
setState(() {
sliverActionsHidden = true;
print('Reach the top');
});
} else {
setState(() {
sliverActionsHidden = false;
print('Reach the bottom');
});
}
}

Preserve Widget State in PageView while enabling Navigation

I have a rather complex situation in a Flutter App.
I have a Home screen that is a swipable PageView,that displays 3 child Widgets : Calendar, Messages, Profile.
My issue at the moment is with the Calendar Widget. It is populated dynamically from the initState() method.
I managed to fix a first issue that came from swiping from one page to another that caused rebuilding the Calendar Widget every time.
My issue now is when I tap an item in the Calendar list, I open the detail view. Then, when I close it… all is still OK. However, when I swipe again the initState() method is called once more and the List view is rebuilt. I would like to prevent that and preserve it's state. any suggestions ?
Here is the Home code.
class HomeStack extends StatefulWidget {
final pages = <HomePages> [
CalendarScreen(),
MessagesScreen(),
ProfileScreen(),
];
#override
_HomeStackState createState() => _HomeStackState();
}
class _HomeStackState extends State<HomeStack> with AutomaticKeepAliveClientMixin<HomeStack> {
User user;
#override
bool get wantKeepAlive{
return true;
}
#override
void initState() {
print("Init home");
_getUser();
super.initState();
}
void _getUser() async {
User _user = await HomeBloc.getUserProfile();
setState(() {
user = _user;
});
}
final PageController _pageController = PageController();
int _selectedIndex = 0;
void _onPageChanged(int index) {
_selectedIndex = index;
}
void _navigationTapped(int index) {
_pageController.animateToPage(
index,
duration: const Duration(milliseconds: 300),
curve: Curves.ease
);
}
GestureDetector _navBarItem({int pageIndex, IconData iconName, String title}) {
return GestureDetector(
child: HomeAppBarTitleItem(
index: pageIndex,
icon: iconName,
title: title,
controller: _pageController
),
onTap: () => _navigationTapped(pageIndex),
);
}
Widget _buildWidget() {
if (user == null) {
return Center(
child: ProgressHud(imageSize: 70.0, progressSize: 70.0, strokeWidth: 5.0),
);
} else {
return Scaffold(
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_navBarItem(
pageIndex: 0,
iconName: Icons.calendar_today,
title: AppLocalizations.of(context).calendarViewTitle,
),
_navBarItem(
pageIndex: 1,
iconName: Icons.message,
title: AppLocalizations.of(context).messagesViewTitle,
),
_navBarItem(
pageIndex: 2,
iconName: Icons.face,
title: AppLocalizations.of(context).profileViewTitle,
),
],
),
backgroundColor: Colors.transparent,
elevation: 0.0,
),
backgroundColor: Colors.transparent,
body: PageView(
onPageChanged: (index) => _onPageChanged(index),
controller: _pageController,
children: widget.pages,
),
floatingActionButton: widget.pages.elementAt(_selectedIndex).fabButton,
);
}
}
#override
Widget build(BuildContext context) {
return WillPopScope(
child: Stack(
children: <Widget>[
BackgroundGradient(),
_buildWidget(),
],
),
onWillPop: () async {
return true;
},
);
}
}
And the Calendar code.
class CalendarScreen extends StatelessWidget implements HomePages {
/// TODO: Prevent reloading
/// when :
/// 1) push detail view
/// 2) swipe pageView
/// 3) come back to calendar it reloads
static const String routeName = "/calendar";
static Color borderColor(EventPresence status) {
switch (status) {
case EventPresence.present:
return CompanyColors.grass;
case EventPresence.absent:
return CompanyColors.asher;
case EventPresence.pending:
return CompanyColors.asher;
default:
return CompanyColors.asher;
}
}
final FloatingActionButton fabButton = FloatingActionButton(
onPressed: () {}, /// TODO: Add action to action button
backgroundColor: CompanyColors.sky,
foregroundColor: CompanyColors.snow,
child: Icon(Icons.add),
);
#override
Widget build(BuildContext context) {
return CalendarProvider(
child: CalendarList(),
);
}
}
class CalendarList extends StatefulWidget {
#override
_CalendarListState createState() => _CalendarListState();
}
class _CalendarListState extends State<CalendarList> with AutomaticKeepAliveClientMixin<CalendarList> {
Events events;
void _getEvents() async {
Events _events = await CalendarBloc.getAllEvents();
setState(() {
events = _events;
});
}
#override
void initState() {
_getEvents();
super.initState();
}
#override
bool get wantKeepAlive{
return true;
}
Widget _displayBody() {
if (events == null) {
return ProgressHud(imageSize: 30.0, progressSize: 40.0, strokeWidth: 3.0);
} else if(events.future.length == 0 && events.past.length == 0) return _emptyStateView();
return EventsListView(events: events);
}
#override
Widget build(BuildContext context) {
return _displayBody();
}
Widget _emptyStateView() {
return Center(
child: Text("No data"),
);
}
}
class EventsListView extends StatefulWidget {
final Events events;
EventsListView({this.events});
#override
_EventsListViewState createState() => _EventsListViewState();
}
class _EventsListViewState extends State<EventsListView> {
GlobalKey _pastEventsScrollViewKey = GlobalKey();
GlobalKey _scrollViewKey = GlobalKey();
double _opacity = 0.0;
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
RenderSliverList renderSliver = _pastEventsScrollViewKey.currentContext.findRenderObject();
setState(() {
CustomScrollView scrollView = _scrollViewKey.currentContext.widget;
scrollView.controller.jumpTo(renderSliver.geometry.scrollExtent);
_opacity = 1.0;
});
});
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: AnimatedOpacity(
opacity: _opacity,
duration: Duration(milliseconds: 300),
child: CustomScrollView(
key: _scrollViewKey,
controller: ScrollController(
//initialScrollOffset: initialScrollOffset,
keepScrollOffset: true,
),
slivers: <Widget>[
SliverList(
key: _pastEventsScrollViewKey,
delegate: SliverChildBuilderDelegate( (context, index) {
Event event = widget.events.past[index];
switch (event.type) {
case EventType.competition:
return CompetitionListItem(event: event);
case EventType.training:
return TrainingListItem(event: event);
case EventType.event:
return EventListItem(event: event);
}
},
childCount: widget.events.past.length,
),
),
SliverList(
delegate: SliverChildBuilderDelegate( (context, index) {
return Padding(
padding: EdgeInsets.only(top: 32.0, left: 16.0, right: 16.0, bottom: 16.0),
child: Text(
DateFormat.MMMMEEEEd().format(DateTime.now()),
style: Theme.of(context).textTheme.body2.copyWith(
color: CompanyColors.snow,
),
),
);
},
childCount: 1,
),
),
SliverList(
delegate: SliverChildBuilderDelegate( (context, index) {
Event event = widget.events.future[index];
switch (event.type) {
case EventType.competition:
return CompetitionListItem(event: event);
case EventType.training:
return TrainingListItem(event: event);
case EventType.event:
return EventListItem(event: event);
}
},
childCount: widget.events.future.length,
),
),
],
),
),
);
}
}
From the documentation on AutomaticKeepAliveClientMixin:
/// A mixin with convenience methods for clients of
[AutomaticKeepAlive]. Used with [State] subclasses.
/// Subclasses must implement [wantKeepAlive], and their [build]
methods must call super.build (the return value will always return
null, and should be ignored).
So in your code, before you return the Scaffold just call super.build:
Widget build(BuildContext context) {
super.build(context);
return Scaffold(...);
}