Navigate without animation in Flutter
Published: 2022-10-23 20:21:06

By default, Flutter configures your [navigation](https://docs.flutter.dev/development/ui/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 {#anonymous-routes}

The most basic and a very common way to navigate is by using an anonymous route. This means, we use [`Navigator.push()`](https://api.flutter.dev/flutter/widgets/Navigator/push.html) and thus create the [`Route`](https://api.flutter.dev/flutter/widgets/Route-class.html) during this call. The route is called *anonymous* because compared to [`Navigator.push()`](https://api.flutter.dev/flutter/widgets/Navigator/pushNamed.html), 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: 

```dart
Navigator.push(
  context,
  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()`](https://api.flutter.dev/flutter/material/MaterialRouteTransitionMixin/buildTransitions.html) function.:

```dart
class PageTransitionsTheme with Diagnosticable {
  /// Constructs an object that selects a transition based on the platform.
  ///
  /// By default the list of builders is: [ZoomPageTransitionsBuilder]
  /// for [TargetPlatform.android], and [CupertinoPageTransitionsBuilder] for
  /// [TargetPlatform.iOS] and [TargetPlatform.macOS].
  const PageTransitionsTheme({ Map<TargetPlatform, PageTransitionsBuilder> builders = _defaultBuilders }) : _builders = builders;

  static const Map<TargetPlatform, PageTransitionsBuilder> _defaultBuilders = <TargetPlatform, PageTransitionsBuilder>{
    TargetPlatform.android: ZoomPageTransitionsBuilder(),
    TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
    TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
  };
```

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

[Object](https://api.flutter.dev/flutter/dart-core/Object-class.html) → [Route](https://api.flutter.dev/flutter/widgets/Route-class.html) → [OverlayRoute](https://api.flutter.dev/flutter/widgets/OverlayRoute-class.html) → [TransitionRoute](https://api.flutter.dev/flutter/widgets/TransitionRoute-class.html) → [ModalRoute](https://api.flutter.dev/flutter/widgets/ModalRoute-class.html) → [PageRoute](https://api.flutter.dev/flutter/widgets/PageRoute-class.html) → [MaterialPageRoute](https://api.flutter.dev/flutter/material/MaterialPageRoute-class.html)

Since the second parameter of the [`push()`](https://api.flutter.dev/flutter/widgets/Navigator/push.html) 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`](https://api.flutter.dev/flutter/widgets/PageRouteBuilder-class.html), which also extends `Route` as this inheritance order shows:

[Object](https://api.flutter.dev/flutter/dart-core/Object-class.html) → [Route](https://api.flutter.dev/flutter/widgets/Route-class.html) → [OverlayRoute](https://api.flutter.dev/flutter/widgets/OverlayRoute-class.html) → [TransitionRoute](https://api.flutter.dev/flutter/widgets/TransitionRoute-class.html) → [ModalRoute](https://api.flutter.dev/flutter/widgets/ModalRoute-class.html) → [PageRoute](https://api.flutter.dev/flutter/widgets/PageRoute-class.html) → [PageRouteBuilder](https://api.flutter.dev/flutter/widgets/PageRouteBuilder-class.html)

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

```dart
Navigator.push(
  context, 
  PageRouteBuilder(
    pageBuilder: (BuildContext context, Animation<double> animation1, Animation<double> animation2) {
      return OtherScreen();
    },
    transitionDuration: Duration.zero,
    reverseTransitionDuration: Duration.zero,
  ),
);
```

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

## Generated routes {#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: 

```dart
class UnanimatedPageRoute<T> extends MaterialPageRoute<T> {
  UnanimatedPageRoute({
    required Widget Function(BuildContext) builder,
    RouteSettings? settings,
    bool maintainState = true,
    bool fullscreenDialog = false,
  }) : super(
          builder: builder,
          settings: settings,
          maintainState: maintainState,
          fullscreenDialog: fullscreenDialog,
        );

  @override
  Widget buildTransitions(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    return child;
  }
}
```

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

```dart
MaterialApp(
  onGenerateRoute: (settings) {
    if (settings.name == OtherScreen.routeName) {
      return UnanimatedPageRoute(
        builder: (context) => OtherScreen(),
      );
    }
  },
)
```

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:

```dart
  Route<T?> buildPageRoute<T>(
    Widget child,
    bool animated,
  ) {
    if (animated) {
      return CupertinoPageRoute<T?>(
        builder: (BuildContext context) => child,
      );
    }

    return UnanimatedPageRoute<T?>(
      builder: (BuildContext context) => child,
    );
  }
```

```dart
MaterialApp(
  onGenerateRoute: (settings) {
    if (settings.name == OtherScreen.routeName) {
      return buildPageRoute(
        builder: (context) => OtherScreen(),
        false // or true if we want it to be slided in
      );
    }
  },
)
```

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

```dart
Navigator.push(
  context,
  UnanimatedPageRoute(builder: (BuildContext context) => OtherScreen()),
);
```

## go_router

With the [go_router](https://pub.dev/packages/go_router) package we get a high level API for declarative routing in Flutter. The package wraps around the [Navigator 2.0 API](https://api.flutter.dev/flutter/widgets/Navigator-class.html#using-the-pages-api). Instead of routes, we deal with [`pages`](https://api.flutter.dev/flutter/widgets/Navigator/pages.html) 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.

```dart
CustomTransitionPage<T> buildPageWithoutAnimation({
  required BuildContext context, 
  required GoRouterState state, 
  required Widget child,
}) {
  return CustomTransitionPage<T>(
    key: state.pageKey,
    child: child,
    transitionsBuilder: (context, animation, secondaryAnimation, child) => child
  );
}

…

GoRoute(
  path: '/other-screen',
  builder: (BuildContext context, GoRouterState state) => const OtherScreen(),
  pageBuilder: (BuildContext context, GoRouterState state) => buildPageWithoutAnimation<void>(
    context: context, 
    state: state, 
    child: OtherScreen(),
  ),
),
```

## Conclusion

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.