I an trying to dynamically add and remove tabs in a TabBar, and it generally works, however when adding tabs Flutter throws an exception. I guess I am doing something wrongly, however it is not obvious what - if the tabs are defined statically everything is fine, when adding dynamically I do those 2 steps:
- I update the model (TabsConfig class)
- I trigger UI update via setState.
The tab is visualized, however in the console there is the exception below:
═══════ 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.<anonymous closure>
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)
════════════════════════════════════════════════════════════════════════════════
And here is a simplified version of the code:
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(),
),
),
);
}
}
Any ideas how to make it work or what might be the cause? Thanks!
CodePudding user response:
To generate tabs dynamically you may have to use a Custom controller and use TickerProviderStateMixin instead of SingleTickerProver and update the controller when you update the screens. Made a demo for you to get started
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MainWidget(),
);
}
}
class TabsConfig {
static List<String> tabs = [];
static int selectedTabIndex = 0;
}
class MainWidget extends StatefulWidget {
const MainWidget({Key? key}) : super(key: key);
@override
State<MainWidget> createState() => MainWidgetState();
}
class MainWidgetState extends State<MainWidget> with TickerProviderStateMixin {
late TabController controller;
@override
void initState() {
// TODO: implement initState
controller = TabController(
length: TabsConfig.tabs.length,
vsync: this,
initialIndex: TabsConfig.selectedTabIndex,
);
super.initState();
}
void updateTabs() {
try {
controller = TabController(
length: TabsConfig.tabs.length,
vsync: this,
initialIndex: TabsConfig.selectedTabIndex,
);
setState(() {});
} catch (on) {
print(on); // TODO: rem
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
TabBar(
isScrollable: true,
controller: controller,
labelColor: Theme.of(context).primaryColor,
unselectedLabelColor: Theme.of(context).hintColor,
indicator: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context).primaryColor,
width: 2,
),
),
),
tabs: List.generate(
TabsConfig.tabs.length,
(index) => Text("${TabsConfig.tabs[index]}"),
),
),
Expanded(
child: TabBarView(
controller: controller,
children: List.generate(
TabsConfig.tabs.length,
(index) => Center(
child: Text("${TabsConfig.tabs[index]}"),
),
),
),
),
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
TabsConfig.tabs.add('New tab ${TabsConfig.tabs.length}');
// setState(() {});
updateTabs();
},
),
);
}
}