31

I need to detect TabBar when I swipe then print somethings on console, how I can do that? This is my code.

bottomNavigationBar: new Material(
             color: Colors.blueAccent,
             child: new TabBar(
               onTap: (int index){ setState(() {
                 _onTap(index);
               });},

               indicatorColor: Colors.white,
               controller: controller,
               tabs: <Widget>[
                 new Tab(icon: new Icon(Icons.shopping_basket)),
                 new Tab(icon: new Icon(Icons.store)),
                 new Tab(icon: new Icon(Icons.local_offer)),
                 new Tab(icon: new Icon(Icons.assignment)),
                 new Tab(icon: new Icon(Icons.settings)),

               ],
             )
           ),
live-love
  • 48,840
  • 22
  • 240
  • 204
Ashtav
  • 2,586
  • 7
  • 28
  • 44

10 Answers10

39

You need to add a listener to your tab controller - maybe in initState.

controller.addListener((){
   print('my index is'+ controller.index.toString());
});
nonybrighto
  • 8,752
  • 5
  • 41
  • 55
22

Swipe functionality is not provided by onTap() function, for that TabController is used

class TabBarDemo extends StatefulWidget {
  @override
  _TabBarDemoState createState() => _TabBarDemoState();
}

class _TabBarDemoState extends State<TabBarDemo>
    with SingleTickerProviderStateMixin {
  TabController _controller;
  int _selectedIndex = 0;

  List<Widget> list = [
    Tab(icon: Icon(Icons.card_travel)),
    Tab(icon: Icon(Icons.add_shopping_cart)),
  ];

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    // Create TabController for getting the index of current tab
    _controller = TabController(length: list.length, vsync: this);

    _controller.addListener(() {
      setState(() {
        _selectedIndex = _controller.index;
      });
      print("Selected Index: " + _controller.index.toString());
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          bottom: TabBar(
            onTap: (index) {
              // Should not used it as it only called when tab options are clicked,
              // not when user swapped
            },
            controller: _controller,
            tabs: list,
          ),
          title: Text('Tabs Demo'),
        ),
        body: TabBarView(
          controller: _controller,
          children: [
            Center(
                child: Text(
                  _selectedIndex.toString(),
                  style: TextStyle(fontSize: 40),
                )),
            Center(
                child: Text(
                  _selectedIndex.toString(),
                  style: TextStyle(fontSize: 40),
                )),
          ],
        ),
      ),
    );
  }
}

Github Repo:

https://github.com/jitsm555/Flutter-Problems/tree/master/tab_bar_tricks

Output:

enter image description here

Jitesh Mohite
  • 31,138
  • 12
  • 157
  • 147
  • 2
    Works nice, but `addListener()` gets called twice on tab change. Any way to remove that? – Expressingx Jan 25 '21 at 12:16
  • The listener never gets called for me even after adding the controller to the TabBar and the TabBarView –  Mar 31 '21 at 18:57
  • @Expressingx Did you find solution? – Kien Vu May 10 '21 at 11:04
  • @Expressingx & @tdtkien after tab change you dispose of the state. Afterward, call `initState` once more so that each time you change the tab new listener is being created. You can add a listener to some variable and add it along with removing the listener to the dispose method. That way you ensure just one listener is up. (Directly the one in the tab you are currently at) – Maqcel May 23 '22 at 11:21
14

Here is a full example. Use a TabController and add a callback using addListener.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Demo',
      home: MyTabbedPage(),
    );
  }
}

class MyTabbedPage extends StatefulWidget {
  const MyTabbedPage({Key key}) : super(key: key);
  @override
  _MyTabbedPageState createState() => _MyTabbedPageState();
}

class _MyTabbedPageState extends State<MyTabbedPage> with SingleTickerProviderStateMixin {
  var _context;

  final List<Tab> myTabs = <Tab>[
    Tab(text: 'LEFT'),
    Tab(text: 'RIGHT'),
  ];

  TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(vsync: this, length: myTabs.length);

    _tabController.addListener(_handleTabSelection);
  }

  void _handleTabSelection() {
    if (_tabController.indexIsChanging) {
      switch (_tabController.index) {
        case 0:
          Scaffold.of(_context).showSnackBar(SnackBar(
            content: Text('Page 1 tapped.'),
            duration: Duration(milliseconds: 500),
          ));
          break;
        case 1:
          Scaffold.of(_context).showSnackBar(SnackBar(
            content: Text('Page 2 tapped.'),
            duration: Duration(milliseconds: 500),
          ));
          break;
      }
    }
  }

  @override
  void dispose() {
    _tabController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        bottom: TabBar(
          controller: _tabController,
          tabs: myTabs,
        ),
      ),
      body: Builder(
        builder: (context) {
          _context = context;
          return 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(),
          );
        },
      ),
    );
  }
}
live-love
  • 48,840
  • 22
  • 240
  • 204
  • 11
    ```indexIsChanging``` is true only if we tap the tab bar item otherwise it's false (when swipe the tab bar view) – Vinoth Vino Jan 11 '21 at 13:21
  • 2
    @VinothVino Yeah, but `if (!_tabController.indexIsChanging) print(_tabController.index);`actually prints once every time you switch tabs (by swiping **AND** by tapping). This works as the listener gets called twice when switching tabs by tapping – Raul Mabe Apr 26 '21 at 19:04
2

You can create wrapper widget

class _DefaultTabControllerListener extends StatefulWidget {
  const _DefaultTabControllerListener(
      {Key? key, this.onTabSelected, required this.child})
      : super(key: key);

  final void Function(int index)? onTabSelected;
  final Widget child;

  @override
  _DefaultTabControllerListenerState createState() =>
      _DefaultTabControllerListenerState();
}

class _DefaultTabControllerListenerState
    extends State<_DefaultTabControllerListener> {
  late final void Function()? _listener;
  TabController? _tabController;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance?.addPostFrameCallback((_) {
      final tabController = DefaultTabController.of(context)!;
      _listener = () {
        final onTabSelected = widget.onTabSelected;
        if (onTabSelected != null) {
          onTabSelected(tabController.index);
        }
      };
      tabController.addListener(_listener!);
    });
  }
  
  @override
  void didChangeDependencies() {
    _tabController = DefaultTabController.of(context);
    super.didChangeDependencies();
  }

  @override
  void dispose() {
    if (_listener != null && _tabController != null) {
      _tabController!.removeListener(_listener!);
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }

}

And wrap TabBar with this widget

DefaultTabController(
        child: _DefaultTabControllerListener(
                  onTabSelected: (index) {
                    // Handler
                  },
                  child: TabBar(.....
Nickolay Savchenko
  • 1,474
  • 16
  • 28
  • 1
    `if (tabController.indexIsChanging == true) { return; }` Need this, or else the listener will be called twice while touching a tab to change the tab. It will work without this when tab is changed by swiping – Drunken Daddy Feb 12 '22 at 08:13
1

We can listen to tab scroll notification by using NotificationListener Widget

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: NotificationListener(
          onNotification: (scrollNotification) {
            if (scrollNotification is ScrollUpdateNotification) {
              _onStartScroll(scrollNotification.metrics);
            }
          },
          child: _buildTabBarAndTabBarViews(),
    );
  }

  _onStartScroll(ScrollMetrics metrics) {
    print('hello world');
  }
}
Abdurahman Popal
  • 2,859
  • 24
  • 18
  • This is a good solution simple to apply. I used ScrollEndNotification instead of ScrollUpdateNotification. And when scrolling is finished, check the index of the tabController and execute the function – silvershort Aug 26 '22 at 06:45
1

I ran into a similar issue and following is what I did to accomplish the same: -

import 'package:flutter/material.dart';

import '../widgets/basic_dialog.dart';
import 'sign_in_form.dart';
import 'sign_up_form.dart';

class LoginDialog extends StatefulWidget {
  @override
  _LoginDialogState createState() => _LoginDialogState();
}

class _LoginDialogState extends State<LoginDialog>
    with SingleTickerProviderStateMixin {
  int activeTab = 0;
  TabController controller;

  @override
  void initState() {
    super.initState();
    controller = TabController(vsync: this, length: 2);
  }

  @override
  Widget build(BuildContext context) {
    return NotificationListener(
      onNotification: (ScrollNotification notification) {
        setState(() {
          if (notification.metrics.pixels <= 100) {
            controller.index = 0;
          } else {
            controller.index = 1;
          }
        });

        return true;
      },
      child: BasicDialog(
        child: Container(
          height: controller.index == 0
              ? MediaQuery.of(context).size.height / 2.7
              : MediaQuery.of(context).size.height / 1.8,
          padding: const EdgeInsets.all(8.0),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              TabBar(
                controller: controller,
                tabs: <Widget>[
                  Padding(
                    padding: const EdgeInsets.all(5.0),
                    child: Text('Sign In'),
                  ),
                  Padding(
                    padding: const EdgeInsets.all(5.0),
                    child: Text('Sign up'),
                  ),
                ],
              ),
              Expanded(
                child: TabBarView(
                  controller: controller,
                  children: <Widget>[
                    Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: SingleChildScrollView(
                        child: SignInForm(),
                      ),
                    ),

                    // If a container is not displayed during the tab switch to tab0, renderflex error is thrown because of the height change.
                    controller.index == 0
                        ? Container()
                        : Padding(
                            padding: const EdgeInsets.all(8.0),
                            child: SingleChildScrollView(
                              child: SignUpForm(),
                            ),
                          ),
                  ],
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}
iMujtaba8488
  • 241
  • 3
  • 9
1

If you are using DefaultTabController and want to listen to updates in TabBar, you can expose the controller using the DefaultTabController.of method and then add a listener to it:

DefaultTabController(
        length: 3,
        child: Builder(
          builder: (BuildContext context) {
            final TabController controller = DefaultTabController.of(context)!;
            controller.addListener(() {
              if (!controller.indexIsChanging) {
                print(controller.index);
                // add code to be executed on TabBar change
              }
            });
            return Scaffold(...

Here you have a full example:

class TabControllerDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: DefaultTabController(
          length: 3,
          child: Builder(builder: (BuildContext context) {
            final TabController controller = DefaultTabController.of(context)!;
            controller.addListener(() {
              if (!controller.indexIsChanging) {
                print(controller.index);
                // add code to be executed on TabBar change
              }
            });
            return Scaffold(
              appBar: AppBar(
                bottom: const TabBar(
                  tabs: [
                    Tab(text: "Tab 0"),
                    Tab(text: "Tab 1"),
                    Tab(text: "Tab 2"),
                  ],
                ),
                title: const Text('Tabs Demo'),
              ),
              body: const TabBarView(
                children: [
                  Center(child: Text('View 0')),
                  Center(child: Text('View 1')),
                  Center(child: Text('View 2')),
                ],
              ),
            );
          })),
    );
  }
}

You can also check this DartPad LiveDemo.

Paulo Belo
  • 3,759
  • 1
  • 22
  • 20
0

enter image description here

Warp your tab in BottomNavigationBar . it will give you option onTap() where you can check which tab will clicked.

using this code you will also redirect to particular page when you tap on Tab

import 'package:flutter/material.dart';
    
    
    class BottomBarList extends StatefulWidget {
      @override
      _BottomBarListState createState() => _BottomBarListState();
    }
    
    class _BottomBarListState extends State<BottomBarList> {
      int bottomSelectedIndex = 0;
      int _selectedIndex = 0;
    
    
      List<Widget> _widgetOptions = <Widget>[
        AllMovieList(),
        MovieDescription(),
    
      ];
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          //  appBar: AppBar(),
          bottomNavigationBar: bottomBar(),
          body:_widgetOptions.elementAt(_selectedIndex),
        );
      }
    
      bottomBar() {
        return BottomNavigationBar(
    
          type: BottomNavigationBarType.shifting,
          unselectedItemColor: Colors.grey,
          items: const <BottomNavigationBarItem>[
            BottomNavigationBarItem(
              icon: Icon(Icons.tv),
              title: Text('Home'),
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.star),
              title: Text('Business'),
            ),
          ],
          currentIndex: _selectedIndex,
          selectedItemColor: Colors.black,
          backgroundColor: Colors.orange,
          onTap: _onItemTapped,
        );
      }
    
    
      void _onItemTapped(int index) {
        setState(() {
          _selectedIndex = index;
        });
      }
    }
Community
  • 1
  • 1
Mr vd
  • 878
  • 1
  • 10
  • 19
0

You can use the only scrollNotification (ScrollEndNotification) of the NotificationListener. It covers the tap and swipe actions.

class HandlingTabChanges extends State<JsonTestDetailFrame> with SingleTickerProviderStateMixin {
  late final TabController _tabController;
  final int _tabLength = 2;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: _tabLength, vsync: this);
  }

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: _tabLength,
      child: Scaffold(
        appBar: AppBar(
          title: Text("Some title"),
          bottom: TabBar(
            controller: _tabController,
            tabs: [
              Icon(Icons.settings),
              Icon(Icons.list_alt),
            ],
          ),
        ),
        body: NotificationListener(
          onNotification: (scrollNotification) {
            if (scrollNotification is ScrollEndNotification) _onTabChanged();
            return false;
          },
          child: TabBarView(
            controller: _tabController,
            children: [
              SomeWidget1(),
              SomeWidget2(),
            ],
          ),
        ),
      ),
    );
  }

  void _onTabChanged() {
    switch (_tabController.index) {
      case 0:
        // handle 0 position
        break;
      case 1:
        // handle 1 position
        break;
    }
  }
}
gbixahue
  • 1,383
  • 10
  • 9
-2

You can disable swiping effect on TabBarView by adding:

physics: NeverScrollableScrollPhysics(),

and declaring one TabController and assigning that to your TabBar and TabBarView:

TabController _tabController;

Mohsen Emami
  • 2,709
  • 3
  • 33
  • 40