John
John

Reputation: 363

Dynamically Tabbar Flutter

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

Upvotes: 1

Views: 3063

Answers (2)

moldstadt
moldstadt

Reputation: 362

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

Upvotes: 0

Kothai
Kothai

Reputation: 125

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),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Upvotes: 1

Related Questions