动态调整选项卡数量时,Flutter TabBar 和 TabBarView 不同步

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

我有一个 Widget 可以让我从列表中 select 哪个选项卡选项应该显示在另一个 Widget 中(第二个 Widget 有一个 TabController)。

我正在使用 ChangeNotifier 来保持 select 选项卡在列表中的状态。

一切都很好,除了我在最后一个选项卡上然后删除它的情况 - 在这种情况下它仍然有效,但 TabBar 返回到第一个选项卡,而 TabBarView 返回到第二个选项卡。

我已经尝试了多种不同的方法来解决这个问题(将 keys 添加到小部件,手动将选项卡控制器索引保存在状态中并在延迟后导航到那里,在顶级小部件中添加回调调用 setState) none 其中有任何效果。

这里是完整的代码——我试图让它成为我正在做的事情的尽可能小的版本:

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,
            ),
          ),
      ]
    );

  }
}

为什么TabBar重置为第一个条目,而TabBarView重置为第二个条目?
我该怎么做才能使它们都重置为第一个条目?

TabWidget() 上提供 UniqueKey()。它解决了此代码片段的问题。会像

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