Hey @David,
I've been searching through the internet to try find an elegant solution to this problem. Firstly, I'm implementing a navigation solution that is using named routes. I've seen plenty of implementations that use Navigator.push(context, SlideAnimation(...))
with a stack, and parent + child parameters. Go here for that solution.
I'm sure this is not the most elegant solution, but I'm putting it here such that others can build upon it. I know it's long, and probably not the copy paste solution you're looking for (random internet person reading my answer). I'm hoping someone else out there can make this better.
For a little more context, I wanted to implement two different types of transitions, depending on the relationship between the current page and the page being transitioned to. Page transition guide here.
From the aforementioned site, I wanted 'Top-Level' and 'Peer' transitions. To create those relationships, I started by creating an enum of pages, and a map from those pages to paths.
app_router.dart
enum AppPageEnum {
settings,
home,
homeNested,
// ... etc
}
extension AppPageExtension on AppPageEnum {
String get toPath {
switch (this) {
case AppPageEnum.settings:
return '/settings';
case AppPageEnum.home:
return '/home';
case AppPageEnum.homeNested:
return '/home/nested';
// ... etc
}
}
}
I also generated some utility methods to help identify if this was a base route (has only one /
, or a nested route). Also, one to get the widget that some AppPageEnum points too.
app_router.dart
var appRouter = AppPageEnum.values.asNameMap().map((key, value) {
String k = value.toPath;
Widget v;
switch (value) {
case AppPageEnum.settings:
v = const SettingsPage();
break;
case AppPageEnum.home:
v = const HomePage();
break;
case AppPageEnum.homeNested:
v = const HomeNestedPage();
break;
default:
v = UnknownPageRoute(name: value.name.title());
}
return MapEntry(k, v);
});
bool isBaseString(String path) {
return path.split("/").length <= 2;
}
// OR
bool isBasePage(AppPageEnum page) {
return isBaseString(page.toPath);
}
With the above in mind, here is the transitions I came up with.
main.dart
return MaterialApp(
// ...
onGenerateRoute: (settings) {
if (settings.name == null || appRouter[settings.name!] == null) {
return null;
}
// Not a base page, so both push and pop transitions should be slides
if (!isBaseString(settings.name!)){
return siblingTransition(settings);
}
// A base page, so push and pop transitions should be relative
return parentTransition(settings);
},
// ...
);
app_router.dart
PageRouteBuilder siblingTransition(RouteSettings settings) {
Widget child = appRouter[settings.name!]!;
Duration duration = const Duration(milliseconds: 500);
return PageRouteBuilder(
transitionDuration: duration,
reverseTransitionDuration: duration,
pageBuilder: (context, animation, secondaryAnimation) => child,
settings: settings,
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
// animation determines how the pages comes in and out of the screen.
// secondaryAnimation determines how the page exits the screen when another is pushed on top of it.
// You can remove the nested SlideTransition if you won't have double nested pages (e.g. home/nested/nested).
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
).animate(animation),
child: SlideTransition(
position: Tween<Offset>(
begin: Offset.zero,
end: const Offset(-1.0, 0.0),
).animate(secondaryAnimation),
child: child,
),
);
},
);
}
PageRouteBuilder parentTransition(RouteSettings settings) {
Widget child = appRouter[settings.name!]!;
Duration duration = const Duration(milliseconds: 300);
return PageRouteBuilder(
transitionDuration: duration,
reverseTransitionDuration: duration,
pageBuilder: (context, animation, secondaryAnimation) => child,
settings: settings,
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
// When the animation value is not 1.0, that means we are currently using the primary animation
// which indicates the parent is not already on the stack.
// I'm following the material design instructions here, so I'm simply returning the child.
// This could be some other type of transition between top-level pages.
if (animation.value != 1.0) {
return child;
}
// Here, we know that the secondary animation is being used (to push something on top of this page).
// Thus, we know it's a peer (child) widget to this parent.
// As such, we implement the slide transition as per the material design instructions.
return SlideTransition(
position: Tween<Offset>(
begin: Offset.zero,
end: const Offset(-1.0, 0.0),
).animate(secondaryAnimation),
child: child,
);
},
);
}
Lastly, our navigation between pages looks like this
// Navigate to a top level page.
Navigator.of(context).popUntil((route) => route.settings.name == '/');
Navigator.of(context).pushNamed(AppPageEnum.toPath);
// Navigate to a peer
Navigator.of(context).pushNamed(AppPageEnum.toPath);
I know this is long, and full of potentially unnecessary context. I just wanted to provide a full guide for what I did, using named routes. If your implementation differs from this, I hope there is enough information here such that you can mold it to your need.