How to create a dynamic TabBarView/ Render a new Tab with a function in Flutter?

26,112

Solution 1

Try this. Demo

To make dynamic tab you can use a List and keep appending the list on every button click.

Trick: Clear List and redraw an empty widget and again draw the widgets as per your list.

 import 'package:flutter/material.dart';
void main() {
  runApp(new MaterialApp(
    home: new CardStack(),
  ));
}

class DynamicTabContent {
  IconData icon;
  String tooTip;

  DynamicTabContent.name(this.icon, this.tooTip);
}

class CardStack extends StatefulWidget {
  @override
  _MainState createState() => new _MainState();
}

class _MainState extends State<CardStack> with TickerProviderStateMixin {
  List<DynamicTabContent> myList = new List();

  TabController _cardController;

  TabPageSelector _tabPageSelector;

  @override
  void initState() {
    super.initState();

    myList.add(new DynamicTabContent.name(Icons.favorite, "Favorited"));
    myList.add(new DynamicTabContent.name(Icons.local_pizza, "local pizza"));

    _cardController = new TabController(vsync: this, length: myList.length);
    _tabPageSelector = new TabPageSelector(controller: _cardController);
  }

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

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      backgroundColor: Colors.grey[300],
      appBar: new AppBar(
          actions: <Widget>[
            new Padding(
              padding: const EdgeInsets.all(1.0),
              child: new IconButton(
                icon: const Icon(
                  Icons.add,
                  size: 30.0,
                  color: Colors.white,
                ),
                tooltip: 'Add Tabs',
                onPressed: () {
                  List<DynamicTabContent> tempList = new List();

                  myList.forEach((dynamicContent) {
                    tempList.add(dynamicContent);
                  });

                  setState(() {
                    myList.clear();
                  });

                  if (tempList.length % 2 == 0) {
                    myList.add(new DynamicTabContent.name(Icons.shopping_cart, "shopping cart"));
                  } else {
                    myList.add(new DynamicTabContent.name(Icons.camera, "camera"));
                  }

                  tempList.forEach((dynamicContent) {
                    myList.add(dynamicContent);
                  });

                  setState(() {
                    _cardController = new TabController(vsync: this, length: myList.length);
                    _tabPageSelector = new TabPageSelector(controller: _cardController);
                  });
                },
              ),
            ),
          ],
          title: new Text("Title Here"),
          bottom: new PreferredSize(
              preferredSize: const Size.fromHeight(10.0),
              child: new Theme(
                data: Theme.of(context).copyWith(accentColor: Colors.grey),
                child: myList.isEmpty
                    ? new Container(
                        height: 30.0,
                      )
                    : new Container(
                        height: 30.0,
                        alignment: Alignment.center,
                        child: _tabPageSelector,
                      ),
              ))),
      body: new TabBarView(
        controller: _cardController,
        children: myList.isEmpty
            ? <Widget>[]
            : myList.map((dynamicContent) {
                return new Card(
                  child: new Container(
                      height: 450.0,
                      width: 300.0,
                      child: new IconButton(
                        icon: new Icon(dynamicContent.icon, size: 100.0),
                        tooltip: dynamicContent.tooTip,
                        onPressed: null,
                      )),
                );
              }).toList(),
      ),
    );
  }
}

Hope this helps :)

Solution 2

Problems arise if you need to modify the arrays. They consist in the fact that when modifying an array you do not have the opportunity to use the same controller.

enter image description here

You can use the next custom widget for this case:

import 'package:flutter/material.dart';

  void main() => runApp(MyApp());

  class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        title: 'Flutter Demo',
        home: MyHomePage(),
      );
    }
  }

  class MyHomePage extends StatefulWidget {
    @override
    _MyHomePageState createState() => _MyHomePageState();
  }

  class _MyHomePageState extends State<MyHomePage> {
    List<String> data = ['Page 0', 'Page 1', 'Page 2'];
    int initPosition = 1;

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        body: SafeArea(
          child: CustomTabView(
            initPosition: initPosition,
            itemCount: data.length,
            tabBuilder: (context, index) => Tab(text: data[index]),
            pageBuilder: (context, index) => Center(child: Text(data[index])),
            onPositionChange: (index){
              print('current position: $index');
              initPosition = index;
            },
            onScroll: (position) => print('$position'),
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            setState(() {
              data.add('Page ${data.length}');
            });
          },
          child: Icon(Icons.add),
        ),
      );
    }
  }

  /// Implementation

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

      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);
      }
    }
  }
Share:
26,112
Andy Giovanny Febrianto
Author by

Andy Giovanny Febrianto

Updated on July 09, 2022

Comments

  • Andy Giovanny Febrianto
    Andy Giovanny Febrianto almost 2 years

    So I have been learning flutter in a while and I am stuck in this. Sorry if it is a noobish question. I am currently trying to build something like a Card Tab. The information and widget will be stored in a card.

    Imagine something like Tinder, where they have multiple card stack and swipe left and right to navigate.

    I plan to create that but I cannot seems to find a way to add/render a new card with a button.

    It's like adding something to the list, Flutter will use a ListView builder where we add to the list. But there is no TabBarView builder. Is this something that is not possible to do? I try putting a list inside a tab but it's still wont be the same.

    I created some basic skeleton here to help convey my meaning. So the card will be swipe left and right and there is a button in the appBar to add card. Lenght is 2 now and I wanted the button to render the 3rd card. Is this possible?

    Thanks in advance!

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(new MaterialApp(
        home: new CardStack(),
    
      ));
    }
    
    
    class CardStack extends StatefulWidget {
      @override
      _MainState createState() => new _MainState();
    }
    
    
    class _MainState extends State<CardStack> with SingleTickerProviderStateMixin {
    
      TabController _cardController;
    
      @override
      void initState() {
        super.initState();
        _cardController = new TabController(vsync: this, length: 2);
      }
    
      @override
      void dispose() {
        _cardController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
    
        return new Scaffold(
          backgroundColor: Colors.grey[300],
          appBar: new AppBar(
              actions: <Widget>[
                new IconButton(
                  icon: const Icon(Icons.add),
                  tooltip: 'Add Tabs',
                  onPressed: null,
                ),
              ],
              title: new Text("Title Here"),
              bottom: new PreferredSize(
              preferredSize: const Size.fromHeight(20.0),
              child: new Theme(
                data: Theme.of(context).copyWith(accentColor: Colors.grey),
                child: new Container(
                  height: 50.0,
                  alignment: Alignment.center,
                  child: new TabPageSelector(controller: _cardController),
                ),
              )
            )
          ),
          body: new TabBarView(
            controller: _cardController,
            children: <Widget>[
              new Center(
                child: new Card(
                  child: new Container(
                      height: 450.0,
                      width: 300.0,
                      child: new IconButton(
                        icon: new Icon(Icons.favorite, size: 100.0),
                        tooltip: 'Favorited',
                        onPressed: null,
                      )
                  ),
                ),
              ),
              new Center(
                child: new Card(
                  child: new Container(
                      height: 450.0,
                      width: 300.0,
                      child: new IconButton(
                        icon: new Icon(Icons.local_pizza, size: 50.0,),
                        tooltip: 'Pizza',
                        onPressed: null,
                      )
                  ),
                ),
              ),
            ],
          ),
        );
      }
    }

  • rajeshzmoke
    rajeshzmoke over 5 years
    i ran this example..and i couldnt get the index of the current tab..as you are using a map to display all the tabs, i need the index to show certain components and hide some
  • Telmo Marques
    Telmo Marques over 5 years
    There's an issue with this solution, where TabPageSelector loses it's state when a new TabController is instantiated inside build(). I'm also still trying to fix this issue. Please see: webmshare.com/ZQ00d
  • Amalan Dhananjayan
    Amalan Dhananjayan about 4 years
    This is very helpful. Thanks (y)
  • VenSan
    VenSan almost 4 years
    @Yuriy Luchaninov, If I want to add the "delete page" option for tab view based on selection respective page should remove from the PageBuilder.
  • VenSan
    VenSan almost 4 years
    Please find the tab delete image link reference. ibb.co/9NPNkkZ
  • Abbas Jafari
    Abbas Jafari over 3 years
    Excellent friend
  • Sebastian Gomes
    Sebastian Gomes over 3 years
    This is gold. Much appreciated.
  • Jani
    Jani about 3 years
    This should be a package
  • Emon
    Emon almost 3 years
    It should be accepted answer and Thanks @Yuriy Luchaninov . Works like a charm.
  • Apoorv Pandey
    Apoorv Pandey almost 3 years
    Super amazing, thanks a lot this should be accepted as answer