Navigation is a crucial aspect of any Flutter application. While Flutter provides a straightforward way to manage routes using Navigator, sometimes you need more control over how your application navigates between screens. Implementing custom routing logic allows you to create more dynamic, flexible, and complex navigation flows tailored to your specific needs. This can include conditional routing, deep linking, route guards, and more.
What is Custom Routing Logic in Flutter?
Custom routing logic involves taking control of the navigation stack and implementing your own rules and mechanisms to determine how and when routes are displayed. This means bypassing the standard Navigator methods and implementing your own routing management system. This approach provides enhanced flexibility but requires a deeper understanding of Flutter’s navigation system.
Why Implement Custom Routing Logic?
- Conditional Navigation: Navigate users based on certain conditions (e.g., authentication status).
- Deep Linking: Handle incoming links to navigate to specific parts of the application.
- Route Guards: Prevent users from accessing certain routes based on specific criteria.
- Complex Navigation Flows: Implement intricate navigation scenarios, like tab-based navigation with dynamic content.
- Enhanced Flexibility: More control over the app’s navigation structure and behavior.
How to Implement Custom Routing Logic in Flutter
Let’s explore how to implement custom routing logic in Flutter using different approaches.
Method 1: Using Navigator 2.0 (Declarative Navigation)
Navigator 2.0 introduces a more declarative and flexible way to manage navigation. It involves defining a RouterDelegate and a RouteInformationParser to handle app navigation state.
Step 1: Create a RouterDelegate
The RouterDelegate is responsible for building and managing the navigation stack. It listens to changes in the application’s state and updates the UI accordingly.
import 'package:flutter/material.dart';
class MyRouterDelegate extends RouterDelegate<RouteInformation, Object> {
final ValueNotifier<String?> _currentRoute = ValueNotifier(null);
bool isLoggedIn = false;
@override
Widget build(BuildContext context) {
return Navigator(
pages: [
MaterialPage(child: HomePage(), key: const ValueKey('home')),
if (isLoggedIn)
MaterialPage(child: ProfilePage(), key: const ValueKey('profile'))
else if (_currentRoute.value == '/login')
MaterialPage(child: LoginPage(), key: const ValueKey('login'))
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
_currentRoute.value = null;
return true;
},
);
}
@override
Future<void> setNewRoutePath(RoutePath configuration) async {
if (configuration is LoginPagePath) {
_currentRoute.value = '/login';
} else {
_currentRoute.value = null;
}
}
}
class RoutePath {}
class LoginPagePath extends RoutePath {}
In this example:
MyRouterDelegateextendsRouterDelegateand manages the navigation stack._currentRouteis aValueNotifierthat holds the current route.- The
buildmethod constructs theNavigatorwith a list ofMaterialPagewidgets based on the current state (isLoggedInand_currentRoute). setNewRoutePathupdates the_currentRoutebased on incoming route information.
Step 2: Create a RouteInformationParser
The RouteInformationParser is responsible for parsing the route information from the operating system (e.g., a deep link) and converting it into a usable data structure.
import 'package:flutter/material.dart';
class MyRouteInformationParser extends RouteInformationParser<RoutePath> {
@override
Future<RoutePath> parseRouteInformation(RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.uri);
if (uri.pathSegments.contains('login')) {
return LoginPagePath();
} else {
return RoutePath();
}
}
@override
RouteInformation restoreRouteInformation(RoutePath configuration) {
if (configuration is LoginPagePath) {
return const RouteInformation(uri: Uri.parse('/login'));
} else {
return const RouteInformation(uri: Uri.parse('/'));
}
}
}
In this example:
MyRouteInformationParserextendsRouteInformationParserand handles the parsing of route information.parseRouteInformationparses theUrifromRouteInformationand returns aRoutePath(LoginPagePathif the path contains ‘login’).restoreRouteInformationconverts theRoutePathback into aRouteInformationfor the system to understand.
Step 3: Integrate with MaterialApp.router
To use the custom router, you need to use the MaterialApp.router constructor.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final MyRouterDelegate _routerDelegate = MyRouterDelegate();
final MyRouteInformationParser _routeInformationParser = MyRouteInformationParser();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerDelegate: _routerDelegate,
routeInformationParser: _routeInformationParser,
backButtonDispatcher: RootBackButtonDispatcher(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {
_routerDelegate.isLoggedIn = true;
_routerDelegate.notifyListeners();
},
child: const Text('Go to Profile (Login)'),
),
ElevatedButton(
onPressed: () {
_routerDelegate.setNewRoutePath(LoginPagePath());
_routerDelegate.notifyListeners();
},
child: const Text('Go to Login'),
),
],
),
),
);
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: const Center(child: Text('Login Page')),
);
}
}
class ProfilePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile')),
body: const Center(child: Text('Profile Page')),
);
}
}
Here’s how everything comes together:
- Create instances of
MyRouterDelegateandMyRouteInformationParser. - Use
MaterialApp.router, passing the router delegate and route information parser. RootBackButtonDispatcheris used to handle back button presses.
Method 2: Using Named Routes with a Custom Navigation Function
A more straightforward approach involves using Flutter’s named routes with a custom navigation function to control navigation flow based on specific conditions.
Step 1: Define Named Routes
Define your application’s routes in the MaterialApp constructor.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Custom Routing Demo',
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/login': (context) => LoginPage(),
'/profile': (context) => ProfilePage(),
},
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Center(
child: ElevatedButton(
onPressed: () {
// Custom navigation logic
_navigateToProfile(context, isLoggedIn: true);
},
child: const Text('Go to Profile'),
),
),
);
}
}
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: const Center(child: Text('Login Page')),
);
}
}
class ProfilePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile')),
body: const Center(child: Text('Profile Page')),
);
}
}
Step 2: Implement Custom Navigation Function
Implement a function that decides where to navigate based on specific conditions.
void _navigateToProfile(BuildContext context, {required bool isLoggedIn}) {
if (isLoggedIn) {
Navigator.pushNamed(context, '/profile');
} else {
Navigator.pushNamed(context, '/login');
}
}
Now, the button in HomePage uses this custom function to navigate based on the isLoggedIn parameter.
Method 3: Using a Custom Navigator Observer
A NavigatorObserver allows you to listen for navigation events and execute custom logic whenever a route is pushed, popped, or replaced.
Step 1: Create a Custom NavigatorObserver
import 'package:flutter/material.dart';
class MyNavigatorObserver extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
print('Pushed route: ${route.settings.name}');
// Add custom logic here
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
print('Popped route: ${route.settings.name}');
// Add custom logic here
}
}
This NavigatorObserver prints route names when routes are pushed or popped.
Step 2: Integrate with MaterialApp
Add the custom NavigatorObserver to your MaterialApp.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Custom Routing Demo',
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/login': (context) => LoginPage(),
'/profile': (context) => ProfilePage(),
},
navigatorObservers: [MyNavigatorObserver()],
);
}
}
Now, your custom observer will listen to all navigation events in the app.
Conclusion
Implementing custom routing logic in Flutter provides you with greater control and flexibility in managing navigation flows. Whether you choose to use Navigator 2.0, custom navigation functions with named routes, or custom NavigatorObserver, each approach offers a unique way to tailor navigation to your application’s specific needs. Consider your project’s complexity and required flexibility when selecting the most suitable method. Properly implementing custom routing logic can greatly enhance the user experience and provide more advanced navigation features.