Flutter dynamic TabBar with tabs from Firebase - flutter

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.

Related

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.

Dynamic number of tabs

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

Change badge number

In MainPage, it has 2 bottom navigation bar. One is icon with text, another is icon with text and badge number. When my app is launched, the badge is display 3 in second tab. This works fine.
class MainPage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _MainPageState();
}
method(int num) => _MainPageState().showBadge(num);
}
class _MainPageState extends State<MainPage>
with SingleTickerProviderStateMixin {
int _selectedIndex = 0;
int count = 0;
TabController _tabController;
PageController _pageController;
#override
void initState() {
super.initState();
_tabController = TabController(length: 5, vsync: this);
_pageController = PageController(initialPage: _selectedIndex);
showBadge(3);
}
void showBadge(int number) {
setState(() {
count = number;
});
}
void onPageChange(int index) {
setState(() {
_selectedIndex = index;
});
}
void _onItemTapped(int index) {
_pageController.animateToPage(index,
duration: kTabScrollDuration, curve: Curves.ease);
}
#override
Widget build(BuildContext context) {
return WillPopScope(
child: Scaffold(
body: FixTabBarView(
pageController: _pageController,
onPageChange: onPageChange,
tabController: _tabController,
children: <Widget>[
TabA(),
TabB(),
]),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.A), title: Text('TabA')),
BottomNavigationBarItem(
icon: Stack(children: <Widget>[
Icon(
Icons.B,
),
Positioned(
top: 1.0,
right: 0.0,
child: Stack(
children: <Widget>[
Icon(Icons.brightness_1, size: 18, color: Colors.red),
Positioned(
top: 1.0,
right: 4.0,
child: new Text(count.toString(),
style: new TextStyle(
color: Colors.white,
fontSize: 15.0,
fontWeight: FontWeight.w500)),
)
],
),
)
]),
title: Text('TabB'),
),
],
currentIndex: _selectedIndex,
fixedColor: Colors.blue,
onTap: _onItemTapped,
),
),
onWillPop: () {},
);
}
}
When tab 2 is clicked, I want the badge change to 1,but it throws error.
class TabB extends StatefulWidget {
#override
State<StatefulWidget> createState() => _TabBState();
}
class _TabBState extends State<TabB> {
#override
void initState() {
super.initState();
_bloc.callApi().then((onValue){
MainPage().method(onValue); // onValue is the number return from server
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Tab 2"),
));
}
}
Error
════════ Exception caught by widgets library
═══════════════════════════════════ The following assertion was thrown
building NotificationListener: setState()
called in constructor: _MainPageState#6ed53(lifecycle state: created,
no widget, not mounted)
This happens when you call setState() on a State object for a widget
that hasn't been inserted into the widget tree yet. It is not
necessary to call setState() in the constructor, since the state is
already assumed to be dirty when it is initially created.
Instead of trying to call a function from your parent widget to modify the badge when that child widget is loaded, you should add a listener to your _tabController and change the badge when the tab is selected, like this:
#override
void initState() {
super.initState();
_tabController = TabController(length: 5, vsync: this);
_tabController.addListener((){
if(_tabController.index == 1){
setState(() {
showBadge(1);
});
}
});
_pageController = PageController(initialPage: _selectedIndex);
showBadge(3);
}
Make sure you adjust the if for the tab index you want to match.
On your TabB you can declare that it accepts a Function as part of its constructor and then call that function:
class TabB extends StatefulWidget {
final Function showBadge;
TabB({this.showBadge});
#override
State<StatefulWidget> createState() => _TabBState();
}
class _TabBState extends State<TabB> {
#override
void initState() {
super.initState();
_bloc.callApi().then((onValue){
widget.showBadge(onValue); // onValue is the number return from server
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Tab 2"),
)
);
}
}
On your Main widget:
FixTabBarView(
pageController: _pageController,
onPageChange: onPageChange,
tabController: _tabController,
children: <Widget>[
TabA(),
TabB(showBadge: showBadge,),
]
)

How to get current tab index in Flutter

In flutter implementing a tab layout is easy and straightforward. This is a simple example from the official documentation:
import 'package:flutter/material.dart';
void main() {
runApp(new TabBarDemo());
}
class TabBarDemo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new DefaultTabController(
length: 3,
child: new Scaffold(
appBar: new AppBar(
bottom: new TabBar(
tabs: [
new Tab(icon: new Icon(Icons.directions_car)),
new Tab(icon: new Icon(Icons.directions_transit)),
new Tab(icon: new Icon(Icons.directions_bike)),
],
),
title: new Text('Tabs Demo'),
),
body: new TabBarView(
children: [
new Icon(Icons.directions_car),
new Icon(Icons.directions_transit),
new Icon(Icons.directions_bike),
],
),
),
),
);
}
}
But here is the thing, I want to get the active tab index so I can apply some logic on certain tabs. I search the documentation but I wasn't able to figure it out. Can you guys help and thanks?
The whole point of DefaultTabController is for it to manage tabs by itself.
If you want some custom tab management, use TabController instead.
With TabController you have access to much more informations, including the current index.
class MyTabbedPage extends StatefulWidget {
const MyTabbedPage({Key key}) : super(key: key);
#override
_MyTabbedPageState createState() => new _MyTabbedPageState();
}
class _MyTabbedPageState extends State<MyTabbedPage>
with SingleTickerProviderStateMixin {
final List<Tab> myTabs = <Tab>[
new Tab(text: 'LEFT'),
new Tab(text: 'RIGHT'),
];
TabController _tabController;
#override
void initState() {
super.initState();
_tabController = new TabController(vsync: this, length: myTabs.length);
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
bottom: new TabBar(
controller: _tabController,
tabs: myTabs,
),
),
body: new TabBarView(
controller: _tabController,
children: myTabs.map((Tab tab) {
return new Center(child: new Text(tab.text));
}).toList(),
),
);
}
}
In this case, using StatefulWidget and State isn't a good idea.
You can get current index by DefaultTabController.of(context).index;.
Follow the code:
...
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(~), Tab(~)
]
),
actions: [
// At here you have to get `context` from Builder.
// If you are not sure about this, check InheritedWidget document.
Builder(builder: (context){
final index = DefaultTabController.of(context).index;
// use index at here...
})
]
)
You can access the current index when the tab is selected by onTap event of TabBar.
TabBar(
onTap: (index) {
//your currently selected index
},
tabs: [
Tab1(),
Tab2(),
]);
Just apply a listener on the TabController.
// within your initState() method
_tabController.addListener(_setActiveTabIndex);
void _setActiveTabIndex() {
_activeTabIndex = _tabController.index;
}
Use DefaultTabController you can get current index easily whether the user changes tabs by swiping or tap on the tab bar.
Important: You must wrap your Scaffold inside of a Builder and you can then retrieve the tab index with DefaultTabController.of(context).index inside Scaffold.
Example:
DefaultTabController(
length: 3,
child: Builder(builder: (BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
bottom: TabBar(
isScrollable: true,
tabs: [Text('0'), Text('1'), Text('2')]),
),
body: _buildBody(),
floatingActionButton: FloatingActionButton(
onPressed: () {
print(
'Current Index: ${DefaultTabController.of(context).index}');
},
),
);
}),
),
New working solution
I'd suggest you to use TabController for more customisations. To get active tab index you should use _tabController.addListener and _tabController.indexIsChanging.
Use this full code snippet:
class CustomTabs extends StatefulWidget {
final Function onItemPressed;
CustomTabs({
Key key,
this.onItemPressed,
}) : super(key: key);
#override
_CustomTabsState createState() => _CustomTabsState();
}
class _CustomTabsState extends State<CustomTabs>
with SingleTickerProviderStateMixin {
final List<Tab> myTabs = <Tab>[
Tab(text: 'LEFT'),
Tab(text: 'RIGHT'),
];
TabController _tabController;
int _activeIndex = 0;
#override
void initState() {
super.initState();
_tabController = TabController(
vsync: this,
length: myTabs.length,
);
}
#override
void dispose() {
super.dispose();
_tabController.dispose();
}
#override
Widget build(BuildContext context) {
double width = MediaQuery.of(context).size.width;
_tabController.addListener(() {
if (_tabController.indexIsChanging) {
setState(() {
_activeIndex = _tabController.index;
});
}
});
return Container(
color: Colors.white,
child: TabBar(
controller: _tabController,
isScrollable: true,
indicatorPadding: EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0),
indicator: BoxDecoration(
borderRadius: BorderRadius.circular(10.0), color: Colors.green),
tabs: myTabs
.map<Widget>((myTab) => Tab(
child: Container(
width: width / 3 -
10, // - 10 is used to make compensate horizontal padding
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
color:
_activeIndex == myTabs.indexOf(myTab)
? Colors.transparent
: Color(0xffA4BDD4),
),
margin:
EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0),
child: Align(
alignment: Alignment.center,
child: Text(
myTab.text,
style: TextStyle(color: Colors.white),
),
),
),
))
.toList(),
onTap: widget.onItemPressed,
),
);
}
}
Thanks to the example of Rémi Rousselet, you can do it, the code like this:
_tabController.index
This will return the current index of the position of your TabBarView
You can add a listener to listen to changes in tabs like below
tabController = TabController(vsync: this, length: 4)
..addListener(() {
setState(() {
switch(tabController.index) {
case 0:
// some code here
case 1:
// some code here
}
});
});
Well, nothing here was working in my case.
I tried several responses so as a result i used a provider to keep and retrieve the current index selected.
First the model.
class HomeModel extends ChangeNotifier {
int _selectedTabIndex = 0;
int get currentTabIndex => _selectedTabIndex;
setCurrentTabIndex(final int index){
_selectedTabIndex = index;
// notify listeners if you want here
notifyListeners();
}
...
}
Then i used _tabController.addListener() to update my model.
class HomePageState extends State<HomeScreen> {
late TabController _tabController;
#override
void initState() {
super.initState();
_tabController = TabController(vsync: this, length: _tabs.length);
_tabController.addListener(() {
context.read<HomeModel>().setCurrentTabIndex(_tabController.index);
});
}
...
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: _tabs.length,
child :
Scaffold(
backgroundColor: Colors.white70,
appBar: AppBar(
/*iconTheme: IconThemeData(
color: Colors.black
),*/
bottom: TabBar(
controller: _tabController,
tabs: _tabs,
),
title: Text(_getAppBarTitle(),style: const TextStyle(/*color: Colors.red,*/fontSize: 22.0),)
...
)
)
);
}
...
}
Finally last but not least retrieve value when you need.
class _AppState extends State<App> {
#override
Widget build(BuildContext context) {
return Consumer<HomeModel>(
builder: (context, homeModel, child) {
return Text(homeModel.currentTabIndex); // herre we get the real current index
});
}
}
Set the variable in top.
class _MainTabWidgetState extends State<MainTabWidget> {
#override void initState() {
// TODO: implement initState
super.initState();
}
int selected_index = 0;
}
Now set index in Tabbar onTap
onTap: (index) {
setState(() {
selected_index = index;
});
},
This Code will give you index of Active tab , also save the tab index for future use, and when you back to the tab page the the previous active page will be displayed.
import 'package:flutter/material.dart';
void main() {
runApp(new TabBarDemo());
}
class TabBarDemo extends StatelessWidget {
TabScope _tabScope = TabScope.getInstance();
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new DefaultTabController(
length: 3,
index: _tabScope.tabIndex, //
child: new Scaffold(
appBar: new AppBar(
bottom: new TabBar(
onTap: (index) => _tabScope.setTabIndex(index), //current tab index
tabs: [
new Tab(icon: new Icon(Icons.directions_car)),
new Tab(icon: new Icon(Icons.directions_transit)),
new Tab(icon: new Icon(Icons.directions_bike)),
],
),
title: new Text('Tabs Demo'),
),
body: new TabBarView(
children: [
new Icon(Icons.directions_car),
new Icon(Icons.directions_transit),
new Icon(Icons.directions_bike),
],
),
),
),
);
}
}
class TabScope{ // singleton class
static TabScope _tabScope;
int tabIndex = 0;
static TabScope getInstance(){
if(_tabScope == null) _tabScope = TabScope();
return _tabScope;
}
void setTabIndex(int index){
tabIndex = index;
}
}