Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TabController listener called multiple times. How does indexIsChanging work?

Tags:

flutter

dart

I need to know which Tab is clicked. Therefore I added the SingleTickerProviderStateMixin, created a TabController field in my State and added a Listener (huge boilerplate IMHO...).

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
  TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = new TabController(length: 2, vsync: this);
    _tabController.addListener(() {
      if (_tabController.indexIsChanging) {
        print('click, ${_tabController.index}');
      }
    });
  }

  @override
  Widget build(BuildContext context) {
//...
}
}

However every time I click a Tab, multiple statements are printed instead of just one, as I expected. Why is indexIsChanging not working?

like image 680
ynotu. Avatar asked Feb 16 '20 19:02

ynotu.


People also ask

How does tab detect change in flutter?

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

How do you use tab controller on flutter?

To create a tab in it, create a tab list and create an object of its TabController. TabController _tabController; Initalize the TabController inside the initState() method and also override it in the dispose() method. The CustomTabBar results can be seen in the image.

How do I get the current tab index in flutter?

By using DefaultTabController, you can get the current index easily whether the user changes tabs by Swiping or Tapping on the tab bar. You must wrap your Scaffold inside of a Builder and you can then retrieve the tab index with DefaultTabController. of(context).

How do I disable a tab in flutter?

Use IgnorePointer. It will disable the click action of tabs.


2 Answers

Reason From https://github.com/flutter/flutter/issues/13848

Source code of TabController , notifyListeners() call twice https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/material/tab_controller.dart#L162

if (duration != null) {
      _indexIsChangingCount += 1;
      notifyListeners(); // Because the value of indexIsChanging may have changed.
      _animationController
        .animateTo(_index.toDouble(), duration: duration, curve: curve)
        .whenCompleteOrCancel(() {
          _indexIsChangingCount -= 1;
          notifyListeners();
        });

Solution To print current index only one time
code snippet from https://github.com/flutter/flutter/issues/13848#issuecomment-486051402

@override
  void initState() {
    super.initState();
    _tabController = TabController(vsync: this, length: choices.length)
      ..addListener(() {
        if(_tabController.indexIsChanging) {
          print("tab is animating. from active (getting the index) to inactive(getting the index) ");
        }else {
          //tab is finished animating you get the current index
          //here you can get your index or run some method once.
          print(_tabController.index);
        }
      });
  }

working demo

enter image description here

full test code

import 'package:flutter/material.dart';

class AppBarBottomSample extends StatefulWidget {
  @override
  _AppBarBottomSampleState createState() => _AppBarBottomSampleState();
}

class _AppBarBottomSampleState extends State<AppBarBottomSample>
    with SingleTickerProviderStateMixin {
  TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = TabController(vsync: this, length: choices.length)
      ..addListener(() {
        if(_tabController.indexIsChanging) {
          print("tab is animating. from active (getting the index) to inactive(getting the index) ");
        }else {
          //tab is finished animating you get the current index
          //here you can get your index or run some method once.
          print(_tabController.index);
        }
      });
  }

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

  void _nextPage(int delta) {
    final int newIndex = _tabController.index + delta;
    if (newIndex < 0 || newIndex >= _tabController.length) return;
    _tabController.animateTo(newIndex);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('AppBar Bottom Widget'),
          leading: IconButton(
            tooltip: 'Previous choice',
            icon: const Icon(Icons.arrow_back),
            onPressed: () {
              _nextPage(-1);
            },
          ),
          actions: <Widget>[
            IconButton(
              icon: const Icon(Icons.arrow_forward),
              tooltip: 'Next choice',
              onPressed: () {
                _nextPage(1);
              },
            ),
          ],
          bottom: PreferredSize(
            preferredSize: const Size.fromHeight(48.0),
            child: Theme(
              data: Theme.of(context).copyWith(accentColor: Colors.white),
              child: Container(
                height: 48.0,
                alignment: Alignment.center,
                child: TabPageSelector(controller: _tabController),
              ),
            ),
          ),
        ),
        body: TabBarView(
          controller: _tabController,
          children: choices.map((Choice choice) {
            return Padding(
              padding: const EdgeInsets.all(16.0),
              child: ChoiceCard(choice: choice),
            );
          }).toList(),
        ),
      ),
    );
  }
}

class Choice {
  const Choice({this.title, this.icon});

  final String title;
  final IconData icon;
}

const List<Choice> choices = const <Choice>[
  const Choice(title: 'CAR', icon: Icons.directions_car),
  const Choice(title: 'BICYCLE', icon: Icons.directions_bike),
  const Choice(title: 'BOAT', icon: Icons.directions_boat),
  const Choice(title: 'BUS', icon: Icons.directions_bus),
  const Choice(title: 'TRAIN', icon: Icons.directions_railway),
  const Choice(title: 'WALK', icon: Icons.directions_walk),
];

class ChoiceCard extends StatelessWidget {
  const ChoiceCard({Key key, this.choice}) : super(key: key);

  final Choice choice;

  @override
  Widget build(BuildContext context) {
    final TextStyle textStyle = Theme.of(context).textTheme.display1;
    return Card(
      color: Colors.white,
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Icon(choice.icon, size: 128.0, color: textStyle.color),
            Text(choice.title, style: textStyle),
          ],
        ),
      ),
    );
  }
}

void main() {
  runApp(AppBarBottomSample());
}
like image 116
chunhunghan Avatar answered Oct 20 '22 19:10

chunhunghan


To prevent calling a method twice, you can add this condition,

if(!controller.indexIsChanging){ //perform your operation }
like image 41
Vaidehee Vala Avatar answered Oct 20 '22 20:10

Vaidehee Vala