Home > Enterprise >  Custom Tab bar in flutter
Custom Tab bar in flutter

Time:12-25

I need to implement a sticky tab bar. The same tab bar can be of variable length, as different merchants can have different number of categories.

My current code for merchant page is as:

class _MerchantPageState extends State<MerchantPage>
    with TickerProviderStateMixin {
  ScrollController _scrollController = ScrollController();
  double _scrollPosition = 0;
  late TabController _tabController;
  _scrollListener() {
    setState(() {
      _scrollPosition = _scrollController.position.pixels;
    });
  }

  GridContainerData? _products;
  GridContainerData _getProductData() {
    return productGridContainerData;
  }

  @override
  void initState() {
    super.initState();
    _products = _getProductData();
    _scrollController.addListener(_scrollListener);
    _tabController = TabController(length: 2, vsync: this);
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        body: DefaultTabController(
          length: 2,
          child: NestedScrollView(
            controller: _scrollController,
            headerSliverBuilder:
                (BuildContext context, bool innerBoxIsScrolled) {
              return [
                MerchantSliverAppbar(
                  merchant: widget.merchant!,
                  tabController: _tabController,
                ),
              ];
            },
            body: Column(
              children: [
                TabBar(
                  controller: _tabController,
                  unselectedLabelColor: Colors.redAccent,
                  indicatorSize: TabBarIndicatorSize.tab,
                  indicator: BoxDecoration(
                    gradient: LinearGradient(
                      colors: [Colors.redAccent, Colors.orangeAccent],
                    ),
                    borderRadius: BorderRadius.circular(50),
                    color: Colors.redAccent,
                  ),
                  isScrollable: true,
                  tabs: [
                    Tab(
                      child: Align(
                        alignment: Alignment.center,
                        child: Text("Category 1"),
                      ),
                    ),
                    Tab(
                      child: Align(
                        alignment: Alignment.center,
                        child: Text("Category 2"),
                      ),
                    ),
                  ],
                ),
                Expanded(
                  child: TabBarView(
                    controller: _tabController,
                    children: [
                      _buildProductFeed(),
                      _buildProductFeed(),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
        floatingActionButton: ChatButton(),
      ),
    );
  }

  Widget _buildProductFeed() {
    return Container(
      padding: EdgeInsets.all(10),
      child: GridContainer(
        data: _products,
        isScrollable: true,
      ),
    );
  }
}

I cannot implement the Tab bar in my MerchantSliverAppbar because it has a background image an implementing a Tabbar in the bottom: of the MerchantSliverAppbar puts the tab bar on top of the image.

Here is the full code for MerchantSliverAppbar.

My one solution is:

            SliverPersistentHeader(
              delegate: _SliverAppBarDelegate(
                TabBar(
                  controller: _tabController,
                  unselectedLabelColor: Colors.redAccent,
                  indicatorSize: TabBarIndicatorSize.tab,
                  indicator: BoxDecoration(
                    gradient: LinearGradient(
                      colors: [Colors.redAccent, Colors.orangeAccent],
                    ),
                    borderRadius: BorderRadius.circular(50),
                    color: Colors.redAccent,
                  ),
                  physics: NeverScrollableScrollPhysics(),
                  isScrollable: true,
                  tabs: [
                    Tab(
                      child: Align(
                        alignment: Alignment.center,
                        child: Text("Category 1"),
                      ),
                    ),
                    Tab(
                      child: Align(
                        alignment: Alignment.center,
                        child: Text("Category 2"),
                      ),
                    ),
                  ],
                ),
              ),
              pinned: true,
            ),

And in the same file:

class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
  _SliverAppBarDelegate(this._tabBar);

  final TabBar _tabBar;

  @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(
      child: _tabBar,
    );
  }

  @override
  bool shouldRebuild(_SliverAppBarDelegate oldDelegate) {
    return false;
  }
}

The issue with this implementation is:

  1. I cannot use the same app bar in different pages. I need to make it modular.

How can I solve the issue?

CodePudding user response:

As it turns out I didn't need any of those. Since I am using a CustomSliverAppbar called MerchantSliverAppbar I wrapped it with a SliverOverlapAbsorber in the following way:

  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
        backgroundColor: CustomColors.white,
        body: NestedScrollView(
          controller: _scrollController,
          headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
            return [
              SliverOverlapAbsorber(
                handle:
                    NestedScrollView.sliverOverlapAbsorberHandleFor(context),
                sliver: SliverSafeArea(
                  sliver: MerchantSliverAppbar(merchant: widget.merchant!),
                ),
              )
            ];
          },
          body: FeedSliderContainer(),
        ),
      ),
    );
  }

Notice the:

SliverOverlapAbsorber(
   handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
   sliver: SliverSafeArea(
      sliver: MerchantSliverAppbar(merchant: widget.merchant!),
   ),
)

According to the docs:

A sliver that wraps another, forcing its layout extent to be treated as overlap.

The rest was pretty similar to building a normal TabBar, just create a new stateful widget and put in the following:

  List<String> categories = ['All', 'Women', 'Men', 'Kids', 'Gadgets', 'Shoes'];

  late TabController _tabController;


  @override
  void initState() {
    super.initState();
    _tabController = TabController(vsync: this, length: categories.length);
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Builder(
        builder: (BuildContext context) {
          _tabController.addListener(() {
            if (!_tabController.indexIsChanging) {
              int index = _tabController.index;
              setState(() {});
            }
          });
          return Column(
            children: <Widget>[
              getTabBar(context, _tabController),
              Expanded(
                child: Container(
                  child: TabBarView(
                    controller: _tabController,
                    children: [
                      ... data you wanna show
                    ],
                  ),
                ),
              ),
            ],
          );
        },
      ),
    );
  }

  List<Widget> getTabs(TabController tabController) {
    return categories.map((category) {
      return Tab(
        ..customize the tabs
      );
    }).toList();
  }

  Widget getTabBar(BuildContext context, TabController tabController) {
    return TabBar(
      tabs: getTabs(tabController),
      labelColor: CustomColors.primary,
      unselectedLabelColor: CustomColors.secondary.withOpacity(0.5),
      indicatorColor: Colors.transparent,
      isScrollable: true,
      padding: const EdgeInsets.all(3),
      controller: tabController,
    );
  } 
}
  • Related