NativeScript empowers JavaScript with native platform APIs which means it also works with Angular in delightful ways (past v2 - v13, current v14 and future releases!).
For example, you can use your Angular skills to build rich Android and iOS experiences leveraging the full scope and versatility of your TypeScript driven codebase while maintaining full power of the target platform.
To better illustrate what is meant by this, let's cover a few comparisons between standard web targeted Angular apps vs. NativeScript enabled Angular apps targeting iOS and Android platforms.
- Bootstrap
- Understanding the Lifecycle
- Using Standalone Components
- Using Directives
- Using Pipes
- Performance Metrics
- StackBlitz learn by example
- Summary
Web apps usually don't have to worry much about the browser lifecycle as the flow is pretty straight-forward:
- load page
- user interacts with page
- leave page
The user can even have multiple copies of the same application open across different browser tabs or windows. If the application is open, some UI is showing.
Mobile apps are a bit different.
For example the app can be launched in the background. In other words, we can exit the app UI while the app itself is still running in the background whereby other lifecycle details may need to be taken into consideration by the developer.
To make this a bit more clear, let's look at the differences in app bootstrap:
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { AppModule } from "./app/app.module";
// locate the app-root and bootstrap the component in it's place
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch((err) => console.error(err));
import {
platformNativeScript,
runNativeScriptAngularApp,
} from "@nativescript/angular";
import { AppModule } from "./app/app.module";
// runs the NativeScript application, nothing is bootstrapped yet, as we're only setting up the platform and callbacks
runNativeScriptAngularApp({
// this function will be called when we need to display the application UI
// a major difference from the web is that this module may be destroyed when the user leaves the application and recreated when the user opens it again
// so remember to implement all the needed ngOnDestroy to cleanup any events that were bound here!
appModuleBootstrap: () => platformNativeScript().bootstrapModule(AppModule),
});
On NativeScript, your Angular app is a part of a broader picture (an entirely new platform enriched picture).
It's good to understand providedIn: 'root'
services are singletons for that singlular Angular app instance, so if you need a singleton for the broader "platform picture" you can use providedIn: 'platform'
.
Let us elaborate:
-
On Android, when the user leaves the app with the back button the Activity is destroyed, so your ApplicationRef will be destroyed. This behavior has changed in Android 12 (https://developer.android.com/about/versions/12/behavior-changes-all#back-press), so from 12 and up the angular instance should not be destroyed anymore.
-
On both platforms, if the app is woken up by some kind of event, like a background fetch, your
main.ts
code will run, but the angular application will not be bootstrapped.
Having direct access to the native platform is a powerful tool, but it's important to understand how the platform itself works.
- The platform launches your application.
- This can be either in the background or the foreground. This is where android's Application is created and iOS' main function is called.
- The runtime (V8) is initialized at the earliest possible time.
- Your application entrypoint is called (main.ts)
- Everything from this point onwards happens in JS through the NativeScript Runtime
- Application is initialized in
runNativeScriptAngularApp
- This calls
UIApplicationMain
on iOS and sets up the Application and Activity callbacks for Android.
- This calls
- (optional)
appModuleBootstrap
is called- If the app has been launched in the background, maybe because of a background fetch event, or a firebase Data push notification, the angular application may not be bootstrapped at all unless the user opens the app during this time.
What this means in practice is that you have the best of both worlds. You can use the full platform capabilities to run the minimum amount of code necessary for the task at hand while having the full power of Angular for building your UI.
We're often accustomed to just calling platformBrowserDynamic().bootstrapModule(AppModule)
to get the job done without thinking much about it, but you could, for example, create a very minimal non-UI module that just contains the few services required for a background fetch. For example:
@NgModule({
imports: [NativeScriptModule],
})
export class BackgroundModule {
ngDoBootstrap() {
// do nothing, this is not an UI module
}
}
const moduleRefPromise = platformNativeScript().bootstrapModule(AppModule);
@JavaProxy("com.example.SomeEventReceiver")
@NativeClass()
class SomeEventReceiver extends android.content.BroadcastReceiver {
onReceive(
context: android.content.Context,
intent: android.content.Intent
): void {
console.log("INTENT RECEIVED");
this.doBackgroundFetch();
}
async doBackgroundFetch() {
const moduleRef = await moduleRefPromise;
moduleRef.injector.get(BackgroundService).fetchLatestDataAndSaveToDisk();
}
}
You can also use rxjs or providedIn: 'platform'
services to exchange events between the background module and the main angular app in case it's running.
This is similar to what a Service Worker would look like on the web, but we're sharing the same JS context!
Using standalone components work just like the web as shown in the Angular doc here.
import { Component } from "@angular/core";
import { NativeScriptCommonModule } from "@nativescript/angular";
@Component({
standalone: true,
selector: "hello-standalone",
imports: [NativeScriptCommonModule],
template: `<Label text="Hello, I'm a standalone component"></Label>`,
schemas: [NO_ERRORS_SCHEMA],
})
export class HelloStandaloneComponent {}
Here we can use any NativeScript View components we desire with our standalone components whereby importing the NativeScriptCommonModule
and they will work completely standalone.
Take note that custom schemas are not supported by the angular compiler, so we also use NO_ERRORS_SCHEMA.
To understand how natural Angular development is with NativeScript let's look at a few additional examples from the Angular docs: https://angular.io/guide/attribute-directives
import { Directive, ElementRef, inject } from "@angular/core";
@Directive({
selector: "[appHighlight]",
})
export class HighlightDirective {
private el = inject(ElementRef);
constructor() {
this.el.nativeElement.style.backgroundColor = "yellow";
}
}
Directives work exactly as you would expect within the scope of iOS and Android development with NativeScript.
That is precisely the highlight.directive.ts
, bearing in mind the visuals on screen are pure platform native views such as UILabel on iOS and TextView on Android represented simply via a Label view component provided by @nativescript/core:
<Label appHighlight text="Attribute Directive"></Label>
The directive is further enhanced in the Angular docs with mouseenter
and mouseleave
events via the HostListener decorator.
This works the same in NativeScript regarding the HostListener
decorator however only for events that are applicable for mobile platforms such as iOS and Android. For example, tap is a supported event binding (aka gestures) on mobile platforms:
import { Directive, ElementRef, HostListener, inject } from "@angular/core";
@Directive({
selector: "[appHighlight]",
})
export class HighlightDirective {
currentColor = "";
private el = inject(ElementRef);
@HostListener("tap") onTap() {
if (this.currentColor) {
this.currentColor = "";
} else {
this.currentColor = "yellow";
}
this.highlight(this.currentColor);
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
Pipes are also incredibly useful for Angular developers and work exactly as you would expect as well; taken directly from the Angular docs here for the web:
<p>The hero's birthday is {{ birthday | date }}</p>
This would transform the value to display the formatted date and you can use them exactly the same with NativeScript:
<Label text="The hero's birthday is {{ birthday | date }}"></Label>
NativeScript performance is impressive resulting in delightful outcomes. As with all performance optimizations, understanding your programming language and tech stack are key to unlocking it's full potential.
Here are some out of the box performance metrics comparing:
Angular Web Bootstrap vs. Angular for iOS/Android via NativeScript Bootstrap vs. Pure Native Bootstrap
Note: All metrics regarding Angular bootstrap are measured after the JavaScript engine is ready, whether it be in the browser or on a mobile device.
Environment details: iPhone 13 Pro, iOS 15.5, Mobile Safari browser
- mobile browser time to bootstrap Angular app:
10.417 ms
App can be found here and run on your own:
ng serve --open --host 0.0.0.0
Using the host
argument will allow you to connect using your local IP address in the mobile device web browser.
Environment details: iPhone 13 Pro, iOS 15.5
- time to bootstrap Angular app:
8.357ms
- time from native iOS application creation to when the UI is created (aka
loaded
event in NativeScript):44.695ms
- time from native iOS application creation to when the app is ready (aka
viewDidAppear
of UIViewController):63.917ms
App can be found here and run on your own:
ns run ios --no-hmr
The difference between Angular bootstrap
vs. native iOS application creation
represents the time it takes for JavaScript to boot the Angular app on the iOS device vs. the time it takes for iOS to start the UIApplicationMain after the native Application creation.
Environment details: Android Samsung Galaxy S20, Android 12
- time to bootstrap Angular app:
17.244ms
- time from native Android application creation to when the UI is created (aka
loaded
event in NativeScript):160.039ms
- time from the native Android application to when the app is ready:
239.359ms
App can be found here and run on your own:
ns run android --no-hmr
The difference between Angular bootstrap
vs. native Android application creation
represents the time it takes for JavaScript to boot the Angular app on the Android device vs. the time it takes for Android to start the Main Activity after the native Application creation.
We can measure similar metrics with a pure native application, for example iOS using Objective C alone.
- time from native iOS application creation to when the UI is created (aka
viewDidLoad
of UIViewController):30.651583 ms
- time from native iOS application creation to when the app is ready (aka
viewDidAppear
of UIViewController):54.013500 ms
App can be found here and run on your own by opening the Xcode project to run.
You can learn by example from this StackBlitz which dives deeper into all these topics and more.
This is an advanced example which illustrates fascinating points around how Angular/JavaScript is handled in a pure native mobile app such as...
- How bootstrap differs between web apps and native mobile platform handling with NativeScript to consider advanced mobile platform cases (background services, etc.)
- How subscriptions can leak if not properly handled during ngOnDestroy with
providedIn: 'root'
regarding mobile app exit - How native callbacks are not patched by
NgZone
whereby native functions may fall outside of zone so you want to make sure native callbacks are handled inside zone if UI changes are desired as a result of them - Android less than version 12 behavior regarding hardware back vs. Android version 12 or greater
Combining Angular with NativeScript makes for a powerful tech stack with a lot of versatility. You can enable your team to take advantage of broad app distribution options as well as take advantage of liberating programming angles.
When building for the web and wanting to enable rich mobile platform features, NativeScript makes a great choice. It can also be used alongside Capacitor in both directions: a. starting with Ionic and mixing it's abilities in via @nativescript/capacitor or, b. starting with NativeScript and mixing Capacitor abilities in via @nativescript/ionic-portals.
Additionally it can be combined within workspace style developments to enable wonderful architectural scalability by sharing JavaScript for increased reusability.
NativeScript Technical Steering Committe (TSC) Members: