Flutter no puede mantener el estado de las pestañas usando PageTransitionSwitcher

Nov 28 2020

Estoy luchando con el paquete de animaciones y quiero usar la animación con BottomNavigationBar. Sin animación, puedo guardar mi estado usando IndexedStack. Si envuelvo IndexedStack dentro de PageTransitionSwitcher, no funciona. En particular:

  • las animaciones no se muestran pero el estado se mantiene
  • si utilizo la propiedad clave de mi IndexedStack, se muestran animaciones pero el estado no funciona.

¿Cómo puedo arreglarlo? No sé cómo configurar las claves. ¡¡Muchas gracias!!

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

Cada pestaña es solo una columna con dos widgets de texto, un botón y un contador de texto (para probar el estado de cada pestaña):

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

ACTUALIZACIÓN 1: Usando solo

pageList[_selectedPage],

en vez de

IndexedStack(
...
)

pero no funciona (las animaciones están bien pero el estado no se mantiene)

ACTUALIZACIÓN 2 CON SOLUCIÓN (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,
      ),
    );
  }
}

Respuestas

1 yellowgray Nov 29 2020 at 06:27

Hay algunas formas de solucionar su situación. Ninguno de ellos es muy simple y ya mucha discusión aquí y aquí , tratando de combinar IndexedStackcon PageTransitionSwitcher. No vi ninguna solución hasta ahora.

Recopilo las siguientes formas posibles de lograr esto:

  1. Almacene el estado en otro lugar y pase al niño. No he visto ningún método que pueda detener la PageTransitionSwitcherreconstrucción del widget secundario. Si no extrae la reconstrucción del widget secundario , puede ser el método más sencillo para hacerlo.

  2. Utilice Personalizado IndexedStackcon animación. como este y este . Funciona bien con la característica de IndexStack que los niños no reconstruyen, pero la animación no es tan buena PageTransitionSwitchery solo puede mostrar 1 widget a la vez.