1

if I wanted the user to add / remove tabs on tabbar, how can I do it dynamically ?

For now i can create a new tab but i can' t skip to the right page when create.

class _Page {
  _Page({ this.label, this.colors, this.iconup,this.iconfloatingbutton });

  final String label;
  final MaterialColor colors;
  final IconData iconfloatingbutton;
  final IconData iconup;

  Color get labelColor => colors != null ? colors.shade300 :     Colors.grey.shade300;
  bool get fabDefined => colors != null && iconfloatingbutton != null;
  Color get fabColor => colors.shade400;
  Icon get fabIcon => Icon(iconfloatingbutton);
  Key get fabKey => ValueKey<Color>(fabColor);
}

final List<_Page> _allPages = <_Page>[
  _Page(label: 'Blue', colors: Colors.indigo,iconup:Icons.text_fields,     iconfloatingbutton: Icons.add),
  _Page(label: 'Eco', colors: Colors.green,iconup:Icons.text_fields, iconfloatingbutton: Icons.create),
  _Page(label: 'No',iconup:Icons.text_fields,),
  _Page(label: 'Teal', colors: Colors.teal,iconup:Icons.text_fields, iconfloatingbutton: Icons.add),

];

class TabsFabDemo extends StatefulWidget {

  @override
  _TabsFabDemoState createState() => _TabsFabDemoState();
}

class _TabsFabDemoState extends State<TabsFabDemo> with     SingleTickerProviderStateMixin {


  int initPosition = 1;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        color: Colors.green,
        child: SafeArea(
          child: CustomTabView(
            initPosition: initPosition,
            itemCount: _allPages.length,
            tabBuilder: (context, index) => Tab(text: _allPages[index].label),
            pageBuilder: (context, index) => Container(color:Colors.white,child: Text(_allPages[index].label)),
            onPositionChange: (index){
              print('current position: $index');
            },
            onScroll: (position) => print("POS : "+'$position'),
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          addtab("a",null);
        },
        child: Icon(Icons.add),
      ),
    );
  }


  addtab(label,icon){
    setState(() {
      _allPages.add(new _Page(label: 'Blue', colors: Colors.indigo,iconup:Icons.satellite, iconfloatingbutton: Icons.add));
    });
  }


}

// --------------------- IMPLEMENTATION CUSTOM TABBAR ------------------

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;

  CustomTabView({
    @required this.itemCount,
    @required this.tabBuilder,
    @required this.pageBuilder,
    this.stub,
    this.onPositionChange,
    this.onScroll,
    this.initPosition,
  });

  @override
  _CustomTabsState createState() => _CustomTabsState();
}

class _CustomTabsState extends State<CustomTabView> with         TickerProviderStateMixin {
  TabController controller;
  int _currentCount;
  int _currentPosition;

  @override
  void initState() {
    _currentPosition = widget.initPosition ?? 0;
    print("INIT POSITION : "+widget.initPosition.toString());
    print("INIT POSITION : "+_currentPosition.toString());

    controller = TabController(
      length: widget.itemCount,
      vsync: this,
      initialIndex: _currentPosition,
    );
    controller.addListener(onPositionChange);
    controller.animation.addListener(onScroll);
    _currentCount = widget.itemCount;
    super.initState();
  }

  @override
  void didUpdateWidget(CustomTabView oldWidget) {

    if (_currentCount != widget.itemCount) {
      print("DID UPDATE POSITION : 0");

      controller.animation.removeListener(onScroll);
      controller.removeListener(onPositionChange);
      controller.dispose();

      if (widget.initPosition != null) {
        print("DID UPDATE POSITION : 1");

        _currentPosition = widget.initPosition;
      }

      if (_currentPosition > widget.itemCount - 1) {
        print("DID UPDATE POSITION : 2");

        _currentPosition = widget.itemCount - 1;
        _currentPosition = _currentPosition < 0 ? 0 :
        _currentPosition;    
        if (widget.onPositionChange is ValueChanged<int>) {
          print("DID UPDATE POSITION : 3");

          WidgetsBinding.instance.addPostFrameCallback((_){
            if(mounted) {
              print("DID UPDATE POSITION : 4");

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

    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: <Widget>[
        Container(
          alignment: Alignment.center,
          child: TabBar(
            isScrollable: true,
            controller: controller,
            labelColor: Theme.of(context).primaryColor,
            unselectedLabelColor: Theme.of(context).hintColor,
            indicator: BoxDecoration(
              border: Border(
                bottom: BorderSide(
                  color: Theme.of(context).primaryColor,
                  width: 2,
                ),
              ),
            ),
            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);
    }
  }
}

When I create a tab the scroll must point to the new generated item

When I delete a tab the scroll must point to the previous element

1
  • Look for this answer here , may help you Commented Jan 11, 2020 at 19:40

2 Answers 2

1

In order to add or delete a tab dynamically, you need to call the add or delete methods inside the setState() of your stateful Widget.

To move the controller to the newly added tab, just update the initPosition to the newly added tab index, as below. I have just changed your TabsFabDemo class as the rest of all your code is fine ( Implementation of CustomTabBar and _Page class ).

class TabsFabDemo extends StatefulWidget {
   @override
  _TabsFabDemoState createState() => _TabsFabDemoState();
}

class _TabsFabDemoState extends State<TabsFabDemo>
    with SingleTickerProviderStateMixin {
  int initPosition = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: CustomTabView(
          initPosition: initPosition,
          itemCount: data.length,
          tabBuilder: (context, index) => Tab(text: data[index].label),
          pageBuilder: (context, index) =>
              Center(child: Text(data[index].label)),
          onPositionChange: (index) {
            initPosition = index;
          },
          onScroll: (position) => print('$position'),
        ),
      ),
      floatingActionButton: Container(
        height: 130.0,
        width: 80.0,
        child: FloatingActionButton(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              IconButton(
                icon: Icon(Icons.add),
                onPressed: () {
                  setState(() {
                    data.add(_Page(
                        label: 'Blue',
                        colors: Colors.indigo,
                        iconup: Icons.satellite,
                        iconfloatingbutton: Icons.add));

                    int lastIndex = data.length;
                    initPosition = lastIndex;
                  });
                },
                padding: const EdgeInsets.all(0.0),
              ),
              IconButton(
                icon: Icon(Icons.remove),
                onPressed: () {
                  setState(() {
                    data.removeLast();
                  });
                },
                padding: const EdgeInsets.all(0.0),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
Sign up to request clarification or add additional context in comments.

2 Comments

seems to work but when I add a page it doesn't show the added page but remains on the old page
If you try to do the slide after adding a page you understand what I mean, if you help me to solve it,I mark the question as solved
0

Kothai's answer was almost there. Just had the incorrect index.

Here is the complete working solution, in case anyone finds this:

class _Page {
  _Page({
    required this.label,
    required this.colors,
    required this.iconup,
    required this.iconfloatingbutton,
  });

  final String label;
  final MaterialColor colors;
  final IconData iconfloatingbutton;
  final IconData iconup;

  Color get labelColor => colors != null ? colors.shade300 : Colors.grey.shade300;
  bool get fabDefined => colors != null && iconfloatingbutton != null;
  Color get fabColor => colors.shade400;
  Icon get fabIcon => Icon(iconfloatingbutton);
  Key get fabKey => ValueKey<Color>(fabColor);
}

final List<_Page> _allPages = <_Page>[
  _Page(label: 'Blue', colors: Colors.indigo, iconup: Icons.text_fields, iconfloatingbutton: Icons.add),
  _Page(label: 'Eco', colors: Colors.green, iconup: Icons.text_fields, iconfloatingbutton: Icons.create),
  // _Page(
  //   label: 'No',
  //   iconup: Icons.text_fields,
  // ),
  _Page(label: 'Teal', colors: Colors.teal, iconup: Icons.text_fields, iconfloatingbutton: Icons.add),
];

class TabsFabDemo extends StatefulWidget {
  @override
  _TabsFabDemoState createState() => _TabsFabDemoState();
}

class _TabsFabDemoState extends State<TabsFabDemo> with SingleTickerProviderStateMixin {
  int initPosition = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: CustomTabView(
          initPosition: initPosition,
          itemCount: _allPages.length,
          tabBuilder: (context, index) => Tab(text: _allPages[index].label),
          pageBuilder: (context, index) => Center(child: Text(_allPages[index].label)),
          onPositionChange: (index) {
            initPosition = index;
          },
          onScroll: (position) => print('$position'),
        ),
      ),
      floatingActionButton: Container(
        height: 130.0,
        width: 80.0,
        child: FloatingActionButton(
          onPressed: () {},
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              IconButton(
                icon: Icon(Icons.add),
                onPressed: () {
                  setState(() {
                    _allPages.add(_Page(label: 'Blue', colors: Colors.indigo, iconup: Icons.satellite, iconfloatingbutton: Icons.add));

                    int lastIndex = _allPages.length - 1;
                    initPosition = lastIndex;
                  });
                },
                padding: const EdgeInsets.all(0.0),
              ),
              IconButton(
                icon: Icon(Icons.remove),
                onPressed: () {
                  setState(() {
                    _allPages.removeLast();
                  });
                },
                padding: const EdgeInsets.all(0.0),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

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;

  CustomTabView({
    required this.itemCount,
    required this.tabBuilder,
    required this.pageBuilder,
    this.stub,
    required this.onPositionChange,
    required this.onScroll,
    required this.initPosition,
  });

  @override
  _CustomTabsState createState() => _CustomTabsState();
}

class _CustomTabsState extends State<CustomTabView> with TickerProviderStateMixin {
  late TabController controller;
  late int _currentCount;
  late int _currentPosition;

  @override
  void initState() {
    _currentPosition = widget.initPosition ?? 0;
    print("INIT POSITION : " + widget.initPosition.toString());
    print("INIT POSITION : " + _currentPosition.toString());

    controller = TabController(
      length: widget.itemCount,
      vsync: this,
      initialIndex: _currentPosition,
    );
    controller.addListener(onPositionChange);
    controller.animation!.addListener(onScroll);
    _currentCount = widget.itemCount;
    super.initState();
  }

  @override
  void didUpdateWidget(CustomTabView oldWidget) {
    if (_currentCount != widget.itemCount) {
      print("DID UPDATE POSITION : 0");

      controller.animation!.removeListener(onScroll);
      controller.removeListener(onPositionChange);
      controller.dispose();

      if (widget.initPosition != null) {
        print("DID UPDATE POSITION : 1");

        _currentPosition = widget.initPosition;
      }

      if (_currentPosition > widget.itemCount - 1) {
        print("DID UPDATE POSITION : 2");

        _currentPosition = widget.itemCount - 1;
        _currentPosition = _currentPosition < 0 ? 0 : _currentPosition;
        if (widget.onPositionChange is ValueChanged<int>) {
          print("DID UPDATE POSITION : 3");

          WidgetsBinding.instance!.addPostFrameCallback((_) {
            if (mounted) {
              print("DID UPDATE POSITION : 4");

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

    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: <Widget>[
        Container(
          alignment: Alignment.center,
          child: TabBar(
            isScrollable: true,
            controller: controller,
            labelColor: Theme.of(context).primaryColor,
            unselectedLabelColor: Theme.of(context).hintColor,
            indicator: BoxDecoration(
              border: Border(
                bottom: BorderSide(
                  color: Theme.of(context).primaryColor,
                  width: 2,
                ),
              ),
            ),
            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);
    }
  }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.