Dynamic number of tabs - flutter

I have design like above , my tabbar is inside BottomNavigationBar and i want place the tabbar in my BottomNavigationBar menu. The problem is , i don't want have nested scaffold and i don't want use TabController because my tabs is dynamic , tabs length can be decrease or increase depending of user adding it. If i use TabController and define it inside initState , the tabs can't be increase/decrease because in initState only once define the value.
How can i do this ?
My expectation similiar in below example , when i adding tab and tabMenu it will increase . But get this error
The following assertion was thrown building TabBar(dirty, dependencies: [_InheritedTheme,
_LocalizationsScope-[GlobalKey#a56a4]], state: _TabBarState#fb337):
Controller's length property (2) does not match the number of tabs (3) present in TabBar's tabs
property.
The relevant error-causing widget was:
TabBar
Dummy Example
class TaskScreen extends StatefulWidget {
#override
_TaskScreenState createState() => _TaskScreenState();
}
class _TaskScreenState extends State<TaskScreen> with SingleTickerProviderStateMixin {
TabController _tabController;
final List<Tab> _tab = [
Tab(text: 'Sunday'),
Tab(text: 'Monday'),
];
final List<Widget> _tabMenu = [
Text('This is Sunday'),
Text('This is Monday'),
];
#override
void initState() {
super.initState();
_tabController = TabController(length: _tab.length, vsync: this);
}
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FlatButton(
onPressed: () {
setState(() {
_tab.add(Tab(
text: '1',
));
_tabMenu.add(Text('1'));
});
print('tab ${_tab.length}');
print('tabMenu ${_tabMenu.length}');
},
child: Text('add')),
Container(
color: colorPallete.primaryColor,
padding: EdgeInsets.only(top: sizes.statusBarHeight(context)),
child: TabBar(
tabs: _tab.map((e) => e).toList(),
controller: _tabController,
),
),
Expanded(
child: TabBarView(
children: _tabMenu.map((e) => e).toList(),
controller: _tabController,
),
)
],
);
}
}

use TickerProviderStateMixin instead of SingleTickerProviderStateMixin
and change TabController on changing tabs number;
add isScrollable: true when you don't have space for all your tabs
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
body: SafeArea(
child: MyHomePage(),
),
),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return TaskScreen();
}
}
class TaskScreen extends StatefulWidget {
#override
_TaskScreenState createState() => _TaskScreenState();
}
class _TaskScreenState extends State<TaskScreen> with TickerProviderStateMixin {
TabController _tabController;
final List<Tab> _tab = [
Tab(text: 'Sunday'),
Tab(text: 'Monday'),
];
final List<Widget> _tabMenu = [
Text('This is Sunday'),
Text('This is Monday'),
];
#override
void initState() {
super.initState();
_tabController = TabController(length: _tab.length, vsync: this);
}
#override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FlatButton(
onPressed: () {
setState(() {
_tab.add(
Tab(
text: '1',
),
);
_tabMenu.add(
Text('1'),
);
_tabController =
TabController(length: _tab.length, vsync: this);
});
print('tab ${_tab.length}');
print('tabMenu ${_tabMenu.length}');
},
child: Text('add')),
Container(
color: Colors.blue,
padding: EdgeInsets.only(top: 10),
child: TabBar(
// isScrollable: true,
tabs: _tab.map((e) => e).toList(),
controller: _tabController,
),
),
Expanded(
child: TabBarView(
children: _tabMenu.map((e) => e).toList(),
controller: _tabController,
),
)
],
);
}
}

Related

Flutter dynamic TabBar with tabs from Firebase

Hi,
I am making a to-do/tasks app, I want the users to create their own categories/tabs for them to store notes under, I already can show the notes with the corresponding category but I'm stuck at actually displaying data from firebase as tabs in TabBar, I've created a List with hardcoded values, how do I get values from Firebase into my list and iterate them as actual tabs?
class HomeTopTabs extends StatefulWidget {
const HomeTopTabs({super.key});
#override
State<HomeTopTabs> createState() => _HomeTopTabs();
}
class _HomeTopTabs extends State<HomeTopTabs>
with SingleTickerProviderStateMixin {
final Stream<QuerySnapshot> categories =
FirebaseFirestore.instance.collection('categories').snapshots();
static const List<Tab> myTabs = <Tab>[
Tab(text: 'Spanish'),
Tab(text: 'Work'),
];
late TabController _tabController;
late final FirebaseCloudStorage _notesService;
String get userId => AuthService.firebase().currentUser!.id;
#override
void initState() {
super.initState();
_notesService = FirebaseCloudStorage();
_tabController = TabController(length: myTabs.length, vsync: this);
// _tabController.addListener(_handleTabSelection);
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(
height: 500,
child: Scaffold(
appBar: AppBar(
flexibleSpace: SafeArea(
child: TabBar(
controller: _tabController,
isScrollable: true,
onTap: (index) {},
tabs: myTabs,
)),
),
body: TabBarView(
controller: _tabController,
children: myTabs.map((Tab tab) {
final String label = tab.text!.toLowerCase();
return Center(
child: Text(
'This is the $label tab',
style: const TextStyle(fontSize: 36),
),
);
}).toList(),
),
),
);
}
}
I have tried to search other questions and relating tutorials but most of them is depreciated code.

Call a method from one statefulWidget in tab page in Flutter

I have two widgets - one is the main page with tabs and buttons and the second one is - Page with some TextFields. I would like to clean all TextFields after the button click. I tried to use
GlobalKey<_Page1> _key = GlobalKey<_Page1>(); and _key.currentState!.CleanAll(); but I received an error - Unhandled Exception: Null check operator used on a null value
Do you have any other ideas on how to do this?
class MyTabs extends StatefulWidget {
const MyTabs({Key? key}) : super(key: key);
#override
_MyTabsState createState() => _MyTabsState();
}
class _MyTabsState extends State<MyTabs> {
#override
Widget build(BuildContext context) {
return Column(
children: [
DefaultTabController(
length: 1, // length of tabs
initialIndex: 0,
child: Column(
children: <Widget>[
const TabBar(
labelColor: Colors.green,
unselectedLabelColor: Colors.blueGrey,
tabs: [
Tab(text: 'Page 1'),
],
),
SizedBox(
height: 200,
child: TabBarView(
children: <Widget>[
Page1(),
]
),
),
],
),),
ElevatedButton(
onPressed: () {
// <- clean all TextField in Page1
},
child: Text('Clean')
),
],
);
}
}
class Page1 extends StatefulWidget {
const Page1({Key? key}) : super(key: key);
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1> {
TextEditingController text1 = TextEditingController();
TextEditingController text2 = TextEditingController();
void CleanAll() {
text1.clear();
text2.clear();
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: text1,
),
TextField(
controller: text2,
),
],
);
}
}
You can use a ValueNotifier that will listen on Page1. then we will override initState on Page1 and clear the controlles.
class _MyTabsState extends State<MyTabs> {
final ValueNotifier<bool> clearNotifier = ValueNotifier(false);
pass it to the Page1
Page1(
clearCallback: clearNotifier,
),
to notify the listener we are just switching the value. bool value does not matter, we just need to update it.
onPressed: () {
clearNotifier.value = !clearNotifier.value;
},
and on Page1
class Page1 extends StatefulWidget {
final ValueNotifier clearCallback;
const Page1({Key? key, required this.clearCallback}) : super(key: key);
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1> {
#override
void initState() {
super.initState();
widget.clearCallback.addListener(() {
CleanAll();
});
}
Test snippet
class MyTabs extends StatefulWidget {
const MyTabs({Key? key}) : super(key: key);
#override
_MyTabsState createState() => _MyTabsState();
}
class _MyTabsState extends State<MyTabs> {
final ValueNotifier<bool> clearNotifier = ValueNotifier(false);
#override
Widget build(BuildContext context) {
return Column(
children: [
DefaultTabController(
length: 1, // length of tabs
initialIndex: 0,
child: Column(
children: <Widget>[
const TabBar(
labelColor: Colors.green,
unselectedLabelColor: Colors.blueGrey,
tabs: [
Tab(text: 'Page 1'),
],
),
SizedBox(
height: 200,
child: TabBarView(children: <Widget>[
Page1(
clearCallback: clearNotifier,
),
]),
),
],
),
),
ElevatedButton(
onPressed: () {
clearNotifier.value = !clearNotifier.value;
},
child: Text('Clean')),
],
);
}
}
class Page1 extends StatefulWidget {
final ValueNotifier clearCallback;
const Page1({Key? key, required this.clearCallback}) : super(key: key);
#override
_Page1State createState() => _Page1State();
}
class _Page1State extends State<Page1> {
#override
void initState() {
super.initState();
widget.clearCallback.addListener(() {
CleanAll();
});
}
TextEditingController text1 = TextEditingController();
TextEditingController text2 = TextEditingController();
void CleanAll() {
text1.clear();
text2.clear();
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: text1,
),
TextField(
controller: text2,
),
],
);
}
}

How to use tabBar controller in flutter?

import 'dart:ffi';
import 'package:flutter/material.dart';
import 'package:whatsapp_clone/chatScreen.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin{
late TabController _tabcontroller;
#override
void initState(){
super.initState();
_tabcontroller=TabController(length: 4, vsync: this, initialIndex: 1);
}
#override
void dispose(){
_tabcontroller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Color(0xff075E54),
title: const Text('Whatsapp Clone'),
actions: [IconButton(onPressed: (){}, icon: const Icon(Icons.search)),
PopupMenuButton(onSelected: (value){print(value);},
itemBuilder: (BuildContext context){return [
PopupMenuItem(child: Text('New Group'),value: 'New Group'),
PopupMenuItem(child: Text('New Broadcast'),value: 'New Broadcast'),
PopupMenuItem(child: Text('Whatsapp web'),value: 'Whatsapp web'),
PopupMenuItem(child: Text('Starred messages'),value: 'Starred messages'),
PopupMenuItem(child: Text('Settings'),value: 'Settings')
];
}
),
],
bottom: TabBar(
controller: _tabcontroller,
tabs: const [
Tab(icon: Icon(Icons.camera_alt)),
Tab(text:'CHAT'),
Tab(text: 'STATUS'),
Tab(text: 'CALLS')
],
indicatorColor: Colors.white,
),
),
body: TabBarView(controller: _tabcontroller,
children: const [
Text('camera'),
ChatPage(),
Text('status'),
Text('calls')
],
),
);
}
}
I want to show the chatscreen which is at 1 index, when opening the app, and i have given the initial index to 1 also, but whenever i open my app, although the chat tabbar was selected but the content was showing from first screen. how can i solve this problem

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(),
),

How to build a TabView using Flutter GetX

I'm trying to build a app using Flutter with GetX for State Management, and in one of my pages i need to build a TabView widget in the middle of the page, i already found a lot of stuff explaining how to build a TabView as widget, as this post and this article, but all of these extends a State controller with a SingleTickerProviderStateMixin.
As i understand reading the documentation, i'm not supposed to use StatefulWidgets and States like that, but i cant figure out how to elaborate a solution using the GetX architecture.
I already tried in my page things like:
class CourseDetailPage extends StatelessWidget {
final TabController _tabController = Get.put(TabController(vsync: null, length: 2));
}
and
class CourseDetailPage extends StatelessWidget {
final TabController _tabController = TabController(vsync: null, length: 2);
}
But the VSYNC argument for the TabController cannot be null, and i don't figure out how i cant obtain a TickerProvider to populate it.
Would the following be a solution to using a GetX controller to control a TabView?
GetTicker
There's a Get version of SingleTickerProviderMixin which implements the TickerProvider interface (using the same Ticker class from Flutter SDK).
It has a catchy name:
GetSingleTickerProviderStateMixin
(Updated 21-12-20: SingleGetTickerProviderMixin has been deprecated with latest GetX. Thanks to commenter pointing this out.)
The below example is basically from the Flutter docs for TabController.
From the linked example's StatefulWidget I transplanted its contents into a GetxController (MyTabController) and added mixin of SingleGetTickerProviderMixin:
class MyTabController extends GetxController with GetSingleTickerProviderStateMixin {
final List<Tab> myTabs = <Tab>[
Tab(text: 'LEFT'),
Tab(text: 'RIGHT'),
];
late TabController controller;
#override
void onInit() {
super.onInit();
controller = TabController(vsync: this, length: myTabs.length);
}
#override
void onClose() {
controller.dispose();
super.onClose();
}
}
To use MyTabController inside a Stateless widget:
class MyTabbedWidget extends StatelessWidget {
const MyTabbedWidget();
#override
Widget build(BuildContext context) {
final MyTabController _tabx = Get.put(MyTabController());
// ↑ init tab controller
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: _tabx.controller,
tabs: _tabx.myTabs,
),
),
body: TabBarView(
controller: _tabx.controller,
children: _tabx.myTabs.map((Tab tab) {
final String label = tab.text!;
return Center(
child: Text(
'This is the $label tab',
style: const TextStyle(fontSize: 36),
),
);
}).toList(),
),
);
}
}
Here's the rest of the app example to just copy & paste into Android Studio / VisualStudio Code to run:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GetX Tab Example'),
),
body: Column(
children: [
Expanded(
flex: 1,
child: Container(
alignment: Alignment.center,
child: Text('Some random stuff'),
),
),
Expanded(
flex: 4,
child: MyTabbedWidget(),
)
],
),
);
}
}
class MyTabController extends GetxController with GetSingleTickerProviderStateMixin {
final List<Tab> myTabs = <Tab>[
Tab(text: 'LEFT'),
Tab(text: 'RIGHT'),
];
late TabController controller;
#override
void onInit() {
super.onInit();
controller = TabController(vsync: this, length: myTabs.length);
}
#override
void onClose() {
controller.dispose();
super.onClose();
}
}
class MyTabbedWidget extends StatelessWidget {
const MyTabbedWidget();
#override
Widget build(BuildContext context) {
final MyTabController _tabx = Get.put(MyTabController());
// ↑ init tab controller
return Scaffold(
appBar: AppBar(
bottom: TabBar(
controller: _tabx.controller,
tabs: _tabx.myTabs,
),
),
body: TabBarView(
controller: _tabx.controller,
children: _tabx.myTabs.map((Tab tab) {
final String label = tab.text!;
return Center(
child: Text(
'This is the $label tab',
style: const TextStyle(fontSize: 36),
),
);
}).toList(),
),
);
}
}
As you said in all articles they extend State with SingleTickerProviderStateMixin because you won't be able to initialize your TabController outside of a State as TabController manage a state (doc).
A workaround would be to not use a variable for your controller and wrap your TabBar and TabView widget tree inside a DefaultTabController.
Here is an example from the official doc:
class MyDemo extends StatelessWidget {
final List<Tab> myTabs = <Tab>[
Tab(text: 'LEFT'),
Tab(text: 'RIGHT'),
];
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: myTabs.length,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: myTabs,
),
),
body: TabBarView(
children: myTabs.map((Tab tab) {
final String label = tab.text.toLowerCase();
return Center(
child: Text(
'This is the $label tab',
style: const TextStyle(fontSize: 36),
),
);
}).toList(),
),
),
);
}
}
By doing so you won't need to have your TabView inside a State but you won't be using GetX either.
I came across the exact issue and it shows that the GetX framework is not ready for many possible scenarios and you should use it cautiously.
The solution I came up with is a wrapper widget. In the proposed example I used HookWidget but you are free to use Stateful widget as well.
class TabWidget extends HookWidget {
final List<Widget> children;
final RxInt currentTab;
final int initialIndex;
TabWidget({
#required this.children,
#required this.currentTab,
#required this.initialIndex,
});
#override
Widget build(BuildContext context) {
final controller = useTabController(
initialLength: children.length,
initialIndex: initialIndex,
);
currentTab.listen((page) {
controller.animateTo(page);
});
return TabBarView(controller: controller, children: children);
}
}
As you can see there is a Rx variable which controls the state of the tabView. So you have to pass a RxInt from the outside and whenever the value of it changes, the tabView updates respectively.
class TestView extends GetView<TestController> {
#override
Widget build(BuildContext context) {
final screen = 0.obs;
return SafeArea(
child: Scaffold(
appBar: AppBar(
actions: [
IconButton(
icon: Icon(Icons.forward),
onPressed: () => screen.value = screen.value + 1,
)
],
),
body: TabWidget(
initialIndex: 1,
currentTab: screen,
children: [
child1,
child2,
child3,
...,
],
),
),
);
}
Right now we take care of the controller by the help of HookWidget. If you are using Stateful widget you have to dispose it correctly.