flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
166.19k stars 27.49k forks source link

[animations] PageTransitionSwitcher doesn't work well with IndexedStack #56662

Open hiroshihorie opened 4 years ago

hiroshihorie commented 4 years ago

I want to use PageTransitionSwitcher with IndexedStack to display my tab pages. If I don't set a ValueKey to IndexedStack, it doesn't animate but the IndexStack works well. If I do set a ValueKey, it animates but IndexedStack rebuilds all the time and causes the tab page's state to be lost. Which defeats the purpose of using IndexedStack.

I did try to use AutomaticKeepAliveClientMixin, but still rebuilds the tabs, and I just want to avoid it anyways since IndexedStack keeps my code cleaner.

I believe there are many users that want to use this combination since it is a very common pattern and it shouldn't be so complicated.

What is the best solution?

Regards Hiroshi

            PageTransitionSwitcher(
              duration: const Duration(milliseconds: 300),
              reverse: newTabIndex < tabIndexNotifier.previous,
              transitionBuilder: (
                Widget child,
                Animation<double> animation,
                Animation<double> secondaryAnimation,
              ) =>
                  SharedAxisTransition(
                child: child,
                animation: animation,
                secondaryAnimation: secondaryAnimation,
                transitionType: SharedAxisTransitionType.horizontal,
              ),
              child: IndexedStack(
                key: ValueKey(newTabIndex),
                index: newTabIndex,
                children: _pageWidgets,
              ),
            ),
TahaTesser commented 4 years ago

Hi @HiroshiHorie can you please provide your flutter doctor -v? Also, to better address the issue, would be helpful if you could post a complete minimal code sample to reproduce the problem Thank you

hiroshihorie commented 4 years ago

It's also similar to the case of using AnimatedSwitcher with IndexedStack #39398 #48217

[✓] Flutter (Channel stable, v1.17.0, on Mac OS X 10.15.5 19F72f, locale ja-JP)
    • Flutter version 1.17.0 at /Users/hiroshi/flutter
    • Framework revision e6b34c2b5c (6 days ago), 2020-05-02 11:39:18 -0700
    • Engine revision 540786dd51
    • Dart version 2.8.1

[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
    • Android SDK at /Users/hiroshi/Library/Android/sdk
    • Platform android-29, build-tools 29.0.2
    • Java binary at: /Users/hiroshi/Library/Application
      Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/192.6392135/Android
      Studio.app/Contents/jre/jdk/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 1.8.0_212-release-1586-b4-5784211)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 11.4.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Xcode 11.4.1, Build version 11E503a
    • CocoaPods version 1.9.1

[✓] Android Studio (version 3.6)
    • Android Studio at /Users/hiroshi/Library/Application
      Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/192.6392135/Android Studio.app/Contents
    • Flutter plugin version 45.1.1
    • Dart plugin version 192.7761
    • Java version OpenJDK Runtime Environment (build 1.8.0_212-release-1586-b4-5784211)

[✓] VS Code (version 1.43.0)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.8.1

[✓] Connected device (1 available)
    • Android SDK built for x86 • emulator-5554 • android-x86 • Android 10 (API 29) (emulator)

• No issues found!
HappyGhostz commented 4 years ago

This is a problem, PageTransitionSwitcher will rebuild the page when switching pages, and twice.

doppio commented 4 years ago

Is there any workaround for this? It would be nice to use the animation transitions provided by Flutter (e.g. FadeThroughTransition) along with IndexedStack, the recommended way to maintain state when switching between top-level pages with a bottom navigation bar.

jamiewest commented 4 years ago

I am having the same problem as @doppio, I would really like to be able to use this with a bottom navigation bar.

sooxt98 commented 4 years ago

I think the only way to make this work is to wait flutter team rewrite that animations package 😂

jamiewest commented 4 years ago

I've had better luck using the animations package along with the new Pages API where I am able to set the page route transition when using a BottomNavigationBar or NavigationRail.

sooxt98 commented 4 years ago

Ping creator @goderbauer , any idea how to fix it?

JonathanPeterCole commented 4 years ago

I recently ran into this issue and this is the best solution I've come up with so far. It's not ideal as it still involves two rebuilds, but the state is maintained:

import 'package:flutter/widgets.dart';

/// Based on the PageTransitionSwitcher from the animations package, this widget
/// allows you to transition between an array of widgets using entry and exit
/// animations whilst maintaining their state.
class IndexedTransitionSwitcher extends StatefulWidget {
  /// Creates an [IndexedTransitionSwitcher].
  const IndexedTransitionSwitcher({
    @required this.index, 
    @required this.children, 
    @required this.transitionBuilder,
    this.reverse = false,
    this.duration = const Duration(milliseconds: 300)});

  /// The index of the child to show.
  final int index;

  /// The widgets to switch between.
  final List<Widget> children;

  /// A function to wrap the child in primary and secondary animations.
  /// 
  /// When the index changes, the new child will animate in with the primary
  /// animation, and the old widget will animate out with the secondary 
  /// animation.
  final Widget Function(
    Widget child, 
    Animation<double> primaryAnimation, 
    Animation<double> secondaryAnimation
  ) transitionBuilder;

  /// The duration of the transition.
  final Duration duration;

  /// Whether or not the transition should be reversed.
  /// 
  /// If true, the new child will animate in behind the oWld child with the
  /// secondary animation running in reverse, whilst the old child animates 
  /// out with the primary animation playing in reverse.
  final bool reverse;

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

class _IndexedTransitionSwitcherState extends State<IndexedTransitionSwitcher> 
    with TickerProviderStateMixin {

  List<_ChildEntry> _childEntries;

  @override
  void initState() {
    super.initState();
    // Create the page entries
    this._childEntries = widget.children
      .asMap().entries
      .map((entry) => _createPageEntry(entry.key, entry.value))
      .toList();
  }

  @override
  void didUpdateWidget(IndexedTransitionSwitcher oldWidget) {
    super.didUpdateWidget(oldWidget);
    // Transition if the index has changed
    if (widget.index != oldWidget.index) {
      _ChildEntry newChild = _childEntries
        .where((entry) => entry.index == widget.index).first;
      _ChildEntry oldChild = _childEntries
        .where((entry) => entry.index == oldWidget.index).first;
      // Animate the children
      if (widget.reverse) {
        // Animate in the new child
        newChild.primaryController.value = 1;
        newChild.secondaryController.reverse(from: 1);
        // Animate out the old child and unstage it when the animation is complete
        oldChild.secondaryController.value = 0;
        oldChild.primaryController.reverse(from: 1).then((value) => setState(() {
          oldChild.onStage = false;
          oldChild.primaryController.reset();
          oldChild.secondaryController.reset();
        }));
      } else {
        // Animate in the new child
        newChild.secondaryController.value = 0;
        newChild.primaryController.forward(from: 0);
        // Animate out the old child and unstage it when the animation is complete
        oldChild.primaryController.value = 1;
        oldChild.secondaryController.forward().then((value) => setState(() {
          oldChild.onStage = false;
          oldChild.primaryController.reset();
          oldChild.secondaryController.reset();
        }));
      }
      // Reorder the stack and set onStage to true for the new child
      _childEntries.remove(newChild);
      _childEntries.remove(oldChild);
      _childEntries.addAll(widget.reverse ? [newChild, oldChild] 
        : [oldChild, newChild]);
      newChild.onStage = true;
    }
  }

  _ChildEntry _createPageEntry(int index, Widget child) {
    // Prepare the animation controllers
    final AnimationController primaryController = AnimationController(
      value: widget.index == index ? 1.0 : 0,
      duration: widget.duration,
      vsync: this,
    );
    final AnimationController secondaryController = AnimationController(
      duration: widget.duration,
      vsync: this,
    );
    // Create the page entry
    return _ChildEntry(
      key: UniqueKey(),
      index: index,
      primaryController: primaryController,
      secondaryController: secondaryController,
      transitionChild: widget.transitionBuilder(child, primaryController, secondaryController),
      onStage: widget.index == index
    );
  }

  @override
  void dispose() {
    // Dispose of the animation controllers
    for (_ChildEntry entry in _childEntries) {
      entry.dispose();
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) => Stack(
    alignment: Alignment.center,
    fit: StackFit.expand,
    children: _childEntries
      .map<Widget>((entry) => Offstage(
        key: entry.key,
        offstage: !entry.onStage,
        child: entry.transitionChild,
      ))
      .toList(),
  );
}

/// Internal representation of a child.
class _ChildEntry {
  _ChildEntry({
    @required this.index,
    @required this.key,
    @required this.primaryController,
    @required this.secondaryController,
    @required this.transitionChild,
    @required this.onStage
  });

  /// The child index.
  final int index;

  /// The key to maintain widget state when moving in the tree.
  final Key key;

  /// The entry animation controller.
  final AnimationController primaryController;

  /// The exit animation controller.
  final AnimationController secondaryController;

  /// The child widget wrapped in the transition.
  final Widget transitionChild;

  /// Whether or not the child should be rendered.
  bool onStage;

  /// Dispose of the animation controllers
  void dispose() {
    primaryController.dispose();
    secondaryController.dispose();
  }
}
nt4f04uNd commented 3 years ago

I think that it would be better to:

matejkramny commented 3 years ago

I've managed to make it work using PageStorageKey. So it preserves state and animates using PageTransitionSwitcher.

sooxt98 commented 3 years ago

@matejkramny may i know how you use PageStorageKey? did you apply straight onto the PageTransitionSwitcher's key? Or on every single child page widget as well?

matejkramny commented 3 years ago

@sooxt98

.. with PageStorage up the tree somewhere
        child: PageTransitionSwitcher(
          reverse: condition,
          transitionBuilder: (child, animation, animation2) {
            return SharedAxisTransition(
              animation: animation,
              secondaryAnimation: animation2,
              transitionType: SharedAxisTransitionType.scaled,
              child: child,
            );
          },
          child: condition
              ? PageTwo()
              : PageOne(
                  key: PageStorageKey<String>('somekey'),
                ),
        ),

For my usecase, PageTwo is a detail page from PageOne. Switching back and forth between these pages, I needed PageOne to be kept "alive". The widget is rebuilt but is actually resurrected from the Storage bucket.

globalwebforce commented 2 years ago

Any update on this one?

twoco commented 2 years ago

Similar issue with tabs (DefaultTabController). It would be great if Flutter provides a simple build-in feature directly to the related widget to keep the state of children. My story in short: I started with a BottomNavigationBar navigation. But want transition between navigation. I ended up with IndexedStack to keep the state of each tab / page. Then I found AnimatedSwitcher to bring some life to it. But somehow it requires the key property for IndexedStack. It works, but the state is no longer preserved between transitions. It is destroyed and created. Then I switched to Tabs, because I want a slide animation anyway and thought tabs will not be destroyed when switching tabs. Ok, this is just my interpretation, as a frontend developer from Angular. ^^ The current Flutter behavior is like lazy load on Angular. But whatever... I think it would be great if Flutter provides an option to keep childrean alive or not (as now). Flutter is great, but it seems that it lacks of a nice page navigation. But I'm new to Flutter. I am currently struggling to implement the basic navigation with transition without losing page state. I want to keep all pages open, but only show each one individually.

shubhamsinghshubham777 commented 1 month ago

Do we have any updates on this, please? This can be a crucial piece of code to bring normal apps to life with some easy-to-use animations.