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,
...
Related
I'm new at flutter and I have been searching for good results in pagination.
Pagination in flutter listview is a way to load the data when you reach the end of the list.
The pagination is used to load the data in part-wise.
Pagination in flutter listview divides the data in page manner like page 1 and page.
Need to load the list of data 10 items on each page
input:
Implement pagination
output:
Image:
https://i.stack.imgur.com/QKHc2.png
You can use loadmore package.
body: RefreshIndicator(
child: LoadMore(
isFinish: count >= 10,
onLoadMore: _loadMore,
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return Container(
child: Text(list[index].toString()),
alignment: Alignment.center,
);
},
itemCount: count,
),
whenEmptyLoad: false,
delegate: DefaultLoadMoreDelegate(),
textBuilder: DefaultLoadMoreTextBuilder.chinese,
),
onRefresh: _refresh,
),
You can use Flutter's NotificationListener<ScrollNotification>(...) widget.
Sample:
import 'package:flutter/material.dart';
class PaginatedListView extends StatefulWidget {
const PaginatedListView({
required this.onNext,
this.nextPageRatio = 1,
this.hasNextPage = false,
required this.child,
this.loadingIndicator,
super.key,
}) : assert(
nextPageRatio >= 0 && nextPageRatio <= 1,
'[nextPageRatio] should be between 0...1',
);
final VoidCallback onNext;
final double nextPageRatio;
final bool hasNextPage;
final SliverList child;
final Widget? loadingIndicator;
#override
State<PaginatedListView> createState() => _PaginatedListViewState();
}
class _PaginatedListViewState extends State<PaginatedListView> {
final ScrollController _controller = ScrollController();
double _currentMaxScrollExtent = 0;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_controller.position.maxScrollExtent == 0 && widget.hasNextPage) {
widget.onNext();
}
});
}
bool _onNotification(ScrollNotification notification) {
if (!widget.hasNextPage) {
return false;
}
final double maxScrollExtent =
notification.metrics.maxScrollExtent * widget.nextPageRatio;
if (notification.metrics.pixels >= maxScrollExtent &&
_currentMaxScrollExtent < maxScrollExtent) {
_currentMaxScrollExtent = maxScrollExtent;
widget.onNext();
}
return false;
}
#override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: _onNotification,
child: CustomScrollView(
controller: _controller,
physics: const AlwaysScrollableScrollPhysics(),
slivers: <Widget>[
widget.child,
if (widget.hasNextPage)
SliverToBoxAdapter(
child: Center(
child: widget.loadingIndicator ??
const CircularProgressIndicator(),
),
),
],
),
);
}
}
You can use the Pull To Refresh package for pagination in Flutter.
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
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;
}
I'm new to Flutter Redux, I got a problem and I have no idea how to deal with it at all! I extracted the main code to keep this simple - tap indicators to switch PageView, scroll PageView to synchronise the indicator. Here is my code:
app state:
class AppState {
final List menuList;
final int currentIndex;
AppState({this.menuList, this.currentIndex});
}
the reducers:
AppState appReducer(AppState state, Object action) {
return AppState(
menuList: menuListReducer(state.menuList, action),
currentIndex: currentIndexReducer(state.currentIndex, action));
}
final menuListReducer = combineReducers<List>(
[TypedReducer<List, SetMenuListAction>(_setMenuList)]);
List _setMenuList(List menuList, SetMenuListAction action) {
menuList = action.menuList;
return menuList;
}
final currentIndexReducer = combineReducers<int>(
[TypedReducer<int, SetCurrentIndexAction>(_setCurrentIndex)]);
int _setCurrentIndex(int currentIndex, SetCurrentIndexAction action) {
currentIndex = action.index;
return currentIndex;
}
the action:
class SetMenuListAction {
List menuList;
SetMenuListAction(this.menuList);
}
class SetCurrentIndexAction {
int index;
SetCurrentIndexAction(this.index);
}
the main logic:
void main() {
final store = Store<AppState>(
appReducer,
initialState: AppState(menuList: [
{
'picUrl': 'http://pic3.16pic.com/00/55/42/16pic_5542988_b.jpg',
'description': 'this is the first image'
},
{
'picUrl': 'http://photo.16pic.com/00/38/88/16pic_3888084_b.jpg',
'description': 'this is the second image'
},
{
'picUrl':
'http://img4.imgtn.bdimg.com/it/u=3434394339,2114652299&fm=214&gp=0.jpg',
'description': 'this is the third image'
},
{
'picUrl': 'http://pic1.win4000.com/pic/2/07/8c57e143b1.jpg',
'description': 'this is the fourth image'
},
], currentIndex: 0),
);
runApp(App(
store: store,
));
}
// App
class App extends StatelessWidget {
final Store<AppState> store;
const App({Key key, this.store}) : super(key: key);
#override
Widget build(BuildContext context) {
return StoreProvider(
store: store,
child: MaterialApp(title: 'Flutter redux example', home: MyDetail()),
);
}
}
class MyDetail extends StatefulWidget {
#override
_MyDetailState createState() => _MyDetailState();
}
class _MyDetailState extends State<MyDetail> with TickerProviderStateMixin {
PageController _controller;
#override
void initState() {
_controller = PageController(initialPage: 0);
super.initState();
}
#override
Widget build(BuildContext context) {
return StoreConnector<AppState, int>(
converter: (store) => store.state.currentIndex,
onDidChange: (newIdx) {
//this won't work because the _controller hasn't been attached to PageView
_controller.jumpToPage(newIdx);
},
builder: (BuildContext context, int idx) {
return StoreConnector<AppState, List>(
converter: (store) => store.state.menuList,
onDidChange: (newList) {
//maybe do something further
},
builder: (BuildContext context, List menus) {
return Container(
color: Colors.white,
child: Column(
children: <Widget>[
//pageview
Expanded(
child: PageView(
children: menus.map((item) {
return Column(
children: <Widget>[
Image.network(item['picUrl']),
Text(
item['description'],
style: TextStyle(fontSize: 24.0),
)
],
);
}).toList(),
onPageChanged: (int index) {
StoreProvider.of<AppState>(context)
.dispatch(SetCurrentIndexAction(index));
},
physics: BouncingScrollPhysics(),
),
),
//indicators
Container(
margin: EdgeInsets.only(bottom: 50.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: menus
.asMap()
.map((i, item) => MapEntry(
i,
GestureDetector(
onTap: () {
//this won't work either maybe because the widgets is rebuilding
_controller.jumpToPage(i);
StoreProvider.of<AppState>(context)
.dispatch(SetCurrentIndexAction(i));
},
child: Container(
width: 10.0,
height: 10.0,
color: i == idx
? Colors.purpleAccent
: Colors.blue,
margin: EdgeInsets.only(right: 10.0),
),
)))
.values
.toList(),
),
)
],
),
);
},
);
},
);
}
}
Sorry for the long code, but I think maybe this can help to understand my problem:
When I tap the indicator, I want to synchronise the PageView, that is _controller.jumpToPage(i), but it will show Errors. So how to make this work?
I can change the currentIndex in another screen, how to synchronise the PageView?
Is there any method to watch the state changes(separately, not the whole state) and do something?
After debugging your code I found that you are missing controller: _controller in PageView, this should fix it:
Expanded(
child: PageView(
controller: _controller,
children: menus.map((item) {
return Column(
children: <Widget>[
Image.network(item['picUrl']),
Text(
item['description'],
style: TextStyle(fontSize: 24.0),
)
],
);
}).toList(),
onPageChanged: (int index) {
StoreProvider.of<AppState>(context)
.dispatch(SetCurrentIndexAction(index));
},
physics: BouncingScrollPhysics(),
),
),
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(...);
}