How to detect TabBar change in Flutter?
Asked Answered
W

10

37

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

               ],
             )
           ),
Wil answered 3/4, 2019 at 0:49 Comment(0)
H
42

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

controller.addListener((){
   print('my index is'+ controller.index.toString());
});
Housebound answered 3/4, 2019 at 2:36 Comment(1)
Where did you add it? Can you update your code so I can see how you used it?Housebound
H
25

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

Hussy answered 19/6, 2020 at 14:29 Comment(4)
Works nice, but addListener() gets called twice on tab change. Any way to remove that?Bobbie
The listener never gets called for me even after adding the controller to the TabBar and the TabBarViewPlacate
@Bobbie Did you find solution?Jankell
@Bobbie & @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)Inchmeal
R
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(),
          );
        },
      ),
    );
  }
}
Ratify answered 3/5, 2020 at 2:20 Comment(2)
indexIsChanging is true only if we tap the tab bar item otherwise it's false (when swipe the tab bar view)Mcneese
@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 tappingBitter
P
3

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.

Pattern answered 26/9, 2022 at 10:24 Comment(0)
K
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(.....
Kesley answered 29/6, 2021 at 16:52 Comment(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 swipingRepatriate
D
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');
  }
}
Duncandunce answered 22/2, 2020 at 14:36 Comment(1)
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 functionIndiscrete
S
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(),
                            ),
                          ),
                  ],
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}
Stubblefield answered 6/7, 2020 at 14:59 Comment(0)
L
1

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;
    }
  }
}
Luxuriate answered 20/5, 2021 at 13:52 Comment(0)
L
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;
        });
      }
    }
Luellaluelle answered 19/6, 2020 at 15:27 Comment(0)
E
-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;

Electrolytic answered 13/8, 2019 at 11:41 Comment(3)
We can't detect tab scroll by NeverScrollableScrollPhysics, we disable swiping by it.Duncandunce
Disabling swiping TabBarView enables you to detect changing between tabs only by tapping on Tabs, that is because there is no listener for swiping tabsElectrolytic
We can listen to tab scroll notification by using NotificationListener Widget https://mcmap.net/q/414109/-how-to-detect-tabbar-change-in-flutterDuncandunce

© 2022 - 2024 — McMap. All rights reserved.