Was ist Apache Cordova?

Apache Cordova ist ein Open-Source Framework zur Entwicklung von mobilen Applikationen. Diese werden mittels der Standard-Webtechnologien JavaScript, HTML und CSS implementiert und durch WebViews in native Anwendungen eingebettet. Man spricht bei diesem Ansatz auch von hybriden Mobilapplikationen. Grundsätzlich unterstützt Cordova verschiedene Ziel-Plattformen, welche aber durch einen Write-Once Ansatz dieselbe Codebasis verwenden (können).

Da Cordova somit nur die Basis für die Entwicklungs- und Laufzeitumgebung zur Verfügung stellt, werden interessanterweise keine Entscheidungen bzgl. weiterer Frontend-Technologien oder der verwendeten Benutzeroberflächen-Elemente getroffen. Somit kann eine Cordova-Anwendung beispielsweise mittels AngularReact oder Vue realisiert werden und dabei BoostrapMaterial UI oder auch Framework7 Komponenten verwenden.

Plugins ermöglichen einen über die Möglichkeiten von HTML hinausgehende Nutzung betriebssystemnahe Funktionalität wie den Zugriff auf Sensoren, Kameras oder das Dateisystem. Außerdem ist es möglich, bidirektionale Kommunikation zwischen der JavaScript-Laufzeit und der nativen Applikation durch ein Foreign Function Interface zu etablieren. Im Falle der Android Ziel-Plattform also zwischen JavaScript und Java.

Übersicht über die Architektur einer Cordova-Anwendung

 

Eine Android-Bibliothek als Ziel-Artefakt

Typischerweise werden nach dem Hinzufügen einer Ziel-Plattform in sich geschlossene Standalone Apps für eben diese Plattform durch Cordova erzeugt – unter Android also ein APK.

Im Folgenden wird am praktischen Beispiel ein Ansatz vorgestellt, wie eine Cordova-Anwendung als Android-Bibliothek bereitgestellt und die damit implementierte Benutzeroberfläche von anderen Host-Applikationen aus genutzt werden kann.

Dies ist besonders dann nützlich, wenn eine Cordova-Anwendung bestimmte UI-getriebene, fachliche Teilfunktionalitäten implementiert, welche dann in einen oder mehrere größere Anwendungskontexte integriert werden sollen. Als Beispiel sei eine native Host-App einer dritten Partei erwähnt, welche den übergreifenden Anwedungsrahmen bildet und bei Bedarf das durch Cordova implementierte UI aufruft und resultierende Teil-Ergebnisse weiterverarbeitet.

 

Erste Schritte mit Cordova

Folgende Schritte führen zur ersten Cordova-Anwendung, welche dann in den folgenden Kapiteln sukzessive erweitert wird. Dabei wird Node.js bzw. der Paket-Manager NPM vorausgesetzt. Diese installiert und aktualisiert man am einfachsten mit dem Node Version Manager oder der passenden Alternative für Windows. Da sich dieser Artikel mit Android als Ziel-Plattform beschäftigt, werden außerdem das Android SDK sowie die damit zusammenhängenden Abhängigkeiten vorausgesetzt.

$ npm install -g cordova (1)
$ cordova help (2)
$ cordova create CordovaLib (3)
  1. Cordova als globales NPM Modul installieren
  2. Alle CLI Parameter in der Übersicht
  3. Das Grundgerüst der Cordova-Anwendung erzeugen

Die resultierende minimale Cordova-Anwendung befindet sich nun im korrespondierenden Verzeichnis CordovaLib.

$ cd CordovaLib
$ find . -type f -print
./.npmignore
./res/README.md
./res/screen/webos/screen-64.png
... (1)
./config.xml (2)
./www/index.html
./www/css/index.css
./www/js/index.js
./www/img/logo.png
./package.json
./hooks/README.md
  1. Weitere plattform-spezifische Ressourcen
  2. Die globale Konfigurationsdatei

Die Funktionalität der App beschränkt sich auf eine einfache Darstellung bezüglich des Ladezustands der Cordova API. Ein zentrales Element ist neben der eigentlichen Anwendung im Unterverzeichnis www die Konfigurationsdatei config.xml, welche zahlreiche Aspekte der Verhaltensweise der Applikation definiert.

 

Es empfiehlt sich, transiente Verzeichnisse, welche durch nachfolgende Schritte erzeugt werden, von der Versionskontrolle auszuschließen:
CordovaLib/.gitignore
/node_modules/
/plugins/
/platforms/

Android als Ziel-Plattform

Der zuvor erzeugten minimalen Anwendung wird nun die entsprechende Plattform hinzugefügt.

$ cd CordoaLib
$ cordova platform add android
Using cordova-fetch for cordova-android@~7.0.0
Adding android project...
Creating Cordova project for the Android platform:
	Path: platforms/android
	Package: io.cordova.hellocordova
	Name: HelloCordova
	Activity: MainActivity
	Android target: android-26
Subproject Path: CordovaLib
Subproject Path: app
Android project created with cordova-android@7.0.0
Android Studio project detected
Android Studio project detected
Discovered plugin "cordova-plugin-whitelist" in config.xml. Adding it to the project
Installing "cordova-plugin-whitelist" for android

               This plugin is only applicable for versions of cordova-android greater than 4.0. If you have a previous platform version, you do *not* need this plugin since the whitelist will be built in.

Adding cordova-plugin-whitelist to package.json
Saved plugin info for "cordova-plugin-whitelist" to config.xml
--save flag or autosave detected
Saving android@~7.0.0 into config.xml file ...

Hierdurch werden Plattform-spezifische Verzeichnisse, Build-Direktiven sowie benötigte Abhängigkeiten im Unterverzeichnis platforms erstellt.

 

Diese flüchtigen Inhalte sollten nicht manuell verändert werden, da Änderungen mit dem oben genannten oder weiteren CLI Befehlen erneut überschrieben würden.
$ cd CordovaLib
$ find ./platforms -type f -print
./platforms/android/cordova/clean.bat
./platforms/android/cordova/version.bat
./platforms/android/cordova/android_sdk_version
./platforms/android/cordova/run.bat
./platforms/android/cordova/node_modules/plist/LICENSE
... (1)
./platforms/android/cordova/check_reqs
./platforms/android/cordova/log.bat
./platforms/android/cordova/build.bat
./platforms/android/cordova/clean
./platforms/android/cordova/version
./platforms/android/cordova/lib/install-device
... (2)
./platforms/android/cordova/check_reqs.bat
./platforms/android/cordova/log
./platforms/android/cordova/build
./platforms/android/cordova/defaults.xml
./platforms/android/cordova/run
./platforms/android/cordova/Api.js
./platforms/android/cordova/loggingHelper.js
./platforms/android/cordova/android_sdk_version.bat
./platforms/android/app/build.gradle
./platforms/android/app/src/main/res/mipmap-mdpi/icon.png
... (3)
./platforms/android/app/src/main/AndroidManifest.xml
./platforms/android/app/src/main/java/org/apache/cordova/whitelist/WhitelistPlugin.java
./platforms/android/app/src/main/java/io/cordova/hellocordova/MainActivity.java
./platforms/android/app/src/main/assets/www/index.html
... (4)
./platforms/android/android.json
./platforms/android/project.properties
./platforms/android/platform_www/cordova.js
./platforms/android/platform_www/cordova-js-src/exec.js
./platforms/android/platform_www/cordova-js-src/plugin/android/app.js
./platforms/android/platform_www/cordova-js-src/android/nativeapiprovider.js
./platforms/android/platform_www/cordova-js-src/android/promptbasednativeapi.js
./platforms/android/platform_www/cordova-js-src/platform.js
./platforms/android/platform_www/cordova_plugins.js
./platforms/android/.gitignore
./platforms/android/build.gradle
./platforms/android/CordovaLib/cordova.gradle
./platforms/android/CordovaLib/AndroidManifest.xml
./platforms/android/CordovaLib/project.properties
./platforms/android/CordovaLib/build.gradle
./platforms/android/CordovaLib/src/org/apache/cordova/CordovaPlugin.java
... (5)
./platforms/android/settings.gradle
./platforms/android/wrapper.gradle
  1. JavaScript Abhängigkeiten
  2. Cordova Werkzeuge
  3. Ressourcen
  4. Plattform-unabängige Quellen
  5. Plattform-spezifische Quellen

Die Anwendung kann mit dem folgenden Befehl in der Debug-Variante übersetzt werden. Somit wird das Ziel-Artefakt CordovaLib/platforms/android/app/build/outputs/apk/debug/app-debug.apk erzeugt.

$ cd CordoaLib
$ cordova build android
Android Studio project detected
ANDROID_HOME=/usr/local/Caskroom/android-sdk/3859397
JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home
studio
Starting a Gradle Daemon (subsequent builds will be faster)

BUILD SUCCESSFUL in 4s
1 actionable task: 1 executed
Subproject Path: CordovaLib
Subproject Path: app
Starting a Gradle Daemon, 1 incompatible Daemon could not be reused, use --status for details
...
BUILD SUCCESSFUL in 15s
47 actionable tasks: 47 executed
Built the following apk(s):
	/Users/aggregat/Documents/dev/eng/blog/CordovaLib/platforms/android/app/build/outputs/apk/debug/app-debug.apk

Auch das Ausführen der App im Emulator kann komfortabel über die Cordova-CLI angestoßen werden.

---
$ cd CordoaLib
$ cordova run android
...
Using apk: /Users/aggregat/Documents/dev/eng/blog/CordovaLib/platforms/android/app/build/outputs/apk/debug/app-debug.apk
Package name: io.cordova.hellocordova
INSTALL SUCCESS
LAUNCH SUCCESS
---

Die Anwendung im Android Emulator

Der Weg zum Ziel

Durch das obige Hinzufügen der Android-Plattform werden resultierende Gradle Build-Skripte und weitere Dateien erzeugt. Diese müssen angepasst werden um schlussendlich zu einer Android-Bibliothek als Ziel-Artefakt zu gelangen. Da es sich hier, wie bereits erwähnt, um flüchtige Inhalte handelt, werden plattform-spezifischen Anpassungen nicht manuell, sondern programmatisch und damit wiederholbar durch Veränderungen an der Cordova-Konfiguration config.xml erzielt. Cordova bietet unter Anderem mittels Hooks umfangreiche Möglichkeiten, den Build-Vorgang durch Skripte zu manipulieren.

 

Anpassungen an der Konfiguration

Die für die Erstellung der Bibliothek benötigten Änderungen und Hooks werden wie folgt vorgenommen:

CordovaLib/config.xml
<?xml version='1.0' encoding='utf-8'?>
(1)
<widget
    id="de.engsoftwarelabs.cordovalib"
    android-activityName="CordovaLibActivity"
    version="1.0.0"
    xmlns="http://www.w3.org/ns/widgets"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:cdv="http://cordova.apache.org/ns/1.0">
    (2)
    <name>CordovaLib</name>
    <description>
        A sample Apache Cordova library that responds to the deviceready event.
    </description>
    <author email="christof.brungraeber@eng-softwarelabs.de" href="https://www.eng-softwarelabs.de">
        ENGINEERING Software Labs
    </author>
    <content src="index.html" />
    <plugin name="cordova-plugin-whitelist" spec="1" />
    <access origin="*" />
    <allow-intent href="http://*/*" />
    <allow-intent href="https://*/*" />
    <allow-intent href="tel:*" />
    <allow-intent href="sms:*" />
    <allow-intent href="mailto:*" />
    <allow-intent href="geo:*" />
    <platform name="android">
        <allow-intent href="market:*" />
        (3)
        <hook src="scripts/android/modify-build-gradle.js" type="after_platform_add" />
        <hook src="scripts/android/modify-manifest.js" type="after_platform_add" />
        <hook src="scripts/android/copy-cordova-sources.js" type="before_build" />
        <resource-file src="scripts/android/gradle.properties" target="gradle.properties" />
        <config-file mode="merge" parent="/manifest" target="app/src/main/AndroidManifest.xml">
            <uses-sdk android:minSdkVersion="19" />
        </config-file>
    </platform>
    <platform name="ios">
        <allow-intent href="itms:*" />
        <allow-intent href="itms-apps:*" />
    </platform>
    <engine name="android" spec="^7.1.1" />
</widget>
  1. Anpassungen am Namensraum, Bezeichner der Aktivität sowie zusätzliche XML Namensräume
  2. Anpassungen am Titel, Beschreibung, etc.
  3. Die für die Erstellung der Bibliothek benötigten Änderungen und Hooks

Im Besonderen werden drei Hooks definiert, welche in einer moderneren ES-Sprachvariante implementiert sind und damit eine aktuelle Node.js Version voraussetzen.

 

Aktionsskript: Manipulation des Android-Manifests

Das folgende Aktionsskript entfernt Attribute aus dem Manifest, um nachgelagerte Konflikte beim Übersetzen der Host-Applikation im Zusammenspiel mit der bereitgestellten Bibliothek zu vermeiden.

CordovaLib/scripts/android/modify-manifest.js
const { join } = require('path');
const { promisify } = require('util');
const { readFile, writeFile } = require('fs');

const readFileAsync = promisify(readFile),
      writeFileAsync = promisify(writeFile);

module.exports = async (ctx) => {

  const {
    requireCordovaModule,
    opts: {
      platforms,
      projectRoot,
    },
  } = ctx;

  if(platforms.indexOf('android') >= 0) {

    const platformRoot = join(projectRoot, 'platforms', 'android'),
          manifestLocation = join(platformRoot, 'app', 'src', 'main', 'AndroidManifest.xml'),
          _ = requireCordovaModule('lodash');

    const RULES = [
      [
        /android:versionCode="[^"]+"/,
        ``,
      ],
      [
        /android:versionName="[^"]+"/,
        ``,
      ],
      [
        `android:icon="@mipmap/icon"`,
        ``,
      ],
      [
        `android:label="@string/app_name"`,
        ``,
      ],
    ];

    await writeFileAsync(manifestLocation,
                         _.reduce(RULES,
                                  (result, [ a, b ]) =>
                                    result.replace(a, b),
                                  await readFileAsync(manifestLocation, 'utf8')),
                         'utf8');
  }
};

Aktionsskript: Manipulation der Build-Direktiven

Das folgende Skript aktiviert primär die Android Library Extension im build.gradle.

CordovaLib/scripts/android/modify-build-gradle.js
const { join } = require('path');
const { promisify } = require('util');
const { readFile, writeFile } = require('fs');

const readFileAsync = promisify(readFile),
      writeFileAsync = promisify(writeFile);

module.exports = async (ctx) => {

  const {
    requireCordovaModule,
    opts: {
      platforms,
      projectRoot,
    },
  } = ctx;

  if(platforms.indexOf('android') >= 0) {

    const platformRoot = join(projectRoot, 'platforms', 'android'),
          buildGradleLocation = join(platformRoot, 'app', 'build.gradle'),
          _ = requireCordovaModule('lodash');

    const RULES = [
      [
        `apply plugin: 'com.android.application'`,
        `apply plugin: 'com.android.library'`,
      ],
      [
        `applicationId privateHelpers.extractStringFromManifest("package")`,
        ``,
      ],
    ];

    await writeFileAsync(buildGradleLocation,
                         _.reduce(RULES,
                                  (result, [ a, b ]) =>
                                    result.replace(a, b),
                                  await readFileAsync(buildGradleLocation, 'utf8')),
                         'utf8');
  }
};

Aktionsskript: Kopieren der Cordova-Quellen

Das Kopieren der Cordova Basis-Quellen erfolgt durch das folgende Skript.

CordovaLib/scripts/android/copy-cordova-sources.js
const { join } = require('path');
const { promisify } = require('util');
const { exists, unlink, link, stat, mkdir, readdir, constants } = require('fs');

const existsAsync = promisify(exists),
      unlinkAsync = promisify(unlink),
      linkAsync = promisify(link),
      statAsync = promisify(stat),
      mkdirAsync = promisify(mkdir),
      readdirAsync = promisify(readdir);


const copy = async (src, dest) => {
  if(await existsAsync(dest))
    await unlinkAsync(dest);
  await linkAsync(src, dest);
};

const copyRecursive = async (src, dest) => {
  const srcExists = await existsAsync(src),
        destExists = await existsAsync(dest),
        stats = srcExists && await statAsync(src);

  if(srcExists && stats.isDirectory()) {
    if(!destExists)
      await mkdirAsync(dest);

    for(const childItemName of await readdirAsync(src)) {
      await copyRecursive(join(src, childItemName),
                          join(dest, childItemName));
    }
  }
  else
    await copy(src, dest)
};

module.exports = async (ctx) => {

  const {
    opts: {
      platforms,
      projectRoot,
    },
  } = ctx;

  if(platforms.indexOf('android') >= 0) {

    const platformRoot = join(projectRoot, 'platforms', 'android'),
          cordovaLibSrcLocation = join(platformRoot, 'CordovaLib', 'src'),
          appSrcLocation = join(platformRoot, 'app', 'src', 'main', 'java');

    await copyRecursive(cordovaLibSrcLocation,
                        appSrcLocation);
  }
};

Aktionsskript: Manipulation der Gradle-Properties

Zu guter Letzt erfolgt noch das Umbenennen des Ziel-Artefakts durch das folgende Skript.

CordovaLib/scripts/android/gradle.properties
archivesBaseName=cordovalib

Übersetzen der Android-Bibliothek

Das Übersetzen der Android-Bibliothek wird nun mittels der Cordova-CLI angestoßen. Die zuvor erstellten, auf ein APK als Ziel-Artefakt ausgerichteten Inhalte werden jedoch zunächst gelöscht.

$ cd CordovaLib
$ rm -rf ./platforms
$ cordova build android
Android Studio project detected
ANDROID_HOME=/usr/local/Caskroom/android-sdk/3859397
JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home
studio
Starting a Gradle Daemon, 1 incompatible Daemon could not be reused, use --status for details
...
BUILD SUCCESSFUL in 5s
36 actionable tasks: 36 executed
Built the following apk(s):

Somit steht das Ziel-Artefakt CordovaLib/platforms/android/app/build/outputs/aar/cordovalib-debug.aar zur weiteren Verwendung zur Verfügung.

$ ls -alh CordoaLib/platforms/android/app/build/outputs/aar
total 3416
drwxr-xr-x  3 aggregat  staff    96B  2 Aug 15:29 .
drwxr-xr-x  4 aggregat  staff   128B  2 Aug 15:29 ..
-rw-r--r--  1 aggregat  staff   1,7M  2 Aug 15:29 cordovalib-debug.aar

Verwenden der Android-Bibliothek in einer Host-Applikation

Als Anwendungsrahmen dient beispielhaft beine minimale Android-App, welche durch Android Studio erzeugt wird.

Neues Android Studio Projekt erstellen

Die im vorhergehenden Abschnitt übersetzte Bibliothek wird in das Projekt kopiert.

$ cp ./CordovaLib/platforms/android/app/build/outputs/aar/* ./HostApp/app/libs

Anpassungen am build.gradle fügen diese dem Projekt als Abhängigkeit im Dateisystem hinzu.

HostApp/app/build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "de.engsoftwarelabs.hostapp"
        minSdkVersion 19
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    repositories {
        flatDir { dirs 'libs' } (1)
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    implementation (name: 'cordovalib-debug', ext:'aar') (2)
}
  1. Flat Repository Resolver
  2. Die Bibliothek als Build-Abhängigkeit

Das Layout der Host-Applikation wird noch durch einen einfachen Button erweitert, welcher dann dazu dient, die durch Cordova realisierte Benutzeroberfläche aufzurufen.

Einfaches Layout mit einem Button

Die Erweiterung der Host-Applikation gestaltet sich denkbar einfach. Die in der Cordova-Bibliothek enthaltene Activity wird durch einen Intent gestartet. Es wir also eine Bindung zur Laufzeit zwischen zwei unterschiedlichen Komponenten realisiert – in diesem Fall zwischen der Activity der Host- und der Activity der Cordova-App.

HostApp/app/src/main/java/de/engsoftwarelabs/hostapp/MainActivity.java
package de.engsoftwarelabs.hostapp;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import de.engsoftwarelabs.cordovalib.CordovaLibActivity;

public class MainActivity extends AppCompatActivity {
  static final int LIB_RESULT = 0x1001;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Button startButton = (Button) findViewById(R.id.startButton);
    startButton.setOnClickListener( new View.OnClickListener() {

        @Override
        public void onClick(View v) {
          Intent intent = new Intent(getApplicationContext(), CordovaLibActivity.class);
          startActivityForResult(intent, LIB_RESULT);
        }
      });
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent resultData) {
    super.onActivityResult(requestCode, resultCode, resultData);
  }
}

Zur Demonstrationszwecken wird die Host-Applikation nun z.B. per Android Studio im Emulator ausgeführt.

Die Cordova-Bibliothek wurde durch die Host-Applikation aufgerufen

Zusammenfassung

Cordova bietet umfangreiche Möglichkeiten, Anpassungen an der Konfiguration und damit an resultierenden plattform-spezifischen Build-Direktiven vorzunehmen. In diesem Artikel haben wir diese Möglichkeiten genutzt, um mit relativ geringem Aufwand eine Cordova-Anwendung als Android-Bibliothek zur Verfügung zu stellen. Das Beispiel und den Artikel finden Sie auch auf unserem GitHub Account.