make scrollable list tabview in flutter - flutter

I want to make a scrollable tablist view. The length of the tab bar view list is not fixed. I want to change the select tab while scrolling the list item and the tab should also work on tap.
The code gives the exception message:
Exception has occurred.
_AssertionError ('package:flutter/src/material/tab_controller.dart': Failed assertion: line 181 pos 12: 'value >= 0 && (value < length || length == 0)': is not true)
while scroll reach to the last of list and ontap the tab is not scroll to its list.
Here is my code:
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
late TabController _controller;
late ScrollController _scrollController;
List<Map<String, List<String>>> _tablist = \[
{ "tab1": \[ "Tab11", "tab12", "tab13", "tab14", \]
},
{
"tab2": \[ "Tab21", "tab22", "tab23","tab24", \]
},
{ "tab3": \[ "Tab31", "tab32", \]
},
{
"tab4": \[ "Tab41", "tab42", "tab43","tab44", \]
},
\];
#override
void initState() {
super.initState();
_controller = TabController(vsync: this, length: _tablist.length);
_scrollController = ScrollController();
_scrollController.addListener(() {
int index = (_scrollController.offset /
(_scrollController.position.maxScrollExtent / _tablist.length))
.floor();
_controller.animateTo(index);
});
}
#override
void dispose() {
_controller.dispose();
_scrollController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Food Menu"),
bottom: TabBar(
controller: _controller,
tabs: _tablist.map((item) => Tab(text: item.keys.first)).toList(),
),
),
body: ListView.builder(
controller: _scrollController,
itemCount: _tablist.length,
itemBuilder: (context, index) {
return Column(
children: <Widget>\[
Container(
color: _controller.index == index ? Colors.grey\[200\] : null,
padding: EdgeInsets.all(16),
child: Text(
_tablist\[index\].keys.first,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
),
..._tablist\[index\].values.first.map((item) => ListTile(title: Text(item))).toList()
\],
);
},
),
);
}
}
I want to make it like this:
https://drive.google.com/file/d/1ZCcAvxnnzNfJiKyQ0ZQG6LUsDxaXVqeD/view?usp=share_link

There is property named " isScrollable: " in TabBar

Related

FLUTTER: The following NoSuchMethodError was thrown building: '[]' Dynamic call of null. Receiver: null Arguments: ["data"]

I'm trying to call HTTP API to show the information of products on the Pageview.builder but it gets the error The following NoSuchMethodError was thrown building: '[]' Dynamic call of null. Receiver: null Arguments: ["data"], I've tried many ways to solve it but it still get an error and can't display the data of HTTP API on app screen
Anyone can solve it, many thanks!
class HomePage extends StatefulWidget {
const HomePage({super.key});
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<Information> postList = [];
int currentIndex = 0;
late PageController _controller;
#override
void initState() {
_controller = PageController(initialPage: 0);
super.initState();
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
var data;
Future<void> getData() async {
final response = await http
.get(Uri.parse('https://berequirement.herokuapp.com/products'));
if (response.statusCode == 200) {
data = jsonDecode(response.body.toString());
} else {}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: [
Expanded(
child: Container(
height: 500,
width: 300,
child: FutureBuilder(
future: getData(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: Text('Loading ...'));
} else {
return PageView.builder(
controller: _controller,
itemCount: data.length,
onPageChanged: (int index) {
setState(() {
currentIndex = index;
});
},
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.all(40),
child: Column(
children: [
Text(
data[index]['data']['name'],
style: GoogleFonts.poppins(
fontWeight: FontWeight.bold,
fontSize: 22,
),
),
const SizedBox(height: 20),
Text(
data[index]['data']['code'],
textAlign: TextAlign.center,
style: GoogleFonts.poppins(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Colors.grey),
)
],
));
},
);
}
}),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(
postList.length,
(index) => buildDot(index, context),
),
),
])}
The problem might be located around here:
Text(
data[index]['data']['name'],
...
)
I've tried sending a GET request to your URL and the following was given:
{
"success": true,
"message": "Products found",
"data": [
{
"name": "Shadow Witch",
...
},
{
"name": "Sadness Mummy",
...
},
{
"name": "Candy Witch",
...
}
]
}
Given this, your Text() doesn't have the correct parameters. It should look like this:
Text(
data['data'][index]['name'],
...
)
Also, this might be redundant, but in your getData() you don't need to call the toString() method on response.body:
...
if (response.statusCode == 200) {
data = jsonDecode(response.body);
} else {}
...

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

How to implement listview lazyload inside a NestedScrollView?

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;
}

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

How to watch state changes in flutter redux

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(),
),
),