简体   繁体   中英

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

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.

在此处输入图片说明

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?

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

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

在此处输入图片说明

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM