Navigation
The Navigation Framework library structures a multi-page application. It turns a set of screens into structured navigation between pages identified by a key, maintains the navigation history, and can animate every page change with a transition.
The library builds on MWT: a page produces an MWT Widget, and the navigator mounts it on the application’s Desktop.
It answers the concerns an application would otherwise implement by hand: which page is active, what happens on a back navigation, when a page must build or release its content, and how one page gives way to the next.
Usage
Add the Navigation Framework dependency to the Application project build file:
implementation("ej.library.ui:navigation-framework:1.0.1")
<dependency org="ej.library.ui" name="navigation-framework" rev="1.0.1"/>
Then start MicroUI, initialize the navigator with the desktop it drives and the factory that builds the pages, and navigate to the first page:
public static void main(String[] args) {
MicroUI.start();
Desktop desktop = new Desktop();
desktop.setStylesheet(buildStylesheet());
Navigator.initialize(desktop, new ExamplePageFactory());
Navigator.getInstance().navigateTo(Pages.HOME);
}
The navigator shows the desktop on the first navigation.
The application does not call Desktop.requestShow() itself.
Concepts
Navigator
The Navigator is the controller and the entry point of the library.
It is a singleton: the application never constructs one, it configures the instance once with Navigator.initialize(Desktop, PageFactory) and obtains it with Navigator.getInstance().
Every method invoked before initialization throws IllegalStateException.
Initializing again restarts the navigator from a clean state (empty history, no listener, default transition).
The navigator drives the Desktop given at initialization and mounts each page’s content on it.
The application keeps ownership of that desktop: it sets the stylesheet, the render policy and the input handling on it.
Pages and Keys
A page is a factory for its content, not the content itself: the navigator calls getContent() each time the page is about to be shown, so a fresh widget is produced on every navigation to it.
The content can be any MWT Widget, such as one of those provided by the Widget library:
public class HomePage extends Page {
@Override
protected Widget getContent() {
Button button = new Button("Settings");
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick() {
Navigator.getInstance().navigateTo(Pages.SETTINGS);
}
});
return button;
}
}
A page is identified in the public API by a non-negative int key.
The application declares its keys as constants:
public class Pages {
public static final int HOME = 0;
public static final int SETTINGS = 1;
public static final int DETAILS = 2;
}
Identity is per history entry, not per key. The same key may appear more than once in the history, and each forward navigation builds a new page.
Page Factory
The application implements one PageFactory, which maps a key to a page:
public class ExamplePageFactory implements PageFactory {
@Override
public Page create(int key, @Nullable Object argument) throws IllegalArgumentException {
switch (key) {
case Pages.HOME:
return new HomePage();
case Pages.SETTINGS:
return new SettingsPage();
default:
throw new IllegalArgumentException("Unknown key.");
}
}
}
The factory returns a fresh page on each call and never caches one.
It is called only by the operations that build a page, navigateTo() and replaceWith().
A back navigation re-shows the page instance the history holds, so it does not consult the factory.
The navigator is not passed to the factory.
A factory, or a page it builds, reaches it with Navigator.getInstance().
Navigation Operations
Four operations change the active page.
Each one has an overload taking an explicit Transition.
Operation |
Effect |
Page origin |
|---|---|---|
|
Pushes a new entry on top of the history. |
Built by the factory |
|
Pops the active entry; the entry beneath becomes active. |
History |
|
Pops back to the nearest entry below the active one with that key, removing every entry above it. |
History |
|
Replaces the active page in place, leaving the history depth unchanged. |
Built by the factory |
Passing Data to a Page
A page receives its data through its own constructor: the framework owns no parameter slot. The two operations that build a page carry an optional argument to the factory, which injects it:
// At the call site: hand the selection to the page being built.
Navigator.getInstance().navigateTo(Pages.DETAILS, selection);
// In the factory: check what was received and inject it.
case Pages.DETAILS:
if (!(argument instanceof Selection)) {
throw new IllegalArgumentException("The details page requires a selection.");
}
return new DetailsPage((Selection) argument);
The navigator keeps no reference to the argument.
It is forwarded to PageFactory.create() and nowhere else, so whatever the page needs from it, the page keeps itself.
Back navigations take no argument: they re-show the page the history holds.
Page Lifecycle
A page is notified when it becomes, or ceases to be, the active page:
onEntered(): the page has become the active page.onExited(): the page has ceased to be the active page.
Both are invoked immediately after the navigation operation, before the transition animation starts, and only on navigation operations. Use them to start and stop what must live only while the page is displayed, such as a timer or a sensor subscription.
Navigation Listeners
A NavigationListener observes page changes from outside the pages, for example to update a breadcrumb, record an analytics trace, or refresh a shared header:
Navigator.getInstance().addNavigationListener(new NavigationListener() {
@Override
public void onNavigation(@Nullable Page exitedPage, Page enteredPage) {
// exitedPage is null on the first navigation.
}
});
The callback fires once per navigation, in lockstep with the page lifecycle callbacks. The listeners notified for a navigation are those registered when its notification pass starts.
History Queries
The navigator exposes the current state of the history:
getActivePage()andgetActiveKey(): the page on top of the history, and its key.getHistory()andgetHistoryKeys(): a copy of the history, oldest first, the active page last.
Transitions
Every page change is animated by a Transition.
The library ships three:
Transition.IMMEDIATE: the incoming content replaces the outgoing one in a single frame. This is the navigator’s initial default.FadeTransition: cross-dissolves the incoming content over the outgoing one.SlideTransition: slides both contents, in theSlideTransition.RIGHT_TO_LEFTorSlideTransition.LEFT_TO_RIGHTdirection.
Both animated transitions are configurable objects, not constants.
Their duration defaults to Transition.DEFAULT_DURATION_MILLIS (300 ms) and can be set at construction.
A transition applies either to every navigation, as the default, or to a single one:
Navigator navigator = Navigator.getInstance();
// Applies to every navigation made through an overload without a transition.
navigator.setTransition(new FadeTransition());
// Applies to this navigation only.
navigator.navigateTo(Pages.DETAILS, new SlideTransition(SlideTransition.RIGHT_TO_LEFT, 500));
A navigation requested while a transition is running snaps that transition to its end before proceeding, so a fast sequence of navigations never leaves a page half-animated.
Custom Transition
An application defines its own effect by implementing Transition.
The TransitionContext the navigator supplies exposes the two contents, the content area size, and the rendering primitives: positionContents() to move the contents, blendIncoming() to fade the incoming one in, and animate() to register the Animation driving the effect.
Animation is the MWT interface described in the Animations section.
public class SlideUpTransition implements Transition {
private static final int DURATION = 400;
@Override
public void run(TransitionContext context, TransitionListener listener) {
int height = context.getContentHeight();
context.positionContents(0, 0, 0, height);
context.animate(new Animation() {
private long startTime = -1;
@Override
public boolean tick(long platformTimeMillis) {
if (this.startTime < 0) {
this.startTime = platformTimeMillis;
}
long elapsed = platformTimeMillis - this.startTime;
if (elapsed >= DURATION) {
context.positionContents(0, -height, 0, 0);
listener.onTransitionEnd();
return false;
}
int offset = (int) ((elapsed * height) / DURATION);
context.positionContents(0, -offset, 0, height - offset);
return true;
}
});
}
}
The implementation must call TransitionListener.onTransitionEnd() exactly once, when the animation is complete.
This is what lets the navigator detach the outgoing content.
Registering the animation with context.animate() is what lets the navigator snap the transition to its end when a new navigation interrupts it.
Examples
The Navigation Framework example application demonstrates the whole library: a splash screen replaced by the home page, one page per shipped transition, a custom transition, and a form passing its result to the next page as a navigation argument.
