Flutter TabBar and TabBarView get out of sync when dynamically adjusting number of tabs
Provide UniqueKey()
on TabWidget()
. It solves the issue for this code-snippet. It will be like
TabWidget(
tp.availableTabItems,
tp._selectedTabIds,
key: UniqueKey(),
),
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, 2023Comments
-
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 theTabBarView
goes back to the second tab.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 asetState
) 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 theTabBarView
resets to the 2nd entry?
And what can I do to fix it so they both reset to the 1st entry?-
Yeasin Sheikh over 2 yearsProviding key solve the issue in my case
-
-
kris over 2 yearsI 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 over 2 yearsSometimes we just need an extra pair of eyes just to be sure. You can check this video to know more about key
-
BambinoUA about 2 years@YeasinSheikh, Why
UniqueKey
exactly? NotGlobalKey
orValueKey
? -
Yeasin Sheikh about 2 years
UniqueKey
is only equal to itself. It was needed for siblings. yesGlobalKey
provides more than this. you can check on flutter.devl -
BambinoUA about 2 yearsOk. 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 about 2 yearsI need to test 1st, then might be able to say something. Can you create separate question with minimal-reproducible-example