动态调整选项卡数量时,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(),
),
我有一个 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(),
),