Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Adds new route methods pushReplacement, pushReplacementNamed, and pushReplacementOverlay #2249

Merged
merged 17 commits into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions doc/flame/router.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ The **RouterComponent**'s job is to manage navigation across multiple screens wi
similar in spirit to Flutter's [Navigator][Flutter Navigator] class, except that it works with Flame
components instead of Flutter widgets.

A typical game will usually consists of multiple pages: the splash screen, the starting menu page,
A typical game will usually consist of multiple pages: the splash screen, the starting menu page,
the settings page, credits, the main game page, several pop-ups, etc. The router will organize
all these destinations and allow you to transition between them.

Internally, the `RouterComponent` contains a stack of routes. When you request it to show a route,
it will be placed on top of all other pages in the stack. Later you can `popPage()` to remove the
it will be placed on top of all other pages in the stack. Later you can `pop()` to remove the
topmost page from the stack. The pages of the router are addressed by their unique names.

Each page in the router can be either transparent or opaque. If a page is opaque, then the pages
below it in the stack are not rendered, and do not receive pointer events (such as taps or drags).
below it in the stack are not rendered and do not receive pointer events (such as taps or drags).
On the contrary, if a page is transparent, then the page below it will be rendered and receive
events normally. Such transparent pages are useful for implementing modal dialogs, inventory or
dialogue UIs, etc.
Expand Down Expand Up @@ -77,6 +77,9 @@ and the `builder` function is only called the first time a route is activated. S
`maintainState` to `false` drops the page component after the route is popped from the route stack
and the `builder` function is called each time the route is activated.

The current route can be replaced using `pushReplacementNamed` or `pushReplacement`. Each method
simply executes `pop` on the current route and then `pushNamed` or `pushRoute`.


## OverlayRoute

Expand Down Expand Up @@ -105,9 +108,12 @@ final router = RouterComponent(
Overlays that were defined within the `GameWidget` don't even need to be declared within the routes
map beforehand: the `RouterComponent.pushOverlay()` method can do it for you. Once an overlay route
was registered, it can be activated either via the regular `.pushNamed()` method, or via the
`.pushOverlay()` -- the two method will do exactly the same, though you can use the second one to
`.pushOverlay()` -- the two methods will do exactly the same, though you can use the second one to
make it more clear in your code that an overlay is being added instead of a regular route.

The current overlay can be replaced using `pushReplacementOverlay`. This method executes
`pushReplacementNamed` or `pushReplacement` based on the status of the overlay being pushed.


## ValueRoute

Expand Down
3 changes: 3 additions & 0 deletions packages/flame/lib/src/components/overlay_route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ class OverlayRoute extends Route {
@internal
Game get game => findGame()!;

@override
String get name => super.name!;

@override
Component build() {
if (_builder != null) {
Expand Down
6 changes: 3 additions & 3 deletions packages/flame/lib/src/components/route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ class Route extends PositionComponent with ParentIsA<RouterComponent> {
final bool maintainState;

/// The name of the route (set by the [RouterComponent]).
String get name => _name;
late String _name;
String? get name => _name;
String? _name;
@internal
set name(String value) => _name = value;
set name(String? value) => _name = value;

/// The function that will be invoked in order to build the page component
/// when this route first becomes active. This function may also be `null`,
Expand Down
79 changes: 76 additions & 3 deletions packages/flame/lib/src/components/router_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,44 @@ class RouterComponent extends Component {
_adjustRoutesVisibility();
}

/// Pops the current route and places the route with [name] on top of the
/// navigation stack.
///
/// If the route is already in the stack, it will simply be moved to the top.
/// Otherwise the route will be mounted and added at the top. The route's
/// page will also start building if it hasn't been built before. If the
/// route is already on top of the stack, this method will do nothing.
///
/// This method calls the [Route.didPush] callback for the newly activated
/// route and also calls the [Route.didPop] callback for the popped route.
void pushReplacementNamed(String name) {
munsterlander marked this conversation as resolved.
Show resolved Hide resolved
final route = _resolveRoute(name);
if (route == currentRoute) {
return;
} else {
_popWithoutMinimumAssert();
}
if (_routeStack.contains(route)) {
_routeStack.remove(route);
} else {
add(route);
}
_routeStack.add(route);
_adjustRoutesOrder();
route.didPush(previousRoute);
_adjustRoutesVisibility();
}

/// Puts a new [route] on top of the navigation stack.
///
/// The route may also be given a [name], in which case it will be cached in
/// the [routes] map under this name (if there was already a route with the
/// same name, it will be overwritten).
///
/// The method calls [Route.didPush] for this new route after it is added.
void pushRoute(Route route, {String name = ''}) {
route.name = name;
if (name.isNotEmpty) {
void pushRoute(Route route, {String? name}) {
if (name != null) {
route.name = name;
_routes[name] = route;
}
add(route);
Expand All @@ -123,6 +151,28 @@ class RouterComponent extends Component {
_adjustRoutesVisibility();
}

/// Pops the current route and puts a new [route] on top of the navigation
/// stack.
///
/// The route may also be given a [name], in which case it will be cached in
/// the [routes] map under this name (if there was already a route with the
/// same name, it will be overwritten).
///
/// The method calls [Route.didPush] for this new route after it is added and
/// also calls the [Route.didPop] callback for the popped route.
void pushReplacement(Route route, {String? name}) {
munsterlander marked this conversation as resolved.
Show resolved Hide resolved
if (name != null) {
route.name = name;
_routes[name] = route;
}
_popWithoutMinimumAssert();
add(route);
_routeStack.add(route);
_adjustRoutesOrder();
route.didPush(previousRoute);
_adjustRoutesVisibility();
}

/// Puts the overlay route [name] on top of the navigation stack.
///
/// If [name] was already registered as a name of an overlay route, then this
Expand All @@ -138,6 +188,22 @@ class RouterComponent extends Component {
}
}

/// Pops the current route and puts the overlay route [name] on top of the
/// navigation stack.
///
/// If [name] was already registered as a name of an overlay route, then this
/// method is equivalent to [pushNamed]. If not, then a new [OverlayRoute]
/// will be created based on the overlay with the same name within the root
/// game.
void pushReplacementOverlay(String name) {
if (_routes.containsKey(name)) {
assert(_routes[name] is OverlayRoute, '"$name" is not an overlay route');
pushReplacementNamed(name);
} else {
pushReplacement(OverlayRoute.existing(), name: name);
}
}

/// Puts [route] on top of the stack and waits until that route is popped.
///
/// More precisely, this method returns a future that can be awaited until
Expand Down Expand Up @@ -188,6 +254,13 @@ class RouterComponent extends Component {
pop();
}

/// Local method to bypass [pop]'s assert
void _popWithoutMinimumAssert() {
final route = _routeStack.removeLast();
route.didPop(_routeStack.last);
route.removeFromParent();
}

/// Attempts to resolve the route with the given [name] by searching in the
/// [_routes] map, or invoking one of the [_routeFactories], or, lastly,
/// falling back to the [onUnknownRoute] function. If none of these methods
Expand Down
31 changes: 29 additions & 2 deletions packages/flame/test/components/router_component_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ void main() {
expect(router.currentRoute.children.length, 1);
expect(router.currentRoute.children.first, isA<_ComponentD>());
expect(router.stack.length, 2);

router.pushReplacementNamed('B');
await game.ready();
expect(router.routes.length, 4);
expect(router.currentRoute.name, 'B');
expect(router.currentRoute.children.length, 1);
expect(router.currentRoute.children.first, isA<_ComponentB>());

router.pushReplacement(Route(_ComponentE.new), name: 'E');
await game.ready();
expect(router.routes.length, 5);
expect(router.currentRoute.name, 'E');
expect(router.currentRoute.children.length, 1);
expect(router.currentRoute.children.first, isA<_ComponentE>());
expect(router.stack.length, 2);
});

testWithFlameGame('Route factories', (game) async {
Expand Down Expand Up @@ -217,16 +232,26 @@ void main() {
expect(game.overlays.activeOverlays, ['first!', 'second']);
expect(find.byKey(key1), findsOneWidget);
expect(find.byKey(key2), findsOneWidget);
router.pop();
await tester.pump();
expect(game.overlays.activeOverlays, ['first!']);

router.pushRoute(
OverlayRoute((ctx, game) => Container(key: key3)),
name: 'new-route',
);
await tester.pump();
expect(game.overlays.activeOverlays, ['first!', 'second', 'new-route']);
expect(game.overlays.activeOverlays, ['first!', 'new-route']);
expect(find.byKey(key1), findsOneWidget);
expect(find.byKey(key2), findsOneWidget);
expect(find.byKey(key2), findsNothing);
expect(find.byKey(key3), findsOneWidget);

router.pushReplacementOverlay('second');
await tester.pump();
expect(game.overlays.activeOverlays, ['first!', 'second']);
expect(find.byKey(key1), findsOneWidget);
expect(find.byKey(key2), findsOneWidget);
expect(find.byKey(key3), findsNothing);
},
);
});
Expand All @@ -239,3 +264,5 @@ class _ComponentB extends Component {}
class _ComponentC extends Component {}

class _ComponentD extends Component {}

class _ComponentE extends Component {}