Flutter动态添加/移除TabBar标签异常问题排查求助
问题现象
在Flutter的TabBar中动态添加/移除标签,功能可正常运行,但添加标签时控制台抛出异常,静态定义标签无问题。操作步骤为:
- 更新模型(
TabsConfig类) - 调用
setState触发UI更新
标签能正常显示,但控制台出现如下异常:
═══════ Exception caught by widgets library ═══════════════════════════════════
The following assertion was thrown building TabBar(dirty, dependencies: [_LocalizationsScope-[GlobalKey#f57c6], _TabControllerScope, _InheritedTheme, Directionality], state: _TabBarState#cf630):
Controller's length property (4) does not match the number of tabs (3) present in TabBar's tabs property.The relevant error-causing widget was
TabBar
lib\main.dart:317
When the exception was thrown, this was the stack
#0 _TabBarState.build.
package:flutter/…/material/tabs.dart:1159
#1 _TabBarState.build
package:flutter/…/material/tabs.dart:1165
#2 StatefulElement.build
package:flutter/…/widgets/framework.dart:4919
#3 ComponentElement.performRebuild
package:flutter/…/widgets/framework.dart:4806
#4 StatefulElement.performRebuild
package:flutter/…/widgets/framework.dart:4977
#5 Element.rebuild
package:flutter/…/widgets/framework.dart:4529
#6 BuildOwner.buildScope
package:flutter/…/widgets/framework.dart:2659
#7 WidgetsBinding.drawFrame
package:flutter/…/widgets/binding.dart:891
#8 RendererBinding._handlePersistentFrameCallback
package:flutter/…/rendering/binding.dart:370
#9 SchedulerBinding._invokeFrameCallback
package:flutter/…/scheduler/binding.dart:1146
#10 SchedulerBinding.handleDrawFrame
package:flutter/…/scheduler/binding.dart:1083
#11 SchedulerBinding._handleDrawFrame
package:flutter/…/scheduler/binding.dart:997
#15 _invoke (dart:ui/hooks.dart:151:10)
#16 PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:308:5)
#17 _drawFrame (dart:ui/hooks.dart:115:31)
(elided 3 frames from dart:async)
════════════════════════════════════════════════════════════════════════════════
简化代码示例
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class TabsConfig { static List<String> tabs = []; static int selectedTabIndex = 0; } class MyApp extends MaterialApp { MyApp({Key? key}) : super( key: key, home: const MainWidget(), ); } class MainWidget extends StatefulWidget { const MainWidget({Key? key}) : super(key: key); @override State<MainWidget> createState() => MainWidgetState(); } class MainWidgetState extends State<MainWidget> with SingleTickerProviderStateMixin { void updateTabs() { try { setState(() {}); } catch (on) { print(on); // TODO: rem } } @override Widget build(BuildContext context) { return Scaffold( body: DefaultTabController( length: TabsConfig.tabs.length, initialIndex: TabsConfig.selectedTabIndex, child: CustomScrollView( slivers: <Widget>[ getSliverTabBar(), ], ), ), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: () { TabsConfig.tabs.add('New tab'); updateTabs(); }, ), ); } Widget getSliverTabBar() { print('tabs: ${TabsConfig.tabs.length}'); //TODO: rem return SliverAppBar( pinned: true, backgroundColor: Colors.blue, title: Align( alignment: AlignmentDirectional.topStart, child: TabBar( indicatorColor: Colors.white, isScrollable: true, onTap: ((int selected) { TabsConfig.selectedTabIndex = selected; }), tabs: TabsConfig.tabs .map( (e) => Tab(text: e), ) .toList(), ), ), ); } }
异常原因
DefaultTabController的生命周期问题:DefaultTabController在build方法中创建,setState触发重建时,新控制器会用更新后的标签数量初始化,但TabBar会先尝试使用旧的控制器实例(未更新length)渲染,导致控制器length与标签数量不匹配,触发断言异常。- 全局静态状态的同步问题:
TabsConfig使用静态变量存储标签数据,全局状态更新与组件树重建不同步,进一步加剧了控制器与标签数量的不一致。
解决方法
方法1:使用自定义TabController替代DefaultTabController
自定义控制器可直接控制length属性更新,避免新旧控制器的不匹配问题,同时将状态移至组件内部,提升同步性:
class MainWidgetState extends State<MainWidget> with SingleTickerProviderStateMixin { late TabController _tabController; List<String> _tabs = []; int _selectedTabIndex = 0; @override void initState() { super.initState(); _tabController = TabController(length: _tabs.length, vsync: this); _tabController.addListener(() { setState(() { _selectedTabIndex = _tabController.index; }); }); } @override void dispose() { _tabController.dispose(); super.dispose(); } void updateTabs() { setState(() { // 更新控制器长度,同步标签数量 _tabController = TabController( length: _tabs.length, vsync: this, initialIndex: _selectedTabIndex, ); _tabController.addListener(() { setState(() { _selectedTabIndex = _tabController.index; }); }); }); } @override Widget build(BuildContext context) { return Scaffold( body: CustomScrollView( slivers: <Widget>[ getSliverTabBar(), ], ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.add), onPressed: () { _tabs.add('New tab'); updateTabs(); }, ), ); } Widget getSliverTabBar() { return SliverAppBar( pinned: true, backgroundColor: Colors.blue, title: Align( alignment: AlignmentDirectional.topStart, child: TabBar( controller: _tabController, // 指定自定义控制器 indicatorColor: Colors.white, isScrollable: true, tabs: _tabs.map((e) => Tab(text: e)).toList(), ), ), ); } }
方法2:给DefaultTabController添加唯一Key
如果坚持使用DefaultTabController,可通过添加唯一Key强制其在标签数量变化时完全重建,避免复用旧实例:
// 修改build方法中的DefaultTabController部分 body: DefaultTabController( key: ValueKey(TabsConfig.tabs.length), // 用标签数量作为唯一标识 length: TabsConfig.tabs.length, initialIndex: TabsConfig.selectedTabIndex, child: CustomScrollView( slivers: <Widget>[ getSliverTabBar(), ], ), ),
建议同时将TabsConfig的静态变量改为组件内部状态,减少全局状态带来的同步问题。
内容的提问来源于stack exchange,提问作者Kras

