Route is an abstract class in Flutter that represents a separate screen or page in the Navigator's navigation history. Concrete implementations of Route — MaterialPageRoute, CupertinoPageRoute and PlatformRoute — determine how the screen is displayed and what animation is used during the transition. Unlike a regular widget, Route has its own lifecycle with methods didPush, didPop, didReplace and didChangeNext. According to the Flutter API Reference (2026), each Route manages ModalBarrier, accounts for platform-specific features (swipe-back on iOS) and ensures state isolation between screens.
Key Takeaways
Route is the foundation of the Flutter navigation system. Every screen that a user sees in an application is represented by a Route object located in the Navigator stack. Route abstracts the screen from the controlling code: Navigator adds and removes Route, while inside the Route is the interface that the user sees. This architecture separates responsibility between navigation and display.
Unlike simple widget replacement, Route provides services unavailable to regular widgets: ModalBarrier (background dimming when a dialog is open), enter and exit animation control, hardware back button handling on Android, and integration with Hero animation for smooth transitions between screens.
According to Flutter Cookbook (2026), Route is a key element for Hero animation: a Hero widget on one Route automatically animates the transition to a Hero widget on the next Route, creating a "flying" element effect. This is possible precisely because Route keeps both screens in the Overlay during animation.
Route is the foundation for both simple mobile applications (via Navigator 1.0) and complex scenarios with deep links (Navigator 2.0). In Navigator 2.0, Route represents a Page that RouterDelegate converts from the route configuration. Thus, understanding Route is essential for working with any Flutter navigation system — regardless of the chosen approach or package.
Flutter provides several built-in Route implementations, each adapting behavior to a specific platform. Choosing the right Route type affects user experience: Material animation on Android and Cupertino animation on iOS create the feel of a "native" application.
| Route Type | Animation | Platform | Features |
|---|---|---|---|
| MaterialPageRoute | Slide from bottom to top | Android, desktop | Shadow on transition, automatic SafeArea handling |
| CupertinoPageRoute | Slide from right to left | iOS, iPadOS | Gesture swipe-back, transparent background during transition |
| PlatformRoute | Automatic selection | All platforms | Selects type based on TargetPlatform |
| PageRouteBuilder | Custom | All platforms | Full control over animation via AnimationController |
MaterialPageRoute is the most commonly used Route implementation. It animates the new screen entering from bottom to top with gradual appearance. When exiting, the screen animates from top to bottom, returning to its original position. The toolbar (AppBar) and the screen body animate separately, creating a hierarchy effect.
CupertinoPageRoute mimics UINavigationController from iOS. The new screen enters from the right, covering the previous one. The key feature is support for interactive swipe-back gesture, implemented via CupertinoBackGestureDetector. This gesture is handled even mid-animation, providing natural behavior familiar to iPhone users.
Route has its own lifecycle, which differs from the lifecycle of a regular StatefulWidget. Understanding this cycle is necessary for proper data initialization, stream subscriptions, and resource release when closing a screen.
The Route lifecycle consists of four main stages. Transition — Route is created and animated on entry (didPush is called). Active — Route is fully displayed and interacting with the user. Inactive — another Route covers the current one (dialog, bottom sheet), but the Route remains in the stack. Disposed — Route is removed from the stack and destroyed, didPop and dispose are called.
Route lifecycle methods can be overridden in a custom implementation. For example, didPop is called when Route is removed from the stack — here you can save draft data. didChangeNext is called when the next Route in the stack has changed — useful for updating UI when navigation history changes.
According to Flutter API Route.didPop (2026), it is important not to confuse the Route lifecycle with the lifecycle of State inside Route. StatefulWidget inside Route has its own initState and dispose, which are called during the Transition and Disposed stages respectively. Route lives longer than its internal State — Route remains in the Overlay even when its widgets are temporarily hidden by another Route.
Route provides mechanisms for passing data both on input (when created) and on output (when finished). Proper data passing via Route eliminates the need for global variables and InheritedWidget, making navigation type-safe and predictable.
To pass data to a new screen, use the constructor of the receiving widget or the arguments parameter in Navigator.pushNamed. Inside Route, data is accessible via RouteSettings.arguments, which is stored in the Route object. This approach works for all Route types — MaterialPageRoute, CupertinoPageRoute and custom implementations.
To return data, use the second argument of Navigator.pop(context, result). Navigator.push returns Future
When calling Navigator.push directly with MaterialPageRoute, data is passed through the constructor of the target screen. MethodChannel is not used — this is pure Dart interaction. This approach is preferred for type-safe passing of complex objects.
Let's look at an example of creating a custom Route with its own animation and data passing. PageRouteBuilder allows you to define enter and exit animations with full control over the animation curve and duration.
// Custom Route with slide animation
Navigator.push(context, PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return DetailPage(productId: '42');
},
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(0.0, 0.3);
const end = Offset.zero;
final tween = Tween(begin: begin, end: end);
final offsetAnimation = animation.drive(tween);
return SlideTransition(position: offsetAnimation, child: child);
},
transitionDuration: const Duration(milliseconds: 400),
));
// Return data from screen
ElevatedButton(
onPressed: () => Navigator.of(context).pop({'selected': true, 'id': '42'}),
child: const Text('Select'),
);
// Get result on calling screen
final result = await Navigator.push(context, MaterialPageRoute(
builder: (context) => const SelectionPage(),
));
if (result != null) {
print('Selected: ${result['selected']}');
}
In the example, PageRouteBuilder defines a custom slide-from-bottom animation with transparency. transitionDuration sets the animation speed. The code also demonstrates result passing: the detail screen returns a Map with the user's selection, and the calling screen receives this data via Future from push. Route ensures complete isolation: drafts on the detail screen do not affect the list state.
Frequently Asked Questions
Route is an object that manages a screen at the navigation level: it stores animation, ModalBarrier and lifecycle. Widget is a description of a part of the interface. Route contains Widget inside itself, but also provides services (Overlay layer, Hero animation) unavailable to regular widgets. One Route can contain a complex hierarchy of widgets of any depth.
Use PageRouteBuilder with pageBuilder (screen construction) and transitionsBuilder (animation definition) parameters. In transitionsBuilder, animation (0.0–1.0) and secondaryAnimation are available for parallel animations. For full control, create a Route subclass and override buildPage, createAnimationController and buildTransitions, giving access to low-level AnimationController.
To pass complex objects, use the constructor of the target screen when calling Navigator.push directly or the arguments parameter with pushNamed. Make sure the object is serializable (Map, JSON or custom class). For type-safe passing in Flutter, use freezed or json_serializable models, which ensure correct deserialization when passing via RouteSettings.
Route.dispose is not called if Route remains in the Navigator stack. For example, when opening a new Route, the old Route goes into inactive state but is not destroyed — it remains in the stack for quick return. Dispose is called only when Route is removed from the stack via pop, pushReplacement or pushAndRemoveUntil. To release resources, use State's dispose inside Route, not Route's own dispose.
Use ModalRoute.of(context) to get the current Route from BuildContext. The ModalRoute.isActive property shows whether the Route is the currently visible screen. ModalRoute.isCurrent — true if the Route is the top of the stack. To observe stack changes, subscribe to Navigator observers via RouteAware and RouteObserver, which notify about active Route changes.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also