tl;dr
You can either
- Use a
PageRouteBuilder
in which you settransitionDuration
andreverseTransitionDuration
toDuration.zero
or
- Extend
MaterialPageRoute
and overridebuildTransitions()
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:
1Navigator.push(
2 context,
3 MaterialPageRoute(builder: (BuildContext context) => OtherScreen()),
4);
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;
8
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
:
Object → Route → OverlayRoute → TransitionRoute → ModalRoute → PageRoute → 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:
Object → Route → OverlayRoute → TransitionRoute → ModalRoute → PageRoute → PageRouteBuilder
In order to prevent the animation, we use the named arguments transitionDuration
and reverseTransitionDuration
, which we both set to Duration.zero
:
1Navigator.push(
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 ),
10);
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 );
13
14 @override
15 Widget buildTransitions(
16 BuildContext context,
17 Animation<double> animation,
18 Animation<double> secondaryAnimation,
19 Widget child,
20 ) {
21 return child;
22 }
23}
In the MaterialApp
widget inside the onGenerateRoute
function, we can now use this new class for navigation:
1MaterialApp(
2 onGenerateRoute: (settings) {
3 if (settings.name == OtherScreen.routeName) {
4 return UnanimatedPageRoute(
5 builder: (context) => OtherScreen(),
6 );
7 }
8 },
9)
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 }
10
11 return UnanimatedPageRoute<T?>(
12 builder: (BuildContext context) => child,
13 );
14 }
1MaterialApp(
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 },
10)
We can utilize this solution for anonymous routing using push()
as well:
1Navigator.push(
2 context,
3 UnanimatedPageRoute(builder: (BuildContext context) => OtherScreen()),
4);
go_router
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 );
11}
12
13…
14
15GoRoute(
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 ),
23),
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.
Comment this 🤌