Flutter can't keep the state of tabs using PageTransitionSwitcher

Issue

I am struggling with animations package and I want to use animation with BottomNavigationBar.
Without animation, I can save my state using IndexedStack.
If I wrap IndexedStack inside PageTransitionSwitcher it doesn’t work. In particular:

  • animations are not showing but state is kept
  • if I use key property of my IndexedStack, animations are showing but state is not working.

How can i fix it? I don’t know how to set up keys.
Thank you very much!!

class MainScreen extends StatefulWidget {
  static String id = 'loading_screen';

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

class _MainScreenState extends State<MainScreen> {
  int _selectedPage = 0;
  List<Widget> pageList = List<Widget>();

  @override
  void initState() {
    pageList.add(PrimoTab());
    pageList.add(SecondoTab());
    pageList.add(TerzoTab());
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Bottom tab'),
      ),
      body: PageTransitionSwitcher(
        transitionBuilder: (child, primaryAnimation, secondaryAnimation) {
          return SharedAxisTransition(
            animation: primaryAnimation,
            secondaryAnimation: secondaryAnimation,
            child: child,
            transitionType: SharedAxisTransitionType.horizontal,
          );
        },
        child: IndexedStack(
          index: _selectedPage,
          children: pageList,
          //key: ValueKey<int>(_selectedPage), NOT WORKING
        ),
      ),
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.directions_car),
            label: 'First Page',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.airplanemode_active),
            label: 'Second Page',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.directions_bike),
            label: 'Third Page',
          ),
        ],
        currentIndex: _selectedPage,
        selectedItemColor: Colors.lightGreen,
        unselectedItemColor: Colors.lightBlueAccent,
        onTap: _onItemTapped,
      ),
    );
  }

  void _onItemTapped(int index) {
    setState(() {
      _selectedPage = index;
    });
  }
}

Each tab is just a column with two text widgets, a button, and a text counter (for testing the state of each tab):

class PrimoTab extends StatefulWidget {

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

class _PrimoTabState extends State<PrimoTab> {
  int cont = -1;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: <Widget>[
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Text(
              'TAB 1 - TEXT 1',
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Text(
              'TAB 1 - TEXT 2',
            ),
          ),
          FlatButton(
            onPressed: () {
              setState(() {
                cont++;
              });
            },
            child: Text("CLICK"),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: Text(
              'Valore contatore $cont',
            ),
          ),
        ],
      ),
    );
  }
}

UPDATE 1: Using just

pageList[_selectedPage],

instead of

IndexedStack(
...
)

but not working (animations ok but state is not kept)

UPDATE 2 WITH SOLUTION (main.dart):

void main() {
  runApp(
    MaterialApp(
      home: MainScreen(),
    ),
  );
}

class MainScreen extends StatefulWidget {
  static String id = 'loading_screen';

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

class _MainScreenState extends State<MainScreen> {
  int _selectedPage = 0;
  List<Widget> pageList = List<Widget>();

  @override
  void initState() {
    pageList.add(PrimoTab());
    pageList.add(SecondoTab());
    pageList.add(TerzoTab());
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Bottom tab'),
      ),
      body: AnimatedIndexedStack(
        index: _selectedPage,
        children: pageList,
      ),
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.directions_car),
            label: 'First Page',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.airplanemode_active),
            label: 'Second Page',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.directions_bike),
            label: 'Third Page',
          ),
        ],
        currentIndex: _selectedPage,
        selectedItemColor: Colors.lightGreen,
        unselectedItemColor: Colors.lightBlueAccent,
        onTap: _onItemTapped,
      ),
    );
  }

  void _onItemTapped(int index) {
    setState(() {
      _selectedPage = index;
    });
  }
}

class AnimatedIndexedStack extends StatefulWidget {
  final int index;
  final List<Widget> children;

  const AnimatedIndexedStack({
    Key key,
    this.index,
    this.children,
  }) : super(key: key);

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

class _AnimatedIndexedStackState extends State<AnimatedIndexedStack>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation<double> _animation;
  int _index;

  @override
  void initState() {
    _controller = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 150),
    );
    _animation = Tween(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Curves.ease,
      ),
    );

    _index = widget.index;
    _controller.forward();
    super.initState();
  }

  @override
  void didUpdateWidget(AnimatedIndexedStack oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.index != _index) {
      _controller.reverse().then((_) {
        setState(() => _index = widget.index);
        _controller.forward();
      });
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) {
        return Opacity(
          opacity: _controller.value,
          child: Transform.scale(
            scale: 1.015 - (_controller.value * 0.015),
            child: child,
          ),
        );
      },
      child: IndexedStack(
        index: _index,
        children: widget.children,
      ),
    );
  }
}

Solution

There are few ways to fix your situation. None of them is perfectly simple and there already lots of discussion here and here, trying to merge IndexedStack with PageTransitionSwitcher. No solution so far I saw.

I collect following possible ways to achieve this:

  1. Store state somewhere else and pass into child. I haven’t seen any method can stop PageTransitionSwitcher from rebuilding child widget. If you don’t mine the child widget rebuild, it may be the most straight forward method to do with this.

  2. Use Custom IndexedStack with animation. like this and this. It works well with the feature in IndexStack that children won’t rebuild, but the animation is not as good as PageTransitionSwitcher and it can only show 1 widget in one time.

Answered By – yellowgray

Answer Checked By – David Goodson (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *