Navigation and Route Parameters
Navigating Between Screens
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:
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:
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.
Navigation Actions
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:
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.