Alyahya
Alyahya

Reputation: 113

How I can handle Nested TabBar with dynamic length in Flutter

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

See the image here

But if the itemCount is greater than the length of sections its not working!

See the error here

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

Answers (1)

Alyahya
Alyahya

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

Related Questions