Navigation and Stacking
This document disects the internals of how Astro implements navigation and stacking in the NavigationPlugin. By the end of this document you'll understand how the navigation plugin works, how it manages the "back stack", and how back navigations work. This document covers both "classic" navigations and single-page app navigations.
Overview
Astro always stacks web views when navigating to a new url (address). This is
implemented within the
navigationPlugin.js.
The NavigationPlugin creates a new WebViewPlugin, calls navigate
on it with
the requested URL, and then pushes it on the navigation stack by calling
navigateToPlugin(webViewPlugin)
on itself.
Astro detects when a WebView is trying to navigate to a new URL and rejects the
navigation, instead triggering an event on the WebViewPlugin as a navigate
event. Typically, in app.js, the event is handled and driven back to the
NavigationPlugin through a navigateToUrl(url)
call resulting in a new
WebViewPlugin being pushed onto the stack (leaving the previous WebView at the
"pre navigation" state enabling "fast back" navigation).
When a PWA is hosted inside Astro, the web view never navigates in the classic sense. Instead, the DOM is modified in-place without a navigation to a new URL. This presents a challenge as the hosting WebView does not get notified automatically that any transition is occurring.
Progressive Web Apps
Astro supports hosting Progressive Web Apps (PWA) which are basically single page apps (SPA) as implemented in the Mobify platform. The PWA has special logic to detect that it is in an Astro app. By coordinating with Astro we can achieve native-like navigations, reduce memory usage, and maintain fast back navigations.
PWA Coordination
Mobify PWA's have been made "app aware" (see [PR
#214](https://github.com/mobify/progressive-web-scaffold/pull/214)). When a PWA
is about to transition to a new state ("navigate"), it triggers a custom
event to notify Astro that the web view is about to transition. This event
(pwa-navigate
) is special in that it never reaches app.js
; it is intercepted
on the native side and handled specially. Because Astro handles back navigation
by simply "unstacking" (popping the current webview off of the navigation stack
and throwing it away) the PWA doesn't raise a pwa-navigate
event when it needs
to navigate back. Instead, Astro instructs the PWA to navigate back whenever the
NavigationPlugin does a back navigation. See below for more details about how
this back navigation works.
Every time the PWA navigates forwards (pwa-navigate
) or backwards (popping) the PWA is hidden by a blank screen during the forwards navigation or a placeholder image during the backwards navigation. This foreground image needs to be removed. In order to remove it the PWA can send a pwa-rendered
event. Astro will remove this foreground image as soon as it receives the event. If the event is not received a backup has been put in place that removes the foreground image after 1 second. It is recommended to send the pwa-rendered
event from the PWA.
Astro Behaviour
When the Astro intercepts a pwa-navigate
event, it stacks a new header item
and prevents the event from propagating up to app.js. It then puts a
placeholder in place of the navigating webview and re-stacks the webview as the
new navigation item.
Imagine a loaded WebViewPlugin with a loaded PWA (*P
hosts a PWA, *C
is a
classic):
---> TOP
+------+
| |
| |
| W1 |
| |
| |
+------+
After a transition (pwa navigation) it would look like this:
---> TOP
+------+ +------+
| | | |
| | | |
| P1 | | W1 |
| | | *P |
| | | |
| | | |
+------+ +------+
Now if we hit a link that navigates away from the PWA ("classic" navigation) it would look like this:
---> TOP
+------+ +------+ +------+
| | | | | |
| | | | | |
| P1 | | W1 | | W2 |
| | | *P | | *C |
| | | | | |
+------+ +------+ +------+
If we go back to the PWA next, we'll see a new WebViePlugin stack which will re-start the placeholder stacking from that point on. After two more pwa navigations the navigation stack would look like this:
+------+ +------+ +------+ +------+ +------+ +------+
| | | | | | | | | | | |
| | | | | | | | | | | |
| P1 | | W1 | | W2 | | P2 | | P3 | | W3 |
| | | *P | | *C | | | | | | *P |
| | | | | | | | | | | |
+------+ +------+ +------+ +------+ +------+ +------+
Platform Specific Behaviour
The iOS and Android implementations are very similar, but there are some differences outlined below.
iOS
On iOS, the NavigationPlugin (through the UINavigationController) provides an "interactive pop gesture". Meaning you can start swiping from the right edge to pop the current navigation item (ie. go back).
In a PWA navigating doesn't take the browser to a new URL/page. It simply
transitions the DOM to a new state. This results in Astro never stacking any web
views after loading a PWA. With the pwa-navigate
event we can stack a new web
view, but we end up navigating to the exact same URL (possibly with a differnet
fragment) but rendering a different UI. This means we basically have a whole
stack of web views that have loaded the same PWA but are in differnet visual
states.
Astro avoids this by only using one web view and taking snapshots right before the PWA "navigates" (changes the DOM). This is done by adding a gesture recognizer to web view and taking a snapshot of the view each time the user interacts with the web view. This means we take more snapshots than necessary, but so far this doesn't seem to affect performance.
When the WebViewPlugin sees the pwa-navigate
event coming out of its web
view, it notifies the NavigationPlugin. The NavigationPlugin then asks the
WebView for it's latest snapshot and creates a placeholder view controller from
that. Finally it replaces the current web view view controller on the stack
with the placeholder and "re-stacks" the web view.
Android
Android works in a similar fashion to the iOS implementation.
Back navigation
Back navigation must also understand PWA navigations. The "back stack" of a NavigationPlugin that has visited a PWA will contain a mixture of placeholders and actual WebViewPlugins (and possibly other custom plugins). The top of the stack will always be some type of actual plugin (typically a WebViewPlugin). Behind any WebViewPlugin there could be one, or more, placeholders.
If we start going back you can see how WebViewPlugins keep slipping back up the stack until there are no more placeholders behind them at which point they are popped just as they always were.
+------+ +------+ +------+ +------+ +------+ +------+
| | | | | | | | | | | |
| | | | | | | | | | | |
| P1 | | W1 | | W2 | | P2 | | P3 | | W3 |
| | | *P | | *C | | *P | | *P | | *P |
| | | | | | | | | | | |
+------+ +------+ +------+ +------+ +------+ +------+
← Back
+------+ +------+ +------+ +------+ +------+
| | | | | | | | | |
| | | | | | | | | |
| P1 | | W1 | | W2 | | P2 | | W3 |
| | | *P | | *C | | *P | | *P |
| | | | | | | | | |
+------+ +------+ +------+ +------+ +------+
← Back
+------+ +------+ +------+ +------+
| | | | | | | |
| | | | | | | |
| P1 | | W1 | | W2 | | W3 |
| | | *P | | *C | | *P |
| | | | | | | |
+------+ +------+ +------+ +------+
← Back
+------+ +------+ +------+
| | | | | |
| | | | | |
| P1 | | W1 | | W2 |
| | | *P | | *C |
| | | | | |
+------+ +------+ +------+
← Back
+------+ +------+
| | | |
| | | |
| P1 | | W1 |
| | | *P |
| | | |
+------+ +------+
← Back
+------+
| |
| |
| W1 |
| *P |
| |
+------+
The algorithm for a back navigation is as follows:
- Is the top item a WebViewPlugin?
- YES Is the second-from-top item a Placeholder?
- YES Swap the WebViewPlugin and Placeholder, pop the Placeholder and throw it away.
- NO Pop the WebViewPlugin and throw it away.
- NO Pop the top item and throw it away