Search code examples
flutterflutter-layouttabbar

Dynamic number of tabs


I have design like above , my tabbar is inside BottomNavigationBar and i want place the tabbar in my BottomNavigationBar menu. The problem is , i don't want have nested scaffold and i don't want use TabController because my tabs is dynamic , tabs length can be decrease or increase depending of user adding it. If i use TabController and define it inside initState , the tabs can't be increase/decrease because in initState only once define the value.

How can i do this ?

My expectation similiar in below example , when i adding tab and tabMenu it will increase . But get this error

The following assertion was thrown building TabBar(dirty, dependencies: [_InheritedTheme,
_LocalizationsScope-[GlobalKey#a56a4]], state: _TabBarState#fb337):
Controller's length property (2) does not match the number of tabs (3) present in TabBar's tabs
property.
The relevant error-causing widget was:
  TabBar

Dummy Example


class TaskScreen extends StatefulWidget {
  @override
  _TaskScreenState createState() => _TaskScreenState();
}

class _TaskScreenState extends State<TaskScreen> with SingleTickerProviderStateMixin {
  TabController _tabController;

  final List<Tab> _tab = [
    Tab(text: 'Sunday'),
    Tab(text: 'Monday'),
  ];
  final List<Widget> _tabMenu = [
    Text('This is Sunday'),
    Text('This is Monday'),
  ];

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

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        FlatButton(
            onPressed: () {
              setState(() {
                _tab.add(Tab(
                  text: '1',
                ));
                _tabMenu.add(Text('1'));
              });
              print('tab ${_tab.length}');
              print('tabMenu ${_tabMenu.length}');
            },
            child: Text('add')),
        Container(
          color: colorPallete.primaryColor,
          padding: EdgeInsets.only(top: sizes.statusBarHeight(context)),
          child: TabBar(
            tabs: _tab.map((e) => e).toList(),
            controller: _tabController,
          ),
        ),
        Expanded(
          child: TabBarView(
            children: _tabMenu.map((e) => e).toList(),
            controller: _tabController,
          ),
        )
      ],
    );
  }
}

Solution

  • use TickerProviderStateMixin instead of SingleTickerProviderStateMixin and change TabController on changing tabs number;

    add isScrollable: true when you don't have space for all your tabs

    import 'package:flutter/material.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
          ),
          home: Scaffold(
            body: SafeArea(
              child: MyHomePage(),
            ),
          ),
        );
      }
    }
    
    class MyHomePage extends StatefulWidget {
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      @override
      Widget build(BuildContext context) {
        return TaskScreen();
      }
    }
    
    class TaskScreen extends StatefulWidget {
      @override
      _TaskScreenState createState() => _TaskScreenState();
    }
    
    class _TaskScreenState extends State<TaskScreen> with TickerProviderStateMixin {
      TabController _tabController;
    
      final List<Tab> _tab = [
        Tab(text: 'Sunday'),
        Tab(text: 'Monday'),
      ];
      final List<Widget> _tabMenu = [
        Text('This is Sunday'),
        Text('This is Monday'),
      ];
    
      @override
      void initState() {
        super.initState();
        _tabController = TabController(length: _tab.length, vsync: this);
      }
    
    
      @override
      void dispose() {
        _tabController.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            FlatButton(
                onPressed: () {
                  setState(() {
                    _tab.add(
                      Tab(
                        text: '1',
                      ),
                    );
                    _tabMenu.add(
                      Text('1'),
                    );
                    _tabController.dispose();
                    _tabController =
                        TabController(length: _tab.length, vsync: this);
                  });
                  print('tab ${_tab.length}');
                  print('tabMenu ${_tabMenu.length}');
                },
                child: Text('add')),
            Container(
              color: Colors.blue,
              padding: EdgeInsets.only(top: 10),
              child: TabBar(
                // isScrollable: true, 
                tabs: _tab.map((e) => e).toList(),
                controller: _tabController,
              ),
            ),
            Expanded(
              child: TabBarView(
                children: _tabMenu.map((e) => e).toList(),
                controller: _tabController,
              ),
            )
          ],
        );
      }
    }