Skip to content

Navigation and Route Parameters

Most AFib runtime contexts offer a series of navigate... functions which allow you to navigate among screens. For example, you can use context.navigatePush to add a new screen below the current one in the hierarchy, and context.navigatePop to navigate up one screen in the hierarchy.

Each navigate function takes its own kind of navigate action. The navigate action contains the data necessary to perform the navigation.

The Screen.navigatePush function

Each screen starts out with a static navigatePush function which returns a AFNavigatePushAction.
That action contains a route parameter, which specifies what screen should be pushed, and an initial value for the screen's route parameter.

So, when you visit a screen, your will often use code with the form:

    context.navigatePush(TodoListScreen.navigatePush(
        // you will pass in parameters that the caller is meant to configure when 
        // visiting the screen.  These parameters will pass through into the initial
        // state of the route parameter for the screen, which is contained in the returned
        // AFNavigatePush action.
        showing: TodoListScreen.filterShowAll
    ));

The static navigatePush function is useful because you will usually visit the screen from both SPI event handlers as part of your production code, and from UI prototype definitions. It encapsulates code that would otherwise be duplicated in at least those two locations.

The Screen.navigatePush function implementation

Note that when you write your navigatePush function, you get to decide which portions of the route parameter state are configurable by the code choosing to visit the screen, and which are controlled by the screen itself. The former should be passed as parameters to the navigatePush function, while the latter will be determined by constants or logic inside the navigatePush function and hidden from the caller.

Often, you will add data to your route parameter which does not need to be exposed to the caller (for example, whether some details on the screen are initially visible), and you will be able to add that in the navigatePush function without impacting any code that visits the screen.

Casting to Other Navigate Actions

An AFNavigatePush action contains the essential data necessary to visit a new screen. Rather than introducing separate static methods for alternate navigational actions, you can simple cast the push action to an alternate action. For example:

    // this takes an AFNavigateReplaceAllAction.
    context.navigateReplaceAll(TodoListScreen.navigatePush(
        showing: TodoListScreen.filterShowAll
    // this case just converts to the correct kind of action.
    ).castToReplaceAll());

Again, using this technique centralizes all the initialization logic for your route param in a single location, so that as the route param evolves it is easier to maintain.

Always use the most 'atomic' version of a navigation

When you navigate between screens, Flutter will re-render the screen that is going out of scope.
To avoid problems with missing route parameters during the navigation animation, AFib keeps the route parameter associated with that screen in a holding area during the animation.

If you attempt to perform several navigations sequentially, like this:

  context.navigatePop();
  context.navigatePush(...);

That system won't work, and you may get problems with missing route parameters during the navigation. Consequently, you should always use the more atomic, single-call version, which in this case would be:

  context.navigateReplaceCurrent(...);

When you do so, AFib is able to ensure that the route parameter for the screen which is going out of scope remains in the holding area for the duration of the navigation.

The available navigation actions include:

navigatePush

Push a screen onto the hierarchy.

navigatePop, navigatePopN, navigatePopTo

Pop a screen off the hierarchy, or N screens, or pop until you reach a specific screen ID.

navigatePopToAndPush

Pop until you reach a particular screen, then push another screen.

navigateReplaceAll

Replace the entire current hierarchy with one new screen.

navigateReplaceCurrent

Replace the current screen with a new one, leaving screens farther up the hierarchy unchanged.

You cannot use the same screen id multiple times in the hierarchy

You cannot have the same screen id occur multiple times in the hierarchy. However, instead of hard-coding the screen id into the screen definition and route parameter definition, you can pass them in as parameters. Doing so allows you to re-use all the same screen code with two different ids.

If you use this approach, you need to manually update the screen map in your xxx_define_core.dart file to map both screen ids to functions that create a screen with that id.

Loading Data During Queries

Often, an app will need to load data before or during a navigation. The AFNavigatePushAction takes two constructor parameters, executeBefore and executeDuring, which are queries executed prior to and during the navigation respectively.

executeBefore
Can integrate data into the global state.
executeDuring
Can integrate data into the global state, or can update the route parameter for the destination screen, which will exist and be in the hierarchy by the time the query executes. However, your destination screen must be tolerant of the fact that the query may not have finished loading its data before the screen is first rendered.

Showing Dialogs and Bottom Sheets

Dialogs and bottom sheets are not part of the screen hierarchy. Instead, their route parameters are added to the global pool.

Preseving Route Param State Across Invocations

If you'd like a dialog or bottom sheet to preserve its route parameter between invocations, you can set an initial route parameter at startup in the global pool, and then use AFRouteParamRef (described below) to refer to that existing parameter when you call context.show....

Return Values

The context.showDialogAfib<YourReturnType> method takes an onReturn callback which is called when the dialog is dismissed. It recieves null if the dialog is cancelled, and a return value that you supply to context.onCloseDialog within the dialog's SPI.

Showing Drawers

Drawers also store their route parameter in the global pool. They are slightly different because they can be dragged onto the screen by the user without an explict call from your code.

Consequently, you must establish an initial route parameter value for your drawer at the time it is registered in the defineScreens method of lib/initialization/xxx_define_core.dart.

Referencing Existing Parameters with AFRouteParamRef

Sometimes, you may want a screen or widget to refer to an existing route parameter that is already in the hierarchy or the global pool. You can do so by passing an instance of AFRouteParamRef instead of an actual route parameter.

AFRouteParamRef takes a screen id, a route location (global or hierarchy), and an optional widget id which is used when referring to the route parameter for a child widget of a particular screen.

Please note that AFRouteParamRef has factory methods that are useful when configuring references for a typical screen, drawer, widget, etc (e.g. AFRouteParamRef.forScreen).

Uses of AFRouteParamRef

Refer to a screen parameter from a child widget

Sometimes, a widget will simply want to inherit or pull down its parent screen's route parameter. You can use an AFRouteParamRef.forScreen as the launch parameter for the widget with the screen id specified.

Have a screen or widget refer to a global route parameter

Sometimes, you might want a screen to maintain state even though it has gone out of scope. Or, you might like a widget on multiple screens to maintain state across all of its appearances.

In that case, you can establish a global route parameter for the screen or widget, and then reference it using AFRouteParamRef.for.... The route parameter will continue to exist across multiple invocations of the widget.

Fine control of route parameter updates

If you need very specific control of what happens when a new route parameter replaces an existing value in the state, you can override mergeOnWrite in the route parameter itself. By default, it does a simple overwrite by returning this, but you could propagage certain aspects of oldParam if you wanted to:

@override
AFRouteParam mergeOnWrite(AFRouteParam oldParam) {
    return this;
}

Working with Stateful Flutter Widgets in Route Parameters

This topic was covered in the Prototyping UI section above.

Other Issues

Timing of the dispose() call on route parameters

Each route parameter has a dispose() call, which you can override to perform cleanup when the route parameter is going out of scope.

However, because Flutter performs one or more renders of a screen as it is animating off the device screen, AFib needs to maintain your route parameter until after the animation completes. For convenience (especially where complex multi-screen navigations are involved), AFib does not dispose of your route param until the next navigation occurs. So, the invocation of dispose on a route parameter is delayed by design.

Route Parameter Time Update Integration

Route parameters have a reviseForTime method, which gets called periodically if you pass in a timeSpecificity parameter to the constructor. By default it does nothing. I have found it useful in certain obscure cases where app remains open overnight, and an active screen's route param contains some kind of time-specific data.