Pattern for updating TabController length from child widget

Viewed 7377

The main page has many children widgets. When searching, SearchWidget is shown, otherwise DashletsWidget is shown.

DashletsWidget has TabController. TabController is kept in the main page, so that active tab is not reset after searching.

DashletsWidget has a dashlet setting pane, which might change number of tabs.

DashletsWidget(ValueNotifier<int> dashletCount, TabController controller) use ValueNotifier to let re-create controller to the parent: . While re-creating, the old TabController cannot be disposed reliably, so let just de-reference without disposing. It is kind-of-work, but so un-natural. Is there a good pattern to update TabController.length.

5 Answers

EDIT:

Since March/2019 this answer is no longer valid. A pull request have included a check and now the TabController's length must match the Tab count.

So it's definitely worth trying the Salvatore's solution, but if your tabs are really dynamic and may change a lot, in my opinion the only solution now is Collin Jackson's one.

As Kyaw, I was struggling to adjust the TabController.length on demand, creating new TabControllers when the state changed (on setState, before building, on didUpdateWidget...). However, I'm not sure why that was not working.

What I'm doing (and it's working) is using the tab controller length as if it was "maximum length". So as the business logic of my app defines 20 tabs max, that's what I'm using as the length (20). I create the TabController with fixed length once at initState() and dispose it once at dispose().

Despite of that, as long the TabBar and TabBarView have their tabs and children items correctly placed, for instance 2 items in both, their UI will work as expected for 2 items.

It's working with no glitches! (that I'm aware of) :)

I found my own way of updating the TabController's length.

I initialize it in initState() with a fake length, that works only like a placeholder. When I know the right length that the TabController should have, I initialize it again, but now with the right length. Here is what I mean:

TabController _tabController;

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

    // Initialize the TabController with a fake length
    _tabController = TabController(vsync: this, length: 0);

    // Retrieve the number of tabs that will be displayed in TabBar
    _retrieveNumOfTabs().then((numOfTabs) {
      // Initialize the TabController with the right length
      _tabController = TabController(vsync: this, length: numOfTabs);

     // Add here all the controller's listeners
    });
  }

With DefaultTabController:

  List<String> tabNames = ["a", "b", "C"];

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
        length: tabNames.length,
        child: Scaffold(
          // add new tab on click
          floatingActionButton: FloatingActionButton(
              onPressed: () => setState(() {
                    tabNames.add("c");
                  })),
          appBar: AppBar(bottom: TabBar(tabs: tabNames.map((x) => Tab(text: x)).toList())),
          body: TabBarView(children: tabNames.map((x) => Container(child: Text(x))).toList()),
        ));
  }

If you are using slivers also add:

TabBar(controller: DefaultTabController.of(context), ...)

With TabController:

  List<String> tabNames = ["a", "b", "C"];
  TabController _controller;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // add new tab on click
      floatingActionButton: FloatingActionButton(
          onPressed: () => setState(() {
                tabNames.add("c");
                // might be good idea to dispose old tabController before
                _controller = TabController(vsync: this, length: tabNames.length);
              })),
      appBar: AppBar(bottom: TabBar(controller: _controller, tabs: tabNames.map((x) => Tab(text: x)).toList())),
      body: TabBarView(controller: _controller, children: tabNames.map((x) => Container(child: Text(x))).toList()),
    );
  }

I use this pattern for changing tabbar length with my items

class MyWidget extends StatefulWidget {
  const MyWidget({Key? key}) : super(key: key);

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget>
    with SingleTickerProviderStateMixin {
  bool inited = false;
  TabController? tabcontroller;
  final List<OrderTabbarWidget> tabbarWidgets = [];
  _initTabbarWidgets() async {
    setState(() {
      inited = false;
    });
    await Future.delayed(const Duration(
        milliseconds: 100)); //some time i give error if this method not exists
    final List<OrderTabbarWidget> _widgest = [];
    int getIndex() => _widgest.length;
    if (condition1)
      _widgest.add(OrderTabbarWidget(NewOrderAddressPage(), getIndex(),
          const Icon(Icons.location_on), OrderTabType.address));
    _widgest.add(OrderTabbarWidget(const NewOrderItemsView(), getIndex(),
        const Icon(Icons.category), OrderTabType.items));
    if (condition2)
      _widgest.add(OrderTabbarWidget(
          const SubmitOrderDiscountPage(),
          getIndex(),
          const Icon(CustomIcons.discount_tag_svgrepo_com),
          OrderTabType.discount));

    if (condition3)
      _widgest.add(OrderTabbarWidget(NewOrderDefaultShipping(), getIndex(),
          const Icon(Icons.local_shipping), OrderTabType.shipping));
    _widgest.add(OrderTabbarWidget(const NewOrderSubmitPage(), getIndex(),
        const Icon(Icons.checklist_rtl_outlined), OrderTabType.submit));
    _widgest.add(OrderTabbarWidget(const NewOrderPaymentPage(), getIndex(),
        const Icon(Icons.payment), OrderTabType.payment));

    tabbarWidgets
      ..clear()
      ..addAll(_widgest);
    tabcontroller?.dispose();
    tabcontroller = null;
    tabcontroller =
        new TabController(length: tabbarWidgets.length, vsync: this);
    setState(() {
      inited = true;
    });
  }

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

  _resetMyTabbar() {
    /** fetch items or anything need for tabbar **/
    _initTabbarWidgets();
  }

  @override
  Widget build(BuildContext context) {
    if (!inited) return const NormalProgress();
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: _resetMyTabbar,
        child: const Icon(Icons.refresh),
      ),
      appBar: AppBar(
        bottom: TabBar(
            controller: tabcontroller,
            tabs: List.generate(
                tabbarWidgets.length, (index) => tabbarWidgets[index].tab)),
      ),
      body: TabBarView(
          controller: tabcontroller,
          children: List.generate(
              tabbarWidgets.length, (index) => tabbarWidgets[index].widget)),
    );
  }
}

OrderTabbarWidget

class OrderTabbarWidget {
  final Widget widget;
  final int index;
  final Widget tab;
  final OrderTabType type;
  bool get isShipping => type == OrderTabType.shipping;
  OrderTabbarWidget(this.widget, this.index, this.tab, this.type);
}
Related