Über unsMediaKontaktImpressum
Nic Raboy 17. Januar 2017

In 21 Schritten mit Angular 2 und NativeScript zur Native mobile App

In diesem Tutorial entsteht mit NativeScript und Angular 2 eine mobile Applikation. Sie verwendet gängige UI-Komponenten und bezieht Daten von RESTful-Web-Services. Ziel des Tutorials ist die Erstellung einer funktionsfähigen, mobilen Applikation, die gebräuchliche UI-Komponenten verwendet und mit NativeScript und Angular 2 Daten von RESTful-Web-Services bezieht.

Kurz gesagt: Es entsteht eine einfache mobile Applikation, die im vorliegenden Fall die Pokémon-Daten eines weit verbreiteten Web-Services namens PokéAPI [1] ausgibt. Zudem verwendet die Applikation animierte Komponenten und Benutzerinteraktionen, wie sie für attraktive und qualitativ hochwertige Software üblich sind.

NativeScript [2] ist ein plattformunabhängiges Developer-Framework zur Erstellung nativer, mobiler Applikationen; dabei finden gängige Web-Entwicklungstechnologien wie JavaScript, CSS und XML Verwendung. Die Betonung liegt dabei auf "nativ": Auch wenn gängige Web-Technologien zum Einsatz kommen, entstehen keine Web- oder hybride Applikationen, sondern tatsächlich native Anwendungen mit der entsprechenden Performance und den nativen User-Interfaces.

Als besonderes Merkmal von NativeScript können Entwickler das weit verbreitete Angular 2- Framework [3] zur Erstellung von Applikationen verwenden. Damit lassen sich nicht nur sehr einfach Android- und iOS-Anwendungen entwickeln, sondern es kann auch ein beträchtlicher Teil des Codes für Web-Anwendungen wiederverwendet werden.

Schritt 1: Ein neues NativeScript-Angular-Projekt anlegen

Als Erstes ist ein neues NativeScript-Projekt anzulegen, das Angular 2 unterstützt. Das kann durch die Ausführung des folgenden Befehls erfolgen:

tns create wd-project --ng

Jede gewünschte Build-Plattform muss in dem Projekt über eine Befehlszeile wie

tns platform add ios
tns platform add android

ergänzt werden.

Nur Entwickler die unter Mac OS X arbeiten, können iOS als Build-Plattform nutzen.

Schritt 2: Die Angular 2-HTTP-Komponente importieren

Das Projekt nutzt Remote-Data und benötigt daher die Angular 2-HTTP-Komponente [4] für den Zugriff auf RESTful-API-Endpunkte. Dazu ist das File app/main.ts zu öffnen und darüber erfolgt dann der Import des NativeScriptHttpModule, etwa so:

import {
    NativeScriptHttpModule
} from "nativescript-angular/http";

Nach dem Import lässt sich das Module in die imports section des @NgModule block injizieren:

imports: [
    NativeScriptModule,
    NativeScriptHttpModule
]

Schritt 3: Die Pokémon Sprites für die Bildschirmausgabe abrufen

Das zur Ermittlung der Pokémon-Daten verwendete PokéAPI (Pokémon RESTful API) ermöglicht keine direkte Verlinkung mit den Sprite Media Files. Abhilfe schafft das frei verfügbare Sprite Package von GitHub [5], das nach dem Download zu entpacken ist.

Das Directory sprites/pokemon mit dem extrahierten Archiv sollte unter dem Namen images in das Directory app abgelegt werden.

Im vorliegenden Fall kommen nur die ersten 151 Pokémon Images zum Einsatz; alle anderen Pokémon Images können gelöscht werden.

Schritt 4: Die TypeScript Component Foundation hinzufügen

Der größte Teil der TypeScript-Logik befindet im Projekt-File app/app.component.ts. Die Klasse muss restrukturiert werden, damit sie so aussieht:

export class AppComponent
    implements OnInit {
    constructor() { }
    ngOnInit() { }
    showInformation(index: number) { }
    showDialog(data: Array<any>) { }
}

Zu beachten ist die OnInit-Implementierung. Das Interface benötigt eine ngOnInit-Methode, die nach der constructor-Methode ausgeführt wird. Es wird so importiert:

import {
    Component,
    OnInit?
} from "@angular/core";

Schritt 5: Eine Liste der verfügbaren Pokémons einholen

Um die Liste der verfügbaren Pokémons einzuholen, muss die Angular 2-HTTP-Komponente injiziert werden. Der Import erfolgt wie folgt:

import {
    Http
} from "@angular/http";
import "rxjs/Rx";

Anschließend kann das Ergebnis in die Methode constructor injiziert werden:

constructor(private http: Http) { }

Zur Speicherung der API-Daten wird ein Public Array benötigt, das beispielsweise so aussieht und mit der Methode constructor initialisiert wird:

public pokemon: Array<any>;

Abschließend sind die Daten in die Methode ngOnInit zu laden:

ngOnInit() {
    this.http.get("https://pokeapi.co/api/v2/pokemon?limit=151")
        .map(result => result.json())
        .flatMap(result => result.results)
        .subscribe(result => {
            this.pokemon.push (result);
        }, error => {
            console.error(error);
        });
}

Das Ergebnis des asynchronen HTTP-Requests wird in dem zuvor angelegten Public Array gespeichert. Die Angular 2-HTTP-Komponente [4] nutzt in großem Umfang RxJS-Technologie [6], wie sie auch der Streaming-Dienst Netflix einsetzt. RxJS ermöglicht die reaktive Programmierung mit JavaScript und Angular 2.

Schritt 6: Die UI-Grundlage aufbauen

Da die Applikation im Grunde genommen aus einer Liste besteht, ist es wichtig, deren Entstehung nachvollziehen zu können. Man nehme zum Beispiel das folgende XML-Markup:

<ActionBar title="WD NativeScript App"></ActionBar>
<StackLayout>

</StackLayout>

Das Ergebnis ist eine Action-Bar – manchmal auch als Navigation-Bar bezeichnet – und ein vertikales, sogenanntes Stack-Layout [7].

Schritt 7: Das Konzept des NativeScript GridLayout verstehen

Die Datenliste ist stark vom NativeScript GridLayout abhängig. Um eine moderne Benutzeroberfläche zu erhalten, ist es möglich, innerhalb jeder Zeile der Liste eine Datentabelle zu verwenden. Möglich ist das beispielsweise so:

<GridLayout
    rows="auto"
    columns="auto * auto">
</GridLayout>

Damit wird eine Tabelle erzeugt, bei der jede Zeile nur so viel Platz belegt, wie sie tatsächlich benötigt. Bezüglich der Spalten, erhalten die erste und die dritte Spalte den erforderlichen Platz und die verbleibende Nutzfläche wird der zweiten Spalte zugewiesen.
Die Anordnung der Spalten und Zeilen lässt sich über jede UI-Komponente vornehmen, etwa so:

<Label
    text="Hello World"
    row="0"?
    col="1">
</Label>

Schritt 8: Die Pokémon-Daten in einer Liste ausgeben

Mit den Kenntnissen über den Aufbau des GridLayout lässt sich ein List View erzeugen. Dazu dient das folgende Markup im StackLayout:

<ListView
    [items]="pokemon">
   <template
        let-monster="item"
        let-index="index">
    </template>
</ListView>

Die Listenelemente stammen aus dem Public Array, das die Pokémon-Daten enthält. Jedes Element wird in einem monster-object gespeichert und der Index aufgezeichnet. Das GridLayout – es ist das gleiche wie in Schritt 7 – residiert im template. Die erste Spalte sieht so aus:

<Label?
    text="{{index + 1}}."
    row="0"
    col="0"
    marginRight="10">
</Label>

Die zweite Spalte enthält den Pokémon-Namen:

<Label
    [text]="monster.name"
    row="0"
    col="1">?
</Label>

Der nächste Schritt enthält Anmerkungen zur dritten Spalte.

Schritt 9: Die Sprite-Daten laden

Die erfassten Pokémon-Sprite-Daten müssen der Applikation zugänglich gemacht werden. Das The ~-Directory, auch als home-Directory bezeichnet, fungiert als app-Directory des Projekts.

<Image
src="~/images/{{index + 1}}.png"
row="0"
col="2">
</Image>

Da die Sprite-Daten im app/images-Directory abgelegt wurden, ist über die Indexnummer ein Zugriff auf die Images möglich. Jedes Image ist ein numerischer Wert, der in einem Indexwert abgebildet ist, der wiederum die Pokémon-Nummer repräsentiert. Damit ist auch die dritte Spalte in der Listenzeile beschrieben.

Schritt 10: Den List View mit CSS gestalten

Standardmäßig sieht die Liste nicht sehr attraktiv aus und sollte daher mit CSS aufbereitet werden. Im File app/app.css kommen daher folgende Zeilen hinzu:

.pokemon-number {
    font-weight: bold;
}

Die Pokémon-Nummer wird damit fett dargestellt.

.pokemon-name {
    text-transform: capitalize;
}

Das API liefert die Pokémon-Namen in Kleinbuchstaben. Die zuletzt aufgeführte Klasse wandelt das erste Zeichen in einen Großbuchstaben um. Zudem können diese Klassen ähnlich wie in Standard-HTML den <Label>-Elementen hinzugefügt werden.

Schritt 11: Die Images mit CSS Keyframes animieren

Tolle Apps benötigen einprägsame Elemente auf der Benutzeroberfläche. Animationen, egal wie einfach sie auch sein mögen, sind dafür ein geeignetes Mittel. Dafür gibt es einige Möglichkeiten, am einfachsten geht es mit CSS @keyframes.

@keyframes poke-img {
    from {
        opacity: 0;
        transform: rotate(0deg);
    }
    to {
        opacity: 1;
        transform: rotate(360deg);
    }
}

Mit diesen Anweisungen wird das Element um 360 Grad gedreht und gleichzeitig eingeblendet.

.pokemon-image {
    animation-name: poke-img;
    animation-duration: 1s;
    animation-delay: 1s;
    opacity: 0;
}

Diese Klasse wird auf die <Image>-Komponente angewendet. Sie definiert die Animation und wie lange sie läuft. CSS Keyframes sind jedoch nicht die einzige Möglichkeit zur Animation in NativeScript. Angular 2 verfügt über ein eigenes Animation-Framework [8], mit dem Entwickler vergleichbare Ergebnisse erzielen können.

Schritt 12: Die Listenelemente um Click Events erweitern

Im Schritt 4 wurde mit dem TypeScript-File die showInformation-Methode eingeführt. Sie wird jetzt über die Benutzeroberfläche aufgerufen. Tap-Events – manchmal auch als Click-Events bezeichnet – lassen sich auf UI-Elemente anwenden. So ist es beispielsweise sinnvoll, diese Events mit den Elementen der Benutzeroberfläche des GridLayouts einzusetzen:

<GridLayout
    rows="auto"
    columns="auto * auto"
    (tap)="showInformation(index+1)">

Das Tap-Attribut ruft die Funktion direkt auf. Da es kein Pokémon mit der Nummer Null gibt, muss jedem Index eine Eins hinzugefügt werden.

Schritt 13: Informationen zu einem speziellen Pokémon erfassen

Im Schritt 2 wurde der Zugriff auf Pokémon RESTful APIs erwähnt, um eine Liste aller verfügbaren Pokémons zu erhalten. Jetzt wird der Zugriff auf ein bestimmtes Pokémon benötigt und dazu dessen Nummer verwendet. In der showInformation-Methode finden sich folgende Zeilen:

this.http.get("https://pokeapi.co/api/v2/pokemon/" + index)
.map(result => result.json())
.flatMap(result => result.types)
.map(result =>
    (<any> result).type.name
)
.toArray()
.subscribe(result => {
    this.showDialog(result);
});

Das Resultat dieser asynchronen Abfrage wird in ein Array von Pokémeon-Attributen konvertiert und an die showDialog-Methode weitergeleitet, die das Resultat auf einem Bildschirm darstellt.

Schritt 14: Native-Alert-Dialoge auf Anfrage anzeigen

Um Informationen in einem Dialog anzeigen zu können, muss die benötigte Komponente zunächst in das TypeScript-File importiert werden.

import dialogs = require("ui/dialogs");

Ist die Komponente importiert, sieht die showDialog-Methode etwa so aus:

dialogs.alert({
    title: "Information",
    message: "Pokemon of type(s) " + data.join(", "),
    okButtonText: "OK"
});

Die Methode nimmt das durch RxJS in der showInformation-Methode konstruierte Array und zeigt die Meldung – nach der Umwandlung des Arrays in einen String – an. Die Buttons können bei Bedarf weiter angepasst werden oder es können weitere Buttons für unterschiedliche Zwecke hinzukommen.

Schritt 15: Ein Native Platform Plugin hinzufügen

Es gibt eine Vielzahl von Plugins auf dem Markt, die Entwicklern die Arbeit deutlich vereinfachen. Das NoSQL-Datenbank-Plugin von Couchbase [9] beispielweise basiert auf nativen iOS- und Android-SDKs und lässt sich wie folgt einbinden:

tns plugin add nativescript-couchbase

Generische Plugins mit TypeScript-Definitionen sind etwa für autocomplete verfügbar und können mit folgender Zeile in das references.d.ts-File des Projekts eingefügt werden.

/// <reference path="./node_modules/nativescript-couchbase/couchbase.d.ts" />

Diese Zeile ergänzt die Type-Definitionen des Couchbase-Plugins.

Nativer Android- oder iOS-Code, der als Teil eines SDKs mit Java, Objective-C oder Swift erstellt wurde, kann zusammen mit NativeScript eingesetzt werden; die nativen APIs sind via JavaScript zugänglich. Damit ist der Weg frei zu einem großen Native-Plugin-Ökosystem, ohne dass Entwickler die nativen Sprachen beherrschen müssten.

Wird beispielsweise Barcode-Scanning in einer Applikation benötigt, lässt sich die bekannte ZXing Library for iOS and Android in eine NativeScript-Anwendung einbinden. Darüber hinaus können Entwickler JavaScript Libraries, die eigentlich für Web-Applikationen gedacht sind, auch in NativeScript-Anwendungen einsetzen, ohne dass sie das Rad neu erfinden müssen.

Schritt 16: Den NoSQL Database Provider vorbereiten

Alle Datenbank-Interaktionen sollten mit einem einzelnen File, bekannt als Angular 2 Provider, erfolgen. Dazu sollte im Projekt ein app/database.ts-File mit diesen Programmzeilen angelegt werden:

export class Database {
    private db: any;
    constructor() {
        this.db = new Couchbase("db");
    }
    getDatabase() {
        return this.db;
    }
}

Der Code erzeugt und öffnet eine lokale Datenbank und retourniert eine Kopie der Datenbank zur weiteren Bearbeitung. Das Couchbase-Plugin wird so importiert:

import {
    Couchbase
} from 'nativescript-couchbase';

Als nächstes wird die Datenbank in den constructor des Projekt-Files app/app.component.ts injiziert:

constructor(private database: Database

Abschließend muss die Datenbank-Komponente in die @Component des Provider Arrays eingefügt werden:

providers: [Database]

Schritt 17: Die Pokémon-API-Daten zwischenspeichern

Um eine gute Performance zu erzielen, bietet es sich an, die API-Daten in einem Cache vorzuhalten. Der direkte Zugriff verlangsamt die Verarbeitung und Wartezeiten sind bei keiner Anwendung akzeptabel. In der subscribe-Methode des ngOnInit-HTTP-Request sind diese Zeilen hinzuzufügen:

.subscribe(result => {
    this.database
        .getDatabase()
        .createDocument(result);
    this.pokemon.push(result);
}

Die Daten werden sowohl gespeichert als auch in das Array gepusht. Durch diesen Caching-Schritt können alle Prozesse normal weiterlaufen.

Schritt 18: Einen NoSQL MapReduce View erzeugen

Beim Caching geht es darum, die gespeicherten Daten abfragen zu können, anstatt einen HTTP-Request abzusetzen. In der constructor-Methode des Files kommen daher diese Zeilen hinzu:

this.db.createView(
    "pokemon",
    "1",
    (document, emitter) => {
        emitter.emit(document._id, document);
    }
);

In diesem View namens Pokémon, wird ein Key-Value-Paar aller Dokumente in der Datenbank ausgegeben. Sollten mehr Datentypen in der Datenbank vorliegen, lässt sich die Emitter-Funktion leicht erweitern.

Schritt 19: Gecachte Daten abfragen

Innerhalb der Methode ngOnInit des Files app/app.component.ts wird die Datenbank abgefragt und entschieden, ob ein HTTP-Request abgesetzt wird. Zunächst sind diese Befehlszeilen abzuarbeiten:

let rows = this.database
    .getDatabase()
    .executeQuery("pokemon");

Wenn rows weniger als eins ergibt, ist der Cache leer und es muss ein Request ausgeführt werden. Ist der Cache nicht leer, kann eine Liste der gespeicherten Daten geladen werden.

if(rows.length < 1) {
    // Do previous HTTP
} else {
    for(let i = 0; i < rows.length; i++) {
        this.pokemon.push(rows[i]);
    }
}

Falls sich keine Daten im Cache befinden, werden auf jeden Fall die HTTP-Requests ausgeführt.

Schritt 20: Die iOS App Transport Security Policies anpassen

Apple hat mit iOS 9 die App Transport Security (ATS) Policies eingeführt [10]. Damit entstand eine Blacklist mit unsicheren Services. Zur Kommunikation mit HTTP- und nicht mit HTTPS-Endpunkten, sind diese Befehlszeilen in das Projekt-File app/App_Resources/iOS/Info.plist einzufügen:

<key>NSAppTransportSecurity</key>
     <dict>
          <key>NSAllowsArbitraryLoads</key>
     <true />
</dict>

Damit kommen alle unsicheren Endpunkte auf eine Blacklist. Auf der Whitelist sind nur solche, die tatsächlich benötigt werden.

Schritt 21: Die Applikation mit einem Gerät oder per Simulation testen

Nachdem alle Arbeiten abgeschlossen sind, kann das Ergebnis per Simulation oder einem physikalischen Gerät getestet werde. Von der Kommandozeile aus wird dabei dieser Befehl ausgeführt:

tns emulate [platform]

Abhängig davon, mit welchem Simulator getestet werden soll, wird [platform] durch ios oder android ersetzt. Um mit Android zu testen, muss das Android SDK installiert und eingerichtet sein. Für den Test mit iOS wird Mac OS X mit X Code benötigt.

Fazit

Die Emulation einer NativeScript-Applikation liefert umfangreiche Logging-Informationen am Command Prompt oder am Terminal. Alle Fehler oder Output-Statements werden in der Kommandozeile angezeigt und ermöglichen eine sachgerechte Fehlersuche und -behebung. Für den Unit-Test stehen die bei JavaScript-Entwicklern beliebten Test-Frameworks Mocha, Jasmine oder QUnit zur Verfügung.

Für ein schnelleres Application Redeployment und das Testen von UI und Logic Features bietet NativeScript auf der Kommandozeile ein Live-Reload Feature. Anstatt nach jeder Änderung ein Build und Install durchzuführen, sucht Live Reload automatisch nach Änderungen, übermittelt sie schrittweise an den Simulator und reduziert damit deutlich die Wartezeiten. Läuft alles wie gewünscht, steht dem produktiven Einsatz nichts mehr im Wege.

Autor

Nic Raboy

Nic Raboy lebt und arbeitet in San Francisco und ist ein Verfechter moderner Web- sowie mobiler Entwicklungstechnologien. Er schreibt Tutorials, hält Vorträge auf Konferenzen und will damit das Verständnis für die...
>> Weiterlesen
botMessage_toctoc_comments_9210