Hey there 😊

If you like what you read, feel free to …

🥗Buy me a salad
Share this 💬

Navigate Without Animation

Using a subtle transition

Navigate Without Animation
No time to read?

You can either

  • Use a PageRouteBuilder in which you set transitionDuration and reverseTransitionDuration to Duration.zero


  • Extend MaterialPageRoute and override buildTransitions() by letting it return the child widget instantly

Using the go_router package, you can extend CustomTransitionPage and override transitionsBuilder().

By default, Flutter configures your navigation in a way that the transition from one screen to another is presented with a slide or fade animation. While it can be a decent way to navigate for most of the scenarios, there are situations in which you might not want this to happen. Let’s see how we can change this predefined behavior.

Anonymous routes

The most basic and a very common way to navigate is by using an anonymous route. This means, we use Navigator.push() and thus create the Route during this call. The route is called anonymous because compared to Navigator.push(), this route does not have a name - similar to an anonymous function.

But if we create the Route in the moment of calling Navigator.push(), how can we configure the transition?

If we use a MaterialPageRoute like this:

2  context,
3  MaterialPageRoute(builder: (BuildContext context) => OtherScreen()),

we end up with a slide animation on iOS and macOS and a fade animation on Android. This is because the animation inside the MaterialPageRoute is determined by the PageTransitionsTheme which is used in the respective buildTransitions() function.:

 1class PageTransitionsTheme with Diagnosticable {
 2  /// Constructs an object that selects a transition based on the platform.
 3  ///
 4  /// By default the list of builders is: [ZoomPageTransitionsBuilder]
 5  /// for [TargetPlatform.android], and [CupertinoPageTransitionsBuilder] for
 6  /// [TargetPlatform.iOS] and [TargetPlatform.macOS].
 7  const PageTransitionsTheme({ Map<TargetPlatform, PageTransitionsBuilder> builders = _defaultBuilders }) : _builders = builders;
 9  static const Map<TargetPlatform, PageTransitionsBuilder> _defaultBuilders = <TargetPlatform, PageTransitionsBuilder>{
10    TargetPlatform.android: ZoomPageTransitionsBuilder(),
11    TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
12    TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
13  };

Luckily, we have another option. Let’s have a look at the inheritance of MaterialPageRoute:


Since the second parameter of the push() method is of type Route, we just have to find another class that extends Route but is more configurable.

While we can not directly configure the Route regarding its transition, we can certainly do that using a descendant of this class: the PageRouteBuilder, which also extends Route as this inheritance order shows:


In order to prevent the animation, we use the named arguments transitionDuration and reverseTransitionDuration, which we both set to Duration.zero:

 2  context, 
 3  PageRouteBuilder(
 4    pageBuilder: (BuildContext context, Animation<double> animation1, Animation<double> animation2) {
 5      return OtherScreen();
 6    },
 7    transitionDuration: Duration.zero,
 8    reverseTransitionDuration: Duration.zero,
 9  ),

By doing this, we ensure that neither navigating to nor returning from the screen results in an animation.

Generated routes

A better way to organize routes instead of directly using the push() method is to use generated routes. The idea is to have a single place where all routes are defined. By giving every route a name, you can refer to that route from everywhere inside your code (by using pushNamed()) instead of redefining this navigation logic multiple times.

For this approach, we will generate a separate class that extends MaterialPageRoute. Instead of creating an animation inside the buildTransitions() function, we just instantly return the child:

 1class UnanimatedPageRoute<T> extends MaterialPageRoute<T> {
 2  UnanimatedPageRoute({
 3    required Widget Function(BuildContext) builder,
 4    RouteSettings? settings,
 5    bool maintainState = true,
 6    bool fullscreenDialog = false,
 7  }) : super(
 8          builder: builder,
 9          settings: settings,
10          maintainState: maintainState,
11          fullscreenDialog: fullscreenDialog,
12        );
14  @override
15  Widget buildTransitions(
16    BuildContext context,
17    Animation<double> animation,
18    Animation<double> secondaryAnimation,
19    Widget child,
20  ) {
21    return child;
22  }

In the MaterialApp widget inside the onGenerateRoute function, we can now use this new class for navigation:

2  onGenerateRoute: (settings) {
3    if (settings.name == OtherScreen.routeName) {
4      return UnanimatedPageRoute(
5        builder: (context) => OtherScreen(),
6      );
7    }
8  },

We could even decide per route if it’s animated or not by creating a function that expects a bool parameter and uses the one or the other Route class depending on this parameter:

 1  Route<T?> buildPageRoute<T>(
 2    Widget child,
 3    bool animated,
 4  ) {
 5    if (animated) {
 6      return CupertinoPageRoute<T?>(
 7        builder: (BuildContext context) => child,
 8      );
 9    }
11    return UnanimatedPageRoute<T?>(
12      builder: (BuildContext context) => child,
13    );
14  }
 2  onGenerateRoute: (settings) {
 3    if (settings.name == OtherScreen.routeName) {
 4      return buildPageRoute(
 5        builder: (context) => OtherScreen(),
 6        false // or true if we want it to be slided in
 7      );
 8    }
 9  },

We can utilize this solution for anonymous routing using push() as well:

2  context,
3  UnanimatedPageRoute(builder: (BuildContext context) => OtherScreen()),


With the go_router package we get a high level API for declarative routing in Flutter. The package wraps around the Navigator 2.0 API. Instead of routes, we deal with pages there.
So in order to configure go_router in a similar fashion like we did above, we create a function called buildPageWithoutAnimation() that ignores the given animation arguments and returns the child immediately.

 1CustomTransitionPage<T> buildPageWithoutAnimation({
 2  required BuildContext context, 
 3  required GoRouterState state, 
 4  required Widget child,
 5}) {
 6  return CustomTransitionPage<T>(
 7    key: state.pageKey,
 8    child: child,
 9    transitionsBuilder: (context, animation, secondaryAnimation, child) => child
10  );
16  path: '/other-screen',
17  builder: (BuildContext context, GoRouterState state) => const OtherScreen(),
18  pageBuilder: (BuildContext context, GoRouterState state) => buildPageWithoutAnimation<void>(
19    context: context, 
20    state: state, 
21    child: OtherScreen(),
22  ),


The concept of how to prevent an animation during the transition from one screen to another is pretty simple: instead of returning an animation in the respective builder function, we return the new screen (widget) directly.

When dealing with push(), we just set the transition duration to zero which leaves no time for the animation.

Comment this 🤌

You are replying to 's commentRemove reference