Flutter TabBar and TabBarView get out of sync when dynamically adjusting number of tabs

345

Provide UniqueKey()on TabWidget(). It solves the issue for this code-snippet. It will be like

         TabWidget(
                    tp.availableTabItems,
                    tp._selectedTabIds,
                    key: UniqueKey(),
                  ),

enter image description here

Share:
345
kris
Author by

kris

I've worked as a professional software developer in Australia, England, and Holland. I’ve also volunteered my skills with NGO’s in Africa and Asia. In 2011 I started the mobile app development business CoCreations.

Updated on January 02, 2023

Comments

  • kris
    kris over 1 year

    I have a situation where I have one Widget which lets me select from a list which tab options should be displayed in another Widget (the 2nd Widget has a TabController).

    I'm using a ChangeNotifier to keep the state of which tabs are selected to be in the list.

    It all works very well except for the situation when I am on the last tab and then delete it - in which case it still works, but the TabBar goes back to the first tab, while the TabBarView goes back to the second tab.

    enter image description here

    I've tried a plethora of different approaches to fix this (adding keys to the widgets, manually saving the tab controller index in state and navigating there after a delay, adding callbacks in the top level widget that call a setState) none of which has any effect.

    Here is the code in full - I've tried to make it the smallest possible version of what I'm doing:

    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    
    void main() {
      runApp(const MyApp());
    }
    
    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);
    
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'Flutter Tab Refresh Issue Demo',
          home: Scaffold(body:
            ChangeNotifierProvider<CurrenLTabsProvider>(
              create: (_) => CurrenLTabsProvider(),
              child: Consumer<CurrenLTabsProvider>(
              builder: (context, tp, child) =>
                Row(
                  children: [
                    const SizedBox( 
                      child: TabSelectionWidget(),
                      width: 200,
                      height: 1000,
                    ),
                    SizedBox( 
                      child: TabWidget(tp.availableTabItems, tp._selectedTabIds), 
                      width: 800,
                      height: 1000,
                    ),
                  ],
                ),
              ),
            ),
          ),
        );
      }
    }
    
    class CurrenLTabsProvider extends ChangeNotifier {
      
      List<MyTabItem> availableTabItems = [
        MyTabItem(1, 'Tab 1', const Text('Content for Tab 1')),
        MyTabItem(2, 'Tab 2', const Text('Content for Tab 2')),
        MyTabItem(3, 'Tab 3', const Text('Content for Tab 3')),
       // MyTabItem(4, 'Tab 4', const Text('Content for Tab 4')),
       // MyTabItem(5, 'Tab 5', const Text('Content for Tab 5')),
      ];
    
      List<int> _selectedTabIds = [];
    
      int currentTabIndex = 0;
    
      set selectedTabs(List<int> ids) {
        _selectedTabIds = ids;
        notifyListeners();
      }
    
      List<int> get selectedTabs => _selectedTabIds;
    
      void doNotifyListeners() {
        notifyListeners();
      }
    }
    
    
    class MyTabItem {
      final int id;
      final String title;
      final Widget widget;
      MyTabItem(this.id, this.title, this.widget);
    }
    
    
    
    class TabSelectionWidget extends StatefulWidget {
      const TabSelectionWidget({Key? key}) : super(key: key);
    
      @override
      _TabSelectionWidgetState createState() => _TabSelectionWidgetState();
    }
    
    class _TabSelectionWidgetState extends State<TabSelectionWidget> {
    
      @override
      Widget build(BuildContext context) {
    
        return Consumer<CurrenLTabsProvider>(
          builder: (context, tabsProvider, child) {
            return Column(
              children: [
                Expanded(
                  child: ListView.builder(
                    itemCount: tabsProvider.availableTabItems.length,
                    itemBuilder: (context, index) {
                      final item = tabsProvider.availableTabItems[index];
                      return ListTile(
                        title: Text(item.title),
                        leading: Checkbox(
                          value: tabsProvider.selectedTabs.contains(item.id),
                          onChanged: (value) {
                            if (value==true) {
                              setState(() {
                                tabsProvider.selectedTabs.add(item.id);
                                tabsProvider.doNotifyListeners();
                              });
                            } else {
                              setState(() {
                                tabsProvider.selectedTabs.remove(item.id);
                                tabsProvider.doNotifyListeners();
                              });
                            }
                          },
                        ),
                      );
                    },
                  ),
                ),
              ],
            );
          }
        );
    
      }
    
    }
    
    
    class TabWidget extends StatefulWidget {
      const TabWidget(this.allItems, this.selectedTabs, {Key? key}) : super(key: key);
    
      final List<MyTabItem> allItems;
      final List<int> selectedTabs;
    
      @override
      _TabWidgetState createState() => _TabWidgetState();
    }
    
    class _TabWidgetState extends State<TabWidget>  with TickerProviderStateMixin {
    
      late TabController _tabController;
      @override
      void initState() {
        _tabController = TabController(length: widget.selectedTabs.length, vsync: this);
        super.initState();
      }
    
      @override
      void dispose() {
        _tabController.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        
        if (widget.selectedTabs.isEmpty) {
          return Container(
            padding: const EdgeInsets.all(20),
            child: const Text("Select some tabs to be available."),
          );
        } // else .. 
    
        // re-initialise here, so changes made in other widgets are picked up when the widget is rebuilt
        _tabController = TabController(length: widget.selectedTabs.length, vsync: this);
    
    
        var tabs = <Widget>[];
        List<Widget> tabBody = [];
        // loop through all available tabs
        for (var i = 0; i < widget.allItems.length; i++) {
          // if it is selected, then show it
          if (widget.selectedTabs.contains(widget.allItems[i].id)) {
            tabs.add( Tab(text: widget.allItems[i].title) );
            tabBody.add( widget.allItems[i].widget );
          }
        }
    
        return  Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
              TabBar(
                  labelColor: Colors.black,
                  unselectedLabelColor: Colors.black54,
                  tabs: tabs,
                  controller: _tabController,
                  indicatorSize: TabBarIndicatorSize.tab,
              ),
              Expanded(
                child: TabBarView(
                  children: tabBody,
                  controller: _tabController,
                ),
              ),
          ]
        );
    
      }
    }
    
    

    Why does the TabBar reset to the 1st entry, while the TabBarView resets to the 2nd entry?
    And what can I do to fix it so they both reset to the 1st entry?

    • Yeasin Sheikh
      Yeasin Sheikh over 2 years
      Providing key solve the issue in my case
  • kris
    kris over 2 years
    I wish I could up vote this 10 times. Thank you! I won't tell you how long I spent trying to figure this out. (I had tried keys as a solution, as mentioned in the question, but I must have only put them on the lower level widgets, on the individual tabs and tab pages themselves, not on the main tab widget itself) Thank you again!
  • Yeasin Sheikh
    Yeasin Sheikh over 2 years
    Sometimes we just need an extra pair of eyes just to be sure. You can check this video to know more about key
  • BambinoUA
    BambinoUA about 2 years
    @YeasinSheikh, Why UniqueKey exactly? Not GlobalKey or ValueKey?
  • Yeasin Sheikh
    Yeasin Sheikh about 2 years
    UniqueKey is only equal to itself. It was needed for siblings. yes GlobalKey provides more than this. you can check on flutter.devl
  • BambinoUA
    BambinoUA about 2 years
    Ok. why not ValueKey('unique')? And could you please to expain how key helps to sync tabs and views? I have the same problem (and I use web session storage for save current tab selection) but I have problem when tab view somehow switches to non-stored tab index but last one... Weird...
  • Yeasin Sheikh
    Yeasin Sheikh about 2 years
    I need to test 1st, then might be able to say something. Can you create separate question with minimal-reproducible-example