11

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.

Kyaw Tun
  • 12,447
  • 10
  • 56
  • 83

5 Answers5

10

It's fine to let your unused TabController be garbage collected.

Here's another strategy that might feel better: You could have store the information about the number of tabs in a model object that is owned in a State at a higher level of your tree than DashletsWidget, and pass that model object as configuration values to DashletsWidget. If the DashletsWidget is rebuilt and the constructor arguments change, the didUpdateWidget method of DashletsWidgetState will be called and you can use that as an opportunity to replace the TabController. Or alternatively you could use the number of tabs to construct a ValueKey for DashletsWidget and so changing tabs configuration would automatically dispose the existing DashletsWidgetState and replace it with a fresh one.

Collin Jackson
  • 110,240
  • 31
  • 221
  • 152
  • Thanks for GC tips and ValueKey trick. Moving state to parent will complicate things. DashletsWidget extends `StoreWatcher` which stores list of dashlet information. From what I tried, `StoreWatcher` is necessary to get instance of `Store`. The widget that extends `StoreWatcher` are also not allow to use `TickerProviderStateMixin` for `TabController`. So it is deadend there. Current solution is also inadequate since there is some issue with Tab switching animation while swapping TabController. – Kyaw Tun Sep 13 '17 at 03:02
7

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
    });
  }
Salvatore
  • 499
  • 10
  • 16
6

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) :)

Feu
  • 5,372
  • 1
  • 31
  • 57
  • Is there any side effects of defining large length? If length doesn't matter, then what's it for? – Hsingchien Cheng Dec 11 '18 at 11:37
  • Unfortunately, I don't know the answer. – Feu Dec 11 '18 at 13:48
  • If I initialize the `TabController` with a fake length instead of the real one (e.g. initialized with 20 but the `TabBar` will contain only 3 tabs), this exception is thrown: `Controller's length property (20) does not match the number of tab elements (3) present in TabBar's tabs property.` How do you deal with this exception? – Salvatore Jun 10 '19 at 09:05
  • You are right, a [pull request](https://github.com/flutter/flutter/pull/29332) made 3 months ago included this check. I'll make a test and check if this check made it impossible to have a dynamic length TabController... The check itself is not wrong, let's see. – Feu Jun 10 '19 at 15:33
1

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()),
    );
  }
Adelina
  • 10,915
  • 1
  • 38
  • 46
0

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);
}
Mohsen Haydari
  • 550
  • 5
  • 20