Managing Routes in Flutter like a PRO with animations.

Sushan Shakya
7 min readJun 21, 2023

--

Article is written by demonstrating refactoring approach.

The code for the article is available here :

If you search for managing routes in Flutter, all you get is a list of things you can do which usually always includes the following 3 :

#1 Unnamed Routes

...

Navigator.of(context).push(
MaterialPageRoute(
builder: (c) => const HomeView(),
),
);

...

#2 Named Routes

...

return MaterialApp(
title: "Flutter Demo",
routes: {
"home": (c) => const HomeView(),
"hotel": (c) => const HotelView(),
"hotelDetail": (c) => const HotelDetailView(),
},
home: const HomeView(),
);

...

Usage:

Navigator.of(context).pushNamed("home");

#3 onGenerateRoute

...

return MaterialApp(
title: "Flutter Demo",
onGenerateRoute: Routes.onGenerateRoutes,
initialRoute: Routes.home,
);

...
...

class Routes {
static const String home = "home";

static Route onGenerateRoutes(RouteSettings settings) {
Widget child;

switch(settings.name) {
case Routes.home:
child = const HomeView();
break;
default:
child = Container();
break;
}

return MaterialPageRoute(builder: (c) => child);
}
}
...

Usage:

...

Navigator.of(context).pushNamed(Routes.home);

...

# The Problem

Let’s say you decided to use the advanced route management technique #3 onGenerateRoute .

Since your app can have a lots of routes, you’ll end up with a Routes class which has a mountain load of cases for the switch statements. Which might look something like this.

This makes it messy.
So, how do we make it clean and a bit more localized to it’s scope ?

# The Solution

We can look at another discipline and see how they manage routes.
For example : Django manages routes by localizing all urls for each module to urls.py .

We can learn from this idea.

Here’s how we separate our routes :

First, we’ll store our routes in a Map object like we did in #2 Named Routes .

static Route onGenerateRoutes(RouteSettings settings) {
var routes = {
Routes.home: (context, settings) => const HomeView(),
};
try {
final child = routes[settings.name];

Widget builder(BuildContext c) => child!(c, settings);

return MaterialPageRoute(
builder: builder,
);
} catch (e) {
throw const FormatException("--- Route doesn't exist");
}
}

Now,
We can refactor this such that we get the routes from another function as follows :

class Routes {
static const String home = "home";

static Map<String, RouteType> _resolveRoutes() {
return {
Routes.home: (context, settings) => const HomeView(),
};
}

static Route onGenerateRoutes(RouteSettings settings) {
var routes = _resolveRoutes();
try {
final child = routes[settings.name];

Widget builder(BuildContext c) => child!(c, settings);

return MaterialPageRoute(
builder: builder,
);
} catch (e) {
throw const FormatException("--- Route doesn't exist");
}
}
}

The _resolveRoutes() function can be refactored such that we have routes separated for each module.

What does that mean ?
Well, let’s say you’re making a hotel booking app, then you might have following pages :

  1. Login View
  2. Signup View
  3. Hotel Listing View
  4. Hotel Detail View

Here, Login View and Signup View are authorization routes so, we should group them into one route, Hotel Listing View and Hotel Detail View is associated with the Hotel so, it’s a good idea to group them.

This can be done as follows :

static _resolveRoutes() {
var authRoutes = {
Routes.login: (c, s) => const LoginView(),
Routes.signup: (c, s) => const SignupView(),
};

var hotelRoutes = {
Routes.hotel: (c, s) => const HotelView(),
Routes.hotelDetail: (c, s) => const HotelDetailView(),
};

return {
Routes.home: (context, settings) => const HomeView(),
...authRoutes,
...hotelRoutes,
}

Then, we can to the similar type of refactoring as before to get these routes from different files as :

import '../../modules/hotel/gui/views/login_view.dart';
import '../../modules/hotel/gui/views/signup_view.dart';
import 'app_routes.dart';

var authRoutes = {
Routes.login: (context, settings) => const LoginView(),
Routes.signup: (context, settings) => const SignupView(),
};
import '../../modules/hotel/gui/views/hotel_detail_view.dart';
import '../../modules/hotel/gui/views/hotel_view.dart';
import 'app_routes.dart';

var hotelRoutes = {
Routes.hotel: (context, settings) => const HotelView(),
Routes.hotelDetail: (context, settings) => const HotelDetailView(),
};
import 'package:flutter/material.dart';
import 'package:flutter_generalization/src/modules/hotel/gui/views/home_view.dart';
import 'auth_routes.dart';
import 'hotel_routes.dart';

class Routes {
static const String home = 'home';
static const String hotel = 'hotel';
static const String hotelDetail = 'hotelDetail';
static const String login = 'login';
static const String signup = 'signup';

static Map<String, RouteType> _resolveRoutes() {
return {
Routes.home: (context, settings) => const HomeView(),
...authRoutes,
...hotelRoutes,
};
}

static Route onGenerateRoutes(RouteSettings settings) {
var routes = _resolveRoutes();
try {
final child = routes[settings.name];

Widget builder(BuildContext c) => child!(c, settings);

return MaterialPageRoute(
builder: builder,
);
} catch (e) {
throw const FormatException("--- Route doesn't exist");
}
}
}

Then, we can refactor the Routes class by moving all the Route strings to a different file as follows :

class AppRoutes {
static const String home = 'home';
static const String hotel = 'hotel';
static const String hotelDetail = 'hotelDetail';
static const String login = 'login';
static const String signup = 'signup';
}
class Routes {
static Map<String, RouteType> _resolveRoutes() {
return {
AppRoutes.home: (context, settings) => const HomeView(),
...authRoutes,
...hotelRoutes,
...packageRoutes,
};
}

static Route onGenerateRoutes(RouteSettings settings) {
var routes = _resolveRoutes();
try {
final child = routes[settings.name];

Widget builder(BuildContext c) => child!(c, settings);

return MaterialPageRoute(
builder: builder,
);
} catch (e) {
throw const FormatException("--- Route doesn't exist");
}
}
}

Then,
We also update the files where the routes were defined to use AppRoutes .

import '../../modules/hotel/gui/views/login_view.dart';
import '../../modules/hotel/gui/views/signup_view.dart';
import 'app_routes.dart';

var authRoutes = {
Routes.login: (context, settings) => const LoginView(),
Routes.signup: (context, settings) => const SignupView(),
};
import '../../modules/hotel/gui/views/hotel_detail_view.dart';
import '../../modules/hotel/gui/views/hotel_view.dart';
import 'app_routes.dart';

var hotelRoutes = {
Routes.hotel: (context, settings) => const HotelView(),
Routes.hotelDetail: (context, settings) => const HotelDetailView(),
};

This, is good enough to be used in projects.
But we can take it one step furthur by adding types to the Map object.

# Adding Types

First, we create a type definition as follows :

import 'package:flutter/material.dart';

typedef RouteType = Widget Function(BuildContext, RouteSettings);

Then, we add the return type for _resolveRoute() function as :

  ...

static Map<String, RouteType> _resolveRoutes() {
return {
AppRoutes.home: (context, settings) => const HomeView(),
...authRoutes,
...hotelRoutes,
};
}

...

We should also update the type for authRoutes and hotelRoutes as follows :

Map<String, RouteType> authRoutes = {
AppRoutes.login: (context, settings) => const LoginView(),
AppRoutes.signup: (context, settings) => const SignupView(),
};
Map<String, RouteType> hotelRoutes = {
AppRoutes.hotel: (context, settings) => const HotelView(),
AppRoutes.hotelDetail: (context, settings) => const HotelDetailView(),
};

Now,
With this our routes will span into multiple files which is much more manageable.

Now,
Let’s say we wanted to implement a new feature of packages which has routes for Package Listing View and Package Detail View . We can create a new file called package_routes.dart as :

import 'package:flutter_generalization/src/core/routing/route_type.dart';
import 'package:flutter_generalization/src/modules/hotel/gui/views/package_detail_view.dart';
import 'package:flutter_generalization/src/modules/hotel/gui/views/package_listing_view.dart';

import 'app_routes.dart';

Map<String, RouteType> packageRoutes = {
AppRoutes.packageListing: (context, settings) => const PackageListingView(),
AppRoutes.packageDetail: (context, settings) => const PackageDetailView(),
};

Then, we can add this route to the _routeResolver() as :

...

static Map<String, RouteType> _resolveRoutes() {
return {
AppRoutes.home: (context, settings) => const HomeView(),
...authRoutes,
...hotelRoutes,
...packageRoutes,
};
}

...

So,
The next time you want to create a cohesive list of routes, you can create a new folder and create a Map<String, RouteType> and then import it into Routes class and spread the Map inside the return of _resolveRoutes() .

# Animating the Routes

Let’s say that, you wanted your app route animations to be such that all the pages slide from bottom to top.

Achieving this is fairly easy because we have a single entry point that defines the PageRoute . This is done in Routes.onGenerateRoutes() where we’ve returned MaterialPageRoute .

The slide top to bottom animation can be achieved by extending PageRouteBuilder :

class SlideRoute extends PageRouteBuilder {
final Widget Function(BuildContext) builder;
final Offset? startOffset;
final Offset? endOffset;
SlideRoute({
required this.builder,
bool fullscreenDialog = false,
this.startOffset,
this.endOffset,
}) : super(
fullscreenDialog: fullscreenDialog,
transitionDuration: const Duration(milliseconds: 500),
pageBuilder: (a, b, c) => builder(a),
);

@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return SlideTransition(
position: Tween<Offset>(
begin: startOffset ?? const Offset(0, 1),
end: endOffset ?? Offset.zero,
).animate(animation),
child: child,
);
}
}

Then,
We can replace MaterialPageRoute as :

class Routes {
static Map<String, RouteType> _resolveRoutes() {
return {
AppRoutes.home: (context, settings) => const HomeView(),
...authRoutes,
...hotelRoutes,
...packageRoutes,
};
}

static Route onGenerateRoutes(RouteSettings settings) {
print(settings.name);
var routes = _resolveRoutes();
try {
final child = routes[settings.name];

return SlideRoute(builder: (c) => child!(c, settings));
} catch (e) {
throw const FormatException("--- Route doesn't exist");
}
}
}

Now,
All the routes in the app will be animated from bottom to top.

# Animating Specific Routes Differently

It is possible that you might only want to animate a particular route such that it slides from bottom to top.

This can be achieved by adding an if statement in onGenerateRoutes as :

...
static Route onGenerateRoutes(RouteSettings settings) {
var routes = _resolveRoutes();
try {
final child = routes[settings.name];

Widget builder(BuildContext c) => child!(c, settings);

if (settings.name == AppRoutes.hotel) {
return SlideRoute(builder: builder);
}

return MaterialPageRoute(builder: builder);
} catch (e) {
throw const FormatException("--- Route doesn't exist");
}
}
...

# Bonus

The Slide Animation can be configured to slide left to right or right to left and even top to bottom .

To achieve this we just have to pass in the offset.

Animating Left to Right

...
return SlideRoute(
builder: builder,
offset: const Offset(-1, 0),
);
...

Animating Right to Left

...
return SlideRoute(
builder: builder,
offset: const Offset(1, 0),
);
...

Animating Top to Bottom

...
return SlideRoute(
builder: builder,
offset: const Offset(0, -1),
);
...

Animating Bottom to Top

...
return SlideRoute(
builder: builder,
offset: const Offset(0, 1),
);
...

Full code here :

--

--