Reputation: 113
I'm trying to display tabs for each main tabs (Nested Tab Bar).
I have a course page that show the information for this course in SliverAppBar()
. Each course has many sections and each section has many exams.
This is my Build method:
@override
Widget build(BuildContext context) {
double height = MediaQuery.of(context).size.height;
double statusBarHeight =
MediaQuery.of(context).padding.top + 56; // 56 is height of Appbar.
return Scaffold(
body: Container(
child: DefaultTabController(
length: _sections.length,
child: NestedScrollView(
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return [
SliverAppBar(
elevation: 0,
title: Text(
widget._course.shortName +
' ' +
widget._course.code.toString(),
style: TextStyle(
color: Colors.white,
fontSize: 20.0,
),
),
actionsIconTheme: IconThemeData(color: widget._course.color),
expandedHeight: height / 2,
floating: true,
pinned: true,
centerTitle: true,
titleSpacing: 5,
leading: IconButton(
icon: Icon(Icons.arrow_back_ios),
tooltip: 'Back',
splashColor: Colors.transparent,
onPressed: () => Navigator.pop(context),
),
backgroundColor: widget._course.color,
flexibleSpace: Container(
padding: EdgeInsets.only(top: statusBarHeight),
child: Text('Course information will be here'),
),
),
SliverPersistentHeader(
floating: false,
delegate: _SliverAppBarDelegate(
TabBar(
indicatorSize: TabBarIndicatorSize.label,
labelPadding: EdgeInsets.symmetric(horizontal: 10),
indicator: CircleTabIndicator(
color: Colors.white,
radius: 2.5,
),
indicatorColor: Colors.white,
isScrollable: true,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
unselectedLabelStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.w300),
labelStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
tabs: List<Widget>.generate(
_sections.length,
(int index) {
return customTab(_sections[index].id);
},
),
),
widget._course.color,
),
pinned: false,
),
];
},
body: Center(
child: getTabBarConten(),
),
),
),
),
);
}
I get _SliverAppBarDelegate()
class from the internet to handle the TabBar()
in the NestedScrollView()
, this is the code:
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate(this._tabBar, this._color);
TabBar _tabBar;
final Color _color;
@override
double get minExtent => _tabBar.preferredSize.height;
@override
double get maxExtent => _tabBar.preferredSize.height;
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return new Container(
color: _color,
alignment: Alignment.center,
child: _tabBar,
);
}
@override
bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
return false;
}
}
Now each section tab content that I created it in Build method it's have many exams tabs. getTabBarConten()
method:
Widget getTabBarConten() {
return TabBarView(
children: List<Widget>.generate(
_sections.length,
(int index) {
return CustomTabView(
color: widget._course.color,
initPosition: initPosition,
itemCount: _sections[index].exams.length,
tabBuilder: (context, index) => customTab(
_sections[index].exams[index].type.toString(),
isSection: true,
),
pageBuilder: (context, index) => Center(
child: Text(_sections[index].exams[index].supervisor +
' ' +
_sections[index].instructor)),
onPositionChange: (index) {
setState(() {
initPosition = index;
});
},
);
},
),
);
}
This getTabBarConten()
method it's for sections tabs content and it's return CustomTabView
that return tabs for each exam.
The problem is:
RangeError (index): Invalid value: Not in range 0..1, inclusive: 2
In this example the course has 2 sections and each section has 3 exams. So the itemCount
in CustomTabView
is 3 and the length of sections is 2 that is the error come from.
If I set the itemCount
to same length of sections it's work fine (even if the itemCount
is less than the length of sections):
But if the itemCount
is greater than the length of sections its not working!
Why this error happened , I mean there is no relation between them, in getTabBarConten()
method it's return TabBarView()
for the section tabs and for each tab it's return CustomTabView()
that return tab for each exam.
So, Why this this error and can anyone help me? please :(
Upvotes: 2
Views: 1888
Reputation: 113
Thanks for chunhunghan his answer in this question is helped me. It's another way but it's work..
[Update: Sep] I will try to write my code for the same example. Maybe it will help someone :)
Here the code:
import 'package:flutter/material.dart';
class CustomTabView extends StatefulWidget {
final int itemCount;
final IndexedWidgetBuilder tabBuilder;
final IndexedWidgetBuilder pageBuilder;
final Widget stub;
final ValueChanged<int> onPositionChange;
final ValueChanged<double> onScroll;
final int initPosition;
final Color color;
final bool isExamTabs;
final TabController controller;
CustomTabView({
@required this.itemCount,
@required this.tabBuilder,
@required this.pageBuilder,
this.stub,
this.onPositionChange,
this.onScroll,
this.initPosition,
this.color,
this.isExamTabs = false,
this.controller,
});
@override
_CustomTabsState createState() => _CustomTabsState();
}
class _CustomTabsState extends State<CustomTabView>
with TickerProviderStateMixin {
TabController controller;
int _currentCount;
int _currentPosition;
@override
void initState() {
if (widget.controller == null) {
_currentPosition = widget.initPosition ?? 0;
controller = TabController(
length: widget.itemCount,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation.addListener(onScroll);
_currentCount = widget.itemCount;
} else {
controller = widget.controller;
}
super.initState();
}
@override
void didUpdateWidget(CustomTabView oldWidget) {
if (_currentCount != widget.itemCount) {
controller.animation.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
if (widget.initPosition != null) {
_currentPosition = widget.initPosition;
}
if (_currentPosition > widget.itemCount - 1) {
_currentPosition = widget.itemCount - 1;
_currentPosition = _currentPosition < 0 ? 0 : _currentPosition;
if (widget.onPositionChange is ValueChanged<int>) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
widget.onPositionChange(_currentPosition);
}
});
}
}
_currentCount = widget.itemCount;
setState(() {
controller = TabController(
length: widget.itemCount,
vsync: this,
initialIndex: _currentPosition,
);
controller.addListener(onPositionChange);
controller.animation.addListener(onScroll);
});
} else if (widget.initPosition != null) {
controller.animateTo(widget.initPosition);
}
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
controller.animation.removeListener(onScroll);
controller.removeListener(onPositionChange);
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (widget.itemCount < 1) return widget.stub ?? Container();
double height = MediaQuery.of(context).size.height;
return Container(
height: height - 100,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
color: widget.color,
alignment: Alignment.center,
child: widget.isExamTabs
? TabBar(
controller: controller,
indicatorSize: TabBarIndicatorSize.label,
indicatorWeight: 3.5,
indicatorColor: Colors.white,
isScrollable: true,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
unselectedLabelStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.w300),
labelStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
tabs: List.generate(
widget.itemCount,
(index) => widget.tabBuilder(context, index),
),
)
: TabBar(
controller: controller,
indicatorSize: TabBarIndicatorSize.label,
labelPadding: EdgeInsets.symmetric(horizontal: 10),
indicatorColor: Colors.white,
isScrollable: true,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
unselectedLabelStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.w300),
labelStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
tabs: List.generate(
widget.itemCount,
(index) => widget.tabBuilder(context, index),
),
),
),
Expanded(
child: TabBarView(
controller: controller,
children: List.generate(
widget.itemCount,
(index) => widget.pageBuilder(context, index),
),
),
),
],
),
);
}
onPositionChange() {
if (!controller.indexIsChanging) {
_currentPosition = controller.index;
if (widget.onPositionChange is ValueChanged<int>) {
widget.onPositionChange(_currentPosition);
}
}
}
onScroll() {
if (widget.onScroll is ValueChanged<double>) {
widget.onScroll(controller.animation.value);
}
}
}
I called it in my course page's body: (Please see the comments in the code)
CustomTabView(
initPosition: 0,
itemCount: _course.sections.length,
tabBuilder: (context, index) =>
secionTab(_course.sections[index].id), // Sections tabs
pageBuilder: (context, index) => getSectionTabBarConten(index), // Content for each section. To show exams for "_course.sections[index]" call inside it "CustomTabView()" again for exams. It's mean all Exams per secion.
onPositionChange: (index) {},
// onScroll: (position) => print("POS : " + '$position'),
color: _course.getColor(),
)
Upvotes: 2