zurück zum Artikel

Native Android- und iOS-Apps mit React Native erstellen

Tam Hanna

React Native erweitert das JavaScript-Framework React um die Fähigkeit, Applikationen für Android und iOS zu erzeugen. Der Name von Facebooks Werkzeug weist bereits darauf hin, dass die Apps keine Kompromisse eingehen, sondern nativ sind.

FaceBooks React Native im Überblick

React Native erweitert das JavaScript-Framework React um die Fähigkeit, Applikationen für Android und iOS zu erzeugen. Der Name von Facebooks Werkzeug weist bereits darauf hin, dass die Apps keine Kompromisse eingehen, sondern nativ sind.

React Native ist kein klassisches Cross-Platform-Framework, dessen Apps überall gleichermaßen funktionieren. Stattdessen können Entwickler auf die Steuerelemente des jeweiligen Betriebssystems zurückgreifen.

Dieser Artikel stellt die wichtigsten neuen Features auf einer Workstation mit Ubuntu 14.04 vor. Grundlagenkenntnisse von React helfen beim Verständnis. Als Zielplattform dient Android 5.0 - frühere Versionen unterstützen der Befehl adb reverse zur Verbindung der Workstation mit dem Android-Gerät nicht.

React setzt Node.js voraus: Facebook empfiehlt das Herunterladen unter Nutzung des NVM-Versionsmanagers. Dazu sind folgende Kommandos notwendig:

sudo apt-get update
sudo apt-get install build-essential libssl-dev
curl \
https://raw.githubusercontent.com/creationix/nvm/v0.16.1/install.sh \
| sh

Nach dem Schließen des Terminals sind die Einstellungen aktiviert. Im nächsten Schritt folgt die Eingabe folgender Kommandos:

tamhan@TAMHAN14:~$ nvm install v4.1.1 && nvm alias default v4.1.1
############################################################### 100,0%
Checksums empty
Now using node v4.1.1
default -> v4.1.1
tamhan@TAMHAN14:~$ npm install -g watchman
tamhan@TAMHAN14:~$ npm install -g flow
tamhan@TAMHAN14:~$ npm install -g react-native-cli

Damit entsteht ein neues React-Native-Projekt. Anschließend muss ein Ordner erstellt und der Befehl react-native init HeiseSample ausgeführt werden. Dabei ist eine Internetverbindung zwingend erforderlich.

Sollte der Generatorprozess an fehlenden Rechten scheitern, helfen folgende Befehle:

tamhan@TAMHAN14:~$ sudo npm install -g react-native-cli
tamhan@TAMHAN14:~$ sudo react-native init HeiseSample

Der Lohn der Mühen ist die in Abbildung 1 gezeigte Projektstruktur. Die Verzeichnisse /android und /ios enthalten jeweils vollwertige Projektskelette, die eine native Android- und eine native iOS-Paketapplikation realisieren. Die eigentliche Intelligenz findet sich in den von der Package-Datei zusammengehaltenen js-Dateien.

React native hat seine Aufgabe erledigt (Abb. 1)

React Native hat seine Aufgabe erledigt (Abb. 1).

Für die Programmausführung unter Android ist eine vollwertige Installation des SDKs erforderlich. Als Mindestausstattung schreibt Facebook die folgenden Pakete vor:

Sobald das Smartphone mit der Workstation verbunden ist, setzt der Befehl export ANDROID_HOME=<sdkpfad> das Home-Verzeichnis von Android. Das Kommando sudo react-native run-android startet das Programm. Sollte die App dabei nicht auf dem Android-Gerät landen, weist eine Fehlermeldung wie Object [object Object] has no method 'spawnSync' auf das Vorhandensein einer veralteten Node.js-Version hin. Als Abhilfe kommt erneut der NVM-Befehl auf einer via sudo -s gestarteten Root-Shell zum Einsatz, um die Version zu aktualisieren. Das Resultat präsentiert sich wie in Abbildung 2 gezeigt.

Das von Facebook bereitgestellte HelloWorld-Beispiel funktioniert (Abb. 2)

Das von Facebook bereitgestellte HelloWorld-Beispiel funktioniert (Abb. 2).

In der Praxis kommt es beim Deployment zu Kommunikationsfehlern, die sich durch einen roten Bildschirm am Smartphone äußern. Ist das der Fall, helfen die folgenden Befehle, um den mit npm ausgelieferten Paketmanager durch eine aktuellere Variante zu ersetzen:

$ npm r -g watchman
$ sudo apt-get install python-dev
$ git clone https://github.com/facebook/watchman.git
$ cd watchman
$ git checkout v3.8.0 # the latest stable release
$ ./autogen.sh
$ ./configure
$ make
$ sudo make install

Für den nächsten Schritt ist ein weiteres Konsolenfenster erforderlich. Dort weist der Befehl reverse tcp:8081 tcp:8081 adb zur Weiterleitung von TCP-Paketen an; die Eingabe von react-native start startet den für die Auslieferung des JavaScripts notwendigen Server.

Mehr Infos

Mach's alleine

Die JavaScript-Ressourcen lassen sich als Ressource ins Projekt integrieren, um die Abhängigkeit vom Server zu brechen. Das Befolgen der in dieser Vorlage [1] beschriebenen Schritte führt dazu, dass jede Änderung am JavaScript-Code ein komplettes und rund 30 Sekunden dauerndes Redeployment der apk-Datei anfordert.

Aufgrund eines weiteren bekannten Fehlers gibt es an dieser Stelle ebenfalls Hindernisse: Abbildung 3 zeigt die Ergebnisse des nächsten Schritts.

Das Deployment funktioniert - leider gilt das nicht für den ausgelieferten Code (Abb. 3)

Das Deployment funktioniert - leider gilt das nicht für den ausgelieferten Code (Abb. 3).

Zur Lösung genügt das Eingeben des folgenden Befehls:

root@TAMHAN14:~/Desktop//HeiseReactNative/sample/HeiseSample/\
node_modules/react-native/node_modules/react-tools/docs/js#
rm react.js

Nach dieser Radikalkur ist es an der Zeit, erste Schritte in die Welt von React Native zu wagen. Das Facebook-Entwicklerteam beschloss, die von Kendo UI und anderen plattformübergreifenden Ansätzen betriebene Methode des Nachbauens von Steuerelementen nicht mitzumachen. Stattdessen gibt es eine Gruppe von UI-Elementen, die NativeScript stilgerecht auf nativen Widgets abbildet.

Zunächst soll das Problem der im offiziellen Teil des Frameworks fehlenden Button-Klasse behoben werden. An dieser Stelle ist ein kleiner Exkurs in die neue Welt Androids notwendig: Ein Android-Studio-Projekt ist im Grunde genommen ein Wrapper um eine .gradle-Build-Datei. Der ermöglicht das Laden eigenen Codes in Android Studio. Das eigentliche Deployment erfolgt direkt aus der IDE, solange der React-Native-Server läuft.

Bei der Ausführung von react-native aus einer Root Shell heraus muss Android Studio die Zugriffserlaubnis auf die Projektdateien erhalten. Das Starten per sudo ist nicht ratsam, da diese Vorgehensweise die Duplizierung des Android-SDKs erfordert:

root@TAMHAN14:~/Desktop/deadstuff/2015Oct/HeiseReactNative/sample# \
chown -R tamhan */

Nun sollte die Datei build.gradle durch die Auswahl von "Import Project" aus dem Willkommenbildschirm der IDE geladen werden. Danach definiert folgender Code eine neue Klasse namens ButtonClass.java:

import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;

public class ButtonClass extends
SimpleViewManager<Button>
{
@UIProp(UIProp.Type.STRING)
public static final String DENGBRODENGDENG = "OI!";
@Override
public String getName()
{
return "HeiseButton";
}
@Override

protected Button createViewInstance(ThemedReactContext reactContext)
{
Button newButton= new Button(reactContext);
newButton.setText("Hallo React!");
return newButton;
}
}

getName() ist für das Zurückliefern des Namens zuständig, unter dem die JavaScript-Runtime Kontakt mit dem neu erstellten nativen Modul Kontakt aufnehmen wird. createViewInstance erzeugt eine neue Instanz des anzuzeigenden Widgets, die danach vom GUI-Stack des Frameworks weiterverarbeitet wird.

Die Deklaration der UIProp-Eigenschaft ist aufgrund eines Fehlers in React Native notwendig: Klassen ohne Properties werden vom GUI-Stack ignoriert (siehe GitHub [2] und Abb. 4).

Hier fehlt eine Property (Abb. 4).

Hier fehlt eine Property (Abb. 4).

In MainActivity findet sich die Deklaration des ReactInstanceManagers, der die Runtime über die diversen in der Applikation enthaltenen Module informiert. Von Haus aus sieht der relevante Teil des Methodenkörpers so aus:

@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
mReactRootView = new ReactRootView(this);
mReactInstanceManager = ReactInstanceManager.builder()
.setApplication(getApplication())
.setBundleAssetName("index.android.bundle")
.setJSMainModuleName("index.android")
.addPackage(new MainReactPackage())
. . .

Wer MainReactPackage durch Drücken von CTRL+B reflektiert, erkennt, dass die Klasse die diversen von Facebook bereitgestellten Module einbindet. Für den konkreten Fall ist createViewManagers besonders interessant – die von Facebook bereitgestellte Implementierung sammelt ein gutes Dutzend Klassen in einem Array, das daraufhin an den Aufrufer zurückgegeben wird:

@Override
public List<ViewManager>
createViewManagers(ReactApplicationContext
reactContext)
{
return Arrays.<ViewManager>asList(
new ReactDrawerLayoutManager(),
new ReactHorizontalScrollViewManager(),
. . .
new ReactViewManager(),
new ReactVirtualTextViewManager());
}

Für die Aktivierung von ButtonClass sollte HeiseReactPackage folgendermaßen ausschauen – die Version von createViewManagers holt die Ergebnisse der Implementierung in der Elternklasse, um sie danach um den Knopf zu erweitern:

public class HeiseReactPackage extends MainReactPackage
{
@Override
public List<ViewManager>
createViewManagers(ReactApplicationContext reactContext)
{
List<ViewManager> myList = super.createViewManagers(reactContext);
LinkedList<ViewManager> retList=new LinkedList<ViewManager>();
retList.addAll(myList);
retList.add(new ButtonClass());
return retList;
}
}

An dieser Stelle gibt es übrigens kein Optimierungspotenzial: Die von createViewManagers zurückgegebene AbstractList-Klasse wirft beim Aufrufen ihrer Add-Funktion eine Exception. In MainActivity muss nun nur noch die Zuweisung des Paketmanagers angepasst werden, um die Integration auf Java-Seite abzuschließen:

mReactInstanceManager =
ReactInstanceManager.builder()
. . .
.addPackage(new HeiseReactPackage()) . . .

Folgende Anpassung von index.android.js bringt den Knopf auf den Bildschirm:

var HeiseButton=React.requireNativeComponent("HeiseButton",null);
var HeiseSample = React.createClass(
{
render: function()
{
return (<View style={{flexDirection: 'row', height: 100, padding: 20}}>
<HeiseButton style={{ flex: 0.3}}/>
</View>);
}
});

In der Praxis wäre es wahrscheinlich sinnvoller, die Komponente in ein eigenes Modul auszulagern. Das Ausführen des Programms sollte zu dem in Abbildung 5 gezeigten Ergebnis führen.

Der native Button funktioniert (Abb. 5).

Der native Button funktioniert (Abb. 5).

Die von ButtonClass zurückgegebenen Elemente sind vollwertige Steuerelemente: Aus technischer Sicht spricht nichts dagegen, Ereignisse und Attribute ausschließlich auf Java-Seite festzulegen. Da diese Anwendungsarchitektur jedoch die Vorteile von React Native ad absurdum führen würde, exponieren die nächsten Schritte einige Attribute an JavaScript.

Als Erstes soll der im Steuerelement angezeigte Text ansprechbar werden. Dazu werden die bisher nutzlose Property aktiviert und angelieferte Werte in das Widget weitergeschrieben:

public class ButtonClass extends
SimpleViewManager<Button>
{
@UIProp(UIProp.Type.STRING)
public static final String MyText = "MyText";

@Override public void updateView(final Button view, final
CatalystStylesDiffMap props)
{
super.updateView(view, props);
if(props.hasKey(MyText))
{
view.setText(props.getString(MyText));
}
}
}

React Native ruft updateView immer dann auf, wenn eine Änderung an einer der als UIProp exponierten Eigenschaften auftritt. Der Beispiel-Code durchsucht das angelieferte Bundle nach dem Wert MyText und übergibt dem Steuerelement den gefundenen Inhalt.

UIProp wird im Rahmen der Initialisierung mit einem String versorgt. Dabei handelt es sich um den Namen, den die JavaScript-Engine als Attributname erwartet. Das Feature ist nicht auf Strings beschränkt – zum Zeitpunkt der Veröffentlichung dieses Artikels unterstützt Facebook die folgenden Datentypen:

public static enum Type 
{
BOOLEAN("boolean"),
NUMBER("number"),
STRING("String"),
MAP("Map"),
ARRAY("Array");
. . .
}

Auf Seiten der JavaScript-Applikation ist die Errichtung eines Properties-Objekts notwendig, das neben einem im Debugger anzuzeigenden Namen den Wert der jeweiligen Eigenschaft liefert:

var iface = 
{
name: 'HeiseKnopf',
propTypes: { MyText: React.PropTypes.string,},
};

var HeiseButton=React.requireNativeComponent("HeiseButton",iface);
var HeiseSample = React.createClass(
{
render: function()
{
return
(
<View style={{flexDirection: 'row', height: 100, padding: 20}}>
<HeiseButton style={{ flex: 0.3}} MyText="Neuer Text"/>
</View>
);
}
});

Wie bei der Deklaration des Properties ist auch hier eine zweistufige Vorgehensweise erforderlich. Der erste Schritt deklariert die zu unterstützenden Eigenschaften iface, um sie danach im Rahmen der eigentlichen Instanziierung zu beleben.

Mehr Infos

Neues HTML!

Änderungen am JavaScript-Teil der Applikation setzen kein komplettes Redeployment voraus. Für die Aktualisierung der laufenden Applikation reicht ein Klick auf den Menüpunkt Option Reload JS.

Zum Exponieren des Click-Events stehen drei Methoden zur Verfügung: In der offiziellen Dokumentation nutzt Facebook die im GUI-Stack eingebaute Event-Brücke, die Ereignisse über ein in UIManagerModuleConstants.java befindliches assoziatives Array "umlegt". Das Vorgehen fasst mehrere native Eventarten zu einem JS-Event zusammen, was aus Entwicklersicht nicht unbedingt sinnvoll ist.

Lösung Nummer zwei wäre das Übergeben eines Callback-Objekts:

@ReactMethod
public void funktion(Callback myCallback)
{
myCallback.invoke(e.getMessage());
}

Leider ist auch diese Vorgehensweise keine wirkliche Lösung: Ein Callback-Objekt darf laut der Dokumentation von React Native nur einmal aufgerufen werden und verfällt danach ersatzlos.

Als dritte Möglichkeit bietet sich das Absetzen eines normalen Events an. Dazu benötigt der Button einen Event-Handler. Da es sich dabei um normalen Android-Code handelt, beschränkt sich dieses Tutorial auf die Handler-Methode:

@Override
public void onClick(View v)
{
ReactContext reactContext = (ReactContext)v.getContext();
WritableMap params = Arguments.createMap();
params.putInt("id",22);
reactContext.getJSModule(DeviceEventManagerModule.
RCTDeviceEventEmitter.class)
.emit("onClicked", params);
}

Das Beschaffen von ReactContext und der Aufruf dessen getJSModule-Methode ermöglicht das eigentliche Abfeuern des Events, das über WritableMap zusätzliche Attribute erhält.

Nun ist eine Änderung des JavaScript-Teils notwendig, um auf die vom Knopf angelieferten Ereignisse angemessen reagieren zu können:

var HeiseButton=React.requireNativeComponent("HeiseButton",iface);
var DeviceEventEmitter = require('RCTDeviceEventEmitter');
var Subscribable = require('Subscribable');
var whoami;

var HeiseSample = React.createClass(
{
mixins: [Subscribable.Mixin],
aMethod:function(e)
{
whoami.setNativeProps({ MyText:"Autsch" });
},
componentDidMount: function()
{
DeviceEventEmitter.addListener('onClicked', this.aMethod);
},
render: function()
{
return (
<View style={{flexDirection: 'row', height: 100, padding: 20}}>
<HeiseButton style={{ flex: 0.3}} MyText="Neuer Text"
ref={function(it){whoami=it;}}/>
</View>
);
}
});

Das Entgegennehmen der vom Java-Code eingeschriebenen Ereignisse erfolgt über den RCTDeviceEventEmitter. Die per require() erzeugte Instanz der Klasse wird in componentDidMount zum Einschreiben eines Eventhandlers herangezogen: Die Impelementierung des Subscribable-Mixins ist aus syntaktischen Gründen erforderlich.

Besondere Aufmerksamkeit verdient das Ermitteln der Button-Referenz. Das ref-Objekt nimmt eine Funktion entgegen, die nach dem Erzeugen des jeweiligen Steuerelements aufgerufen wird. Eine globale Variable nimmt die gelieferte Instanz auf und kann so per setNativeProps zum direkten Verändern des Texts genutzt werden.

Eine abermalige Programmausführung zeigt einen wehleidigen Button.

An dieser Stelle sei noch darauf hingewiesen, dass React Native das Einbinden von in Java gehaltener Logik auch ohne das Hinzuziehen eines GUI-Widgets erlaubt. Als Elternklasse dient dabei ReactContextBaseJavaModule; die Anmeldung erfolgt im Rahmen der folgendermaßen aufgebauten createNativeModules-Funktion:

public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext)
{
return Arrays.<NativeModule>asList(
new AsyncStorageModule(reactContext),
. . .)}

Module unterscheiden sich von GUI-Komponenten vor allem dadurch, dass sie diverse Methoden und Eigenschaften exponieren. Die von Facebook bereitgestellte Dokumentation [3] ist in dieser Hinsicht mehr als lesenswert. Das Bearbeiten des gezeigten ToastModules hilft Entwicklern dabei, sich mit den Besonderheiten vertraut zu machen.

React Native ist kein echtes Cross-Plattform-Framework, weil es Entwickler zur Erstellung und Wartung zweier GUI-Stacks zwingt. Wer den Gutteil seiner Applikationslogik in React erstellt hat, sollte dem Produkt trotzdem eine Chance geben: Die Wartung unterschiedlicher Logikversionen ist im Allgemeinen komplexer als die Wartung zweier Benutzerschnittstellen.

Die im Moment auftretenden Kinderkrankheiten stören noch auffallend, dürften aber im Laufe der Zeit verschwinden. Facebook benötigt für React Native die Aufmerksamkeit von Drittentwicklern – die vorliegende Version dürfte dieses Ziel leider nicht erreichen.

Tam Hanna
befasst sich seit der Zeit des Palm IIIc mit Programmierung und Anwendung von Handheldcomputern. Er entwickelt Programme für diverse Plattformen, betreibt Onlinenews-Dienste zum Thema und steht für Fragen, Trainings und Vorträge gern zur Verfügung.
(rme [4])


URL dieses Artikels:
https://www.heise.de/-3025889

Links in diesem Artikel:
[1] http://stackoverflow.com/questions/32572399/react-native-android-failed-to-load-js-bundle
[2] https://github.com/facebook/react-native/issues/3164
[3] https://facebook.github.io/react-native/docs/native-modules-android.html
[4] mailto:rme@ix.de