Troll knutscht Androiden

Qt für Android – eine Einführung
Kommentare

Nokia-Manager animierten Symbian-Entwickler mit einem einfachen Versprechen zum Umstieg auf Qt: die neue Technologie sollte Apps für die nächste Milliarde Geräte ermöglichen.

Einige Monate – und einen CEO-Wechsel – nach diesem Versprechen wurde das Entwicklungsframework an Digia verkauft, da es mit der damaligen Version von Windows Phone inkompatibel war. Bogdan Vatra bewies eine geradezu prophetische Vorsehungskraft. Er begann die Arbeit an seiner als „Necessitas“ bezeichneten Portierung im Jahre 2010. Mittlerweile wird sein Projekt von KDE mit Serverressourcen versehen, Digia hat sich seine Dienste als Mitarbeiter gesichert. Für Qt-erfahrene Entwickler bedeutet dies, dass sie ihre Applikationen fortan auch auf Googles quelloffenem Betriebssystem anbieten können. Die dadurch in Aussicht stehende Reichweitensteigerung rechtfertigt den zusätzlichen Aufwand mit Sicherheit.

Leuchtturm

Qt war von Anfang an als Cross-Plattform-Bibliothek vorgesehen. Leider war das Portieren des QtGui-Moduls alles andere als einfach: Die von Qt angebotenen Steuerelemente mussten mit den Widgets aus dem GUI-Stack des Betriebssystems verdrahtet werden. Der einfachste Weg zur Lösung dieses Problems war das Mitbringen eines kleinen Fenstermanagers, der das eigentliche Rendering der Steuerelemente erledigt. Im Rahmen der Portierung muss man in diesem Fall nur mehr ein „Wedge“ schreiben, das die vom Renderer angelieferten Bitmapdaten in ein vom Betriebssystem bereitgestelltes Fenster kopiert und eventuelle Eingaben des Benutzers einsammelt.

Qt für Android basiert auf diesem Entwicklungsbranch. Eine Qt-für-Android-Applikation besteht – in der einfachsten Form – aus einer Java-Activity, die den Lighthouse-Window-Manager anwirft und die von ihm generierten Daten ausgibt. Die eigentliche Applikationsintelligenz findet sich in Form eines C++-Blobs, der den Qt-Code enthält.

Eine „komplette“ Distribution aller Qt-Bibliotheken ist einige Megabyte groß – bei kompakten Applikationen vervielfacht sich der Umfang der Distributionsdatei. Vatra löst dieses Problem durch ein als Ministro bezeichnetes Hilfsprogramm. Die in allen populären App Stores bereitstehende Software wird vom in die jeweilige Applikation einkompilierten Startcode angewiesen, die benötigten Dateien von einem Zentralserver herunterzuladen – wenn Ministro selbst noch nicht vorhanden ist, so wird der User zum Download des Programms aufgefordert.

Erste Schritte

Nach diesen einführenden Überlegungen ist es an der Zeit, erste Schritte in die Welt von Qt für Android zu setzen. Die im folgenden Artikel gezeigten Screenshots und Anweisungen entstanden unter einer 32-Bit-Version von Ubuntu – die Verwendung von Mac OS X und Windows ist ebenfalls möglich, Unix stellt die „preferred development platform“ des Projekts dar.

Öffnen Sie die Webseite, um das passende Distributionspaket herunterzuladen. Die rund 20 MB große Datei setzt das Vorhandensein einiger Pakete voraus, die in der folgenden Liste zusammengefasst sind:

  • build-essential
  • openjdk-6-jdk
  • ant, Version 1.8 (!!!)
  • ia32-libs-gtk, für 64-Bit-Betriebssysteme

Eventuell fehlende Pakete lassen sich mit apt-get install herunterladen. Im nächsten Schritt muss die Datei in einem Terminal als ausführbar gekennzeichnet und ausgeführt werden. Unter Ubuntu sieht der dazu notwendige Code so aus:

chmod +x linux-online-necessitas-alpha4.1-sdk-installer 
./linux-online-necessitas-alpha4.1-sdk-installer 

Im Rahmen der Installation werden einige hundert Megabyte an Daten heruntergeladen. Die von Haus aus voreingestellten Settings laden rund 2,6 GB herunter und belegen schließlich und endlich rund 5 GB auf der Festplatte.

Nach der Installation können Sie die im Verzeichnis /QtCreator/bin/ befindliche QtCreator-Datei starten. Wenn Sie andere, nur mit Root-Rechten lauffähige Qt-Werkzeuge installiert haben, so ist das Verwenden von sudo empfehlenswert – einige geteilte Dateien können sonst nicht angesprochen werden.

Necessitas wird von Haus aus mit Qt 4.8 ausgeliefert. Die Version 5.3 des Frameworks bringt einen wesentlich verbesserten GUI-Stack mit – sie lässt sich bei Bedarf von Hand installieren. Dazu ist ein etwas anderes Setup-Programm notwendig, das hier beschrieben wird und zum Download bereitsteht.

Die Frage nach dem GUI-Stack

Wer eine Qt-Applikation entwickelt, muss sich für einen GUI-Stack entscheiden. Das altgediente QtGui enthält eine Vielzahl von vorgefertigten Widgets, die primär für die Verwendung am Desktop vorgesehen sind. Auf QML bzw. Qt Quick basierende Anwendungen erstellen ihre Benutzerschnittstelle in einer Mischung aus JSON und JavaScript. Animationen gehen hier leichter von der Hand, komplexe Steuerelemente müssen dafür von Hand zusammengestellt werden. Research in Motion geht mit Cascades einen „Sonderweg“. Dieser GUI-Stack steht nur unter BlackBerry 10 zur Verfügung. Wenn Sie ihn in einem ihrer Programme einsetzen, müssen Sie das GUI auf jeden Fall neu schreiben. Besonders ärgerlich ist, dass sich Cascades aufgrund des geänderten Rendering-Pfads nicht mit QtGui oder QML kombinieren lässt.

Wir wollen hier aus Gründen der Einfachheit mit einer QtGui-Applikation arbeiten. Erstellen Sie Ihr Programm unter Verwendung des Projekt-Wizards. Er verhält sich im Großen und Ganzen wie von anderen Versionen von Qt Creator her bekannt – die Wahl der passenden CPU-Architektur hängt von der Zielgruppe ihres Produkts ab. Ein von Necessitas erstelltes Projekt unterscheidet sich nur wenig von normalen Projektskeletten. Der wichtigste Unterschied findet sich in der .pro-Datei, in der eine Gruppe von Android-Supportdateien referenziert werden:

OTHER_FILES += \
  android/res/values-fr/strings.xml \
  ...
  android/src/org/kde/necessitas/origo/QtActivity.java \
  android/src/org/kde/necessitas/origo/QtApplication.java

Im Ordner Other files finden Sie neben den Ressourcen auch einige Java-Dateien, die den in der Einleitung erwähnten Viewer realisieren. Im Ordner origo dürfen Sie eigene Java-Klassen anlegen, die als „Brücke“ zu JNI dienen.

Spezielle Projekteinstellungen

Die Android-spezifischen Einstellungen finden Sie in der Rubrik Projects. Im „Run“-Tab finden Sie die beiden in den Abbildungen gezeigten Dialoge, die weitere Informationen zur Projektkonfiguration enthalten (Abb. 1).

Abb. 1: „Package configurations“ enthält diverse Android-spezifische Einstellungen

Abb. 1: „Package configurations“ enthält diverse Android-spezifische Einstellungen

Android stellt einige Anforderungen an Applikationen. Die mit Abstand wichtigsten Einstellungen finden sich in den Tabs Manifest und Permissions – wer sich mit Eclipse auskennt, ist über die Bedeutung der dort befindlichen Einstellungen bestens informiert. Im Feld Libraries dürfen Sie die erforderlichen Module des Qt-Frameworks auswählen. Ein Klick auf den „Read information“-Knopf sorgt dafür, dass Necessitas die erforderlichen Informationen automatisch ermittelt. „Sign Package“ erlaubt Ihnen die Konfiguration der Signaturumgebung. Sie ist für die Identifikation der Applikationsherkunft verantwortlich.

Abb. 2: Das Feld „Deploy Configurations“ entscheidet, woher die Bibliotheken stammen sollen

Abb. 2: Das Feld „Deploy Configurations“ entscheidet, woher die Bibliotheken stammen sollen

Wer eine Android-Applikation in einen Store ausliefert, verwendet normalerweise den in der Einleitung erwähnten Ministro. Beim lokalen Debuggen ist es oft sinnvoller, die schon am Gerät befindlichen Bibliotheken zu verwenden – das lässt sich im Feld Deploy configuration festlegen (Abb. 2).

Applikation testen

Unser Projektskelett enthält eine einsatzbereite Anwendung, die wir im nächsten Schritt auf ein Endgerät bringen wollen. Dazu müssen wir ein im Entwicklermodus befindliches Telefon mit der Workstation verbinden und danach auf den bekannten „Play“-Button klicken.

Im Rahmen des ersten Starts wird die Applikation – wie in Abbildung 3 gezeigt – nach Ministro fragen. Das Bestätigen der Anforderung führt Sie in den Play Store. Installieren Sie die Applikation wie von Spielen und/oder Programmen gewohnt und starten Sie den Deployment-Prozess danach neu. Im zweiten Durchlauf lädt Ministro die notwendigen Bibliotheken herunter. Ab diesem Zeitpunkt ist Ihre Applikation lauffähig – leider erscheint auf den meisten Telefonen nur ein schwarzer Bildschirm.

Abb. 3: Hier fehlt der Bibliothekendownloaddienst

Abb. 3: Hier fehlt der Bibliothekendownloaddienst

Dieses auf den ersten Blick seltsame Verhalten liegt daran, dass Qt für Android den GUI-Stack des Herstellers im Rahmen des Startprozesses analysiert. Telefone mit organischem Display verwenden einen schwarzen Hintergrund, um Energie zu sparen. Fügen Sie einen Button oder ein anderes Steuerelement hinzu, um eine optisch lauffähige Applikation zu erhalten. Das Endresultat ist in Abbildung 4 gezeigt.

Abb. 4: Der Push-Button erscheint am Bildschirm des Telefons

Abb. 4: Der Push-Button erscheint am Bildschirm des Telefons

Applikation testen

Wenn Ihr Zielgerät eine halbwegs aktuelle Version von Android (>= 2.3) mitbringt, können Sie Ihre Kompilate wie von anderen Plattformen gewohnt debuggen. Leider ist dazu etwas Vorbereitungsarbeit notwendig – ein „einfacher Klick“ auf den „Debug“-Button führt in vielen Fällen zu einer sofort abstürzenden Handyapplikation.

Der erste Schritt zur Lösung dieses Problems ist, die IDE wie weiter oben beschrieben zur Nutzung der lokalen Qt-Bibliotheken zu zwingen. Wenn die Programmausführung in diesem Zustand scheitert, liegt oft ein Fehler in der Konfiguration des Projekts vor. Ein klassisches Problem ist ein zu langes Verzeichnis zum SDK – in diesem Fall genügt es, einen Symlink anzulegen.

Manche Benutzer berichten auch bei „korrekter“ Projektkonfiguration von diversen seltsamen Fehlern im Zusammenspiel mit On-Device-Debugging. In diesem Fall empfiehlt sich die Verwendung des Emulators, der im Allgemeinen besser funktioniert.

Native APIs ansprechen

Da Qt-Applikationen im Grunde genommen nur native Programme mit einer aufwändigen Klassenbibliothek sind, können Entwickler auf native Funktionen der Plattform zugreifen. Dies ist der Portabilität des Endprodukts nicht zuträglich, da eine Abstraktionsebene und ein plattformspezifisches Modul erforderlich sind. Für Nokia stellte dies ein ernsthaftes Problem dar: Die Finnen wollten ihre Telefone mit drei oder sogar vier verschiedenen Betriebssystemen gleichzeitig anbieten, die allesamt von Qt aus ansprechbar sein sollten.

Qt Mobility war als Paket von universell nutzbaren APIs vorgesehen, die diverse mobilspezifische Hardwaremodule ansprechbar machten. Das Necessitas-Team hat mittlerweile den Gutteil der damals spezifizierten Funktionen portiert – Location und Co lassen sich mittlerweile direkt aus Qt heraus ansprechen. Leider ist es nicht möglich, alle Features des Betriebssystems über Wrapper abzubilden. In diesem Fall schlägt die Stunde des Java Native Interface. Google sah diese Schnittstelle im NDK vor, um Java-Code das Ansprechen von nativen Modulen zu ermöglichen.

Der durchschnittliche Qt-Entwickler kehrt die Sache einfach um. Die von Google im NDK vorgesehenen JNI-Werkzeuge lassen sich nämlich auch zum Ansprechen von Java-Code nutzen. Wir erweitern unser Produkt um eine im Unterordner origo befindliche .java-Datei, die aus zwei verschiedenen Methoden besteht. ShowAlert() ruft einen Background-Worker auf, der in einer separaten Datei namens SUSBGTask.java realisiert wird (Listing 1).

Im nächsten Schritt muss das Projekt um einen JNI-Adapter ergänzt werden. Dieser agiert als „Schnittstelle“ zwischen dem C- und dem Java-Code. Seine Struktur ist in Listing 2 dargestellt.

Listing 1

public class SUSClass
{
  public SUSClass()
  {
  }
  public int addTwoNumbers(int _a, int _b)
  {
    return _a + _b;
  }
  public void showAlert()
  {
    SUSBGTask task = new SUSBGTask();
    task.execute(new String[] { "some data" });
  }
}

Listing 2

#include "susjniadapter.h"
#include &ltQDebug&gt
static JavaVM* s_javaVM = 0;
static jclass myClassID = 0;
static jmethodID myConstructorMethodID=0;
static jmethodID myAddMethodID=0;
static jmethodID myMessageMethodID=0;

SUSJNIAdapter::SUSJNIAdapter()
{
}
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/)
{
  JNIEnv* env;
  if (vm->GetEnv(reinterpret_cast&ltvoid**>(&env), JNI_VERSION_1_6) != JNI_OK) {
    return -1;
  }
  s_javaVM = vm;
  jclass clazz=env->FindClass("org/kde/necessitas/origo/SUSClass");
  if (!clazz)
  {
    qCritical()<<"Can't find class";
    return -1;
  }
  myClassID = (jclass)env->NewGlobalRef(clazz);
  myConstructorMethodID = env->GetMethodID(myClassID, "", "()V");
  if (!myConstructorMethodID)
  {
    return -1;
  }
  myMessageMethodID = env->GetMethodID(myClassID, "showAlert", "()V");
  if (!myMessageMethodID)
  {
    return -1;
  }
  myAddMethodID = env->GetMethodID(myClassID, "addTwoNumbers", "(II)I");
  if (!myAddMethodID)
  {
    return -1;
  }
  return JNI_VERSION_1_6;
}

Im Header von SUSJNIAdapter findet sich keine Raketenwissenschaft. Die Inklusion von jni.h sorgt dafür, dass der Compiler auf die diversen JNI-bezogenen Methoden zugreifen kann (Listing 3).

Listing 3

#include <jni.h>
class SUSJNIAdapter
{
  public:
    SUSJNIAdapter();
    void showAlert();
    int addStuff(int _a, int _b);
    jobject myJNIObject;
};

Die im Header nicht vorhandene Methode OnLoad wird von der Java-VM im Rahmen der Initialisierung aufgerufen. Sie bekommt einen Zeiger auf die virtuelle Maschine, der zur Ermittlung der diversen für den Aufruf in der Klasse befindlichen Funktionen notwendigen Informationen herangezogen wird. Die Erstellung der Signaturen für die einzelnen Methoden ist eine relativ komplexe Wissenschaft. Hier finden Sie eine Übersicht, die die wichtigsten „Semantics“ erklärt.

Im nächsten Schritt können wir den eigentlichen Objektkonstruktor realisieren, der in unserem Fall aussieht wie in Listing 4 dargestellt.

Listing 4

SUSJNIAdapter::SUSJNIAdapter()
{
  JNIEnv* env;
  if (s_javaVM->AttachCurrentThread(&env, NULL)<0)
  {
    qCritical()<<"AttachCurrentThread failed";
    return;
  }
  // Create a new instance
  myJNIObject = env->NewGlobalRef(env-<NewObject(myClassID, myConstructorMethodID));
  if (!myJNIObject)
  {
    qCritical()<<"Can't create class";
    return;
  }
  s_javaVM->DetachCurrentThread();
}

Die Nutzung unseres weiter oben abgedruckten Objekts setzt eine Instanz voraus. NewGlobalRef ist für die Erstellung neuer Objekte erforderlich, der zurückgegebene Zeiger wird in einer Member-Variable von SUSJNIAdapter gespeichert. An dieser Stelle treffen Sie auf eine weitere Besonderheit der Arbeit mit JNI. Die Java Virtual Machine läuft in einem separaten Thread, der sich nicht ohne Weiteres ansprechen lässt. Sie müssen Ihren „Applikationsthread“ vor der Nutzung der VM mittels AttachCurrentThread anmelden, der darauffolgende Aufruf von DetachCurrentThread gehört zum guten Ton. Damit fehlt uns nur mehr die Deklaration der Akzessor-Funktionen. Sie haben die Aufgabe, die von der C++-Applikation eingehenden Informationen an die unterliegende Java-Klasse weiterzureichen (Listing 5).

Listing 5

SUSJNIAdapter::SUSJNIAdapter()
{
  JNIEnv* env;
  if (s_javaVM->AttachCurrentThread(&env, NULL)<0)
  {
    qCritical()<<"AttachCurrentThread failed";
    return;
  }
  // Create a new instance
  myJNIObject = env->NewGlobalRef(env-<NewObject(myClassID, myConstructorMethodID));
  if (!myJNIObject)
  {
    qCritical()<<"Can't create class";
    return;
  }
  s_javaVM->DetachCurrentThread();
}

In beiden Methoden beginnen wir, indem wir unseren Applikationsthread bei der virtuellen Maschine anmelden. Im nächsten Schritt folgt die Nutzung einer in env enthaltenen Call*Method-Funktion. Dabei kommt stets dasselbe Schema zum Einsatz: Als ersten Parameter übergeben wir das Objekt, als zweiten einen Verweis auf die aufzurufende Methode.

Alle Call*-Funktionen nehmen eine variable Anzahl von Parametern entgegen, die 1:1 an die Java-Funktion weitergegeben werden. Im Fall von addStuff nutzen wir CallIntMethod. Sie retourniert einen Wert vom Typ jint, der sich erfreulicherweise direkt und 1:1 in ein normales Integer konvertieren lässt.

Damit fehlt uns nur mehr der eigentliche Aufruf. Erstellen Sie eine Instanz der Adapterklasse und rufen Sie die jeweilige „Untermethode“ auf . Dank der Einfachheit unserer Beispielklasse gibt es hier nicht mehr zu sehen. Bei komplexeren Methoden müssen Sie die zurückgegebenen Resultate unter Nutzung der diversen JNI-Methoden „lesbar machen“ – weitere Informationen hierzu finden Sie in Java-Lehrbüchern.

Spiele mit Widgets

Die meisten Android-Applikationen werden heute durch Werbung finanziert. Leider bieten nur wenige Werbenetzwerke passende SDKs an – die meisten Unternehmen beschränken sich darauf, ihren Klienten ein natives Widget zur Verfügung zu stellen. Am einfachsten lässt sich dies durch das Anbieten einer hauseigenen und in Java gehaltenen Activity realisieren, die von der von Necessitas angebotenen QtActivity abgeleitet ist. Wenn sie alle überschriebenen Methoden mit einem Aufruf auf die in der Basisklasse implementierte Routine abschließen, können Sie mehr oder weniger beliebige Intelligenz „einschreiben“.

Digia bietet auf dem offiziellen Firmenblog ein detaillierteres Tutorial zum Thema an, das die genaue Vorgehensweise schildert [5]. Es ist auch für jene Entwickler interessant, die eigentlich kein Interesse an IAP haben – die vorgestellten Konzepte lassen sich nämlich auch mit diversen anderen Drittanbieter-SDKs nutzen.

Qt professionell

Trolltech bietet sein Framework seit Jahren in verschiedenen Lizenzen an. Die unter der LGPL stehende Version ist kostenlos, zwingt Entwickler aber zum Offenlegen eventueller Änderungen am übergebenen Code. Dies ist im Mobilmarkt nicht unbedingt unproblematisch, da manche Stores das „dynamische Nachladen“ von Bibliotheken aus Sicherheitsgründen nicht erlauben.

Die Frage nach der Legalität des statischen Verlinkens von LGPL-Code ist seit Jahren ungeklärt. Viele Juristen gehen davon aus, dass Entwickler in diesem Fall zumindest zum Anbieten ihres Objektcodes verpflichtet sind – eine nicht nur im Rüstungsbereich unvorstellbare Situation.

Digia begegnet diesem Problem durch das Anbieten einer kommerziellen Lizenz, die das statische Verlinken des Programmcodes erlaubt. Leider ist dieser nur in Form eines Abonnements verfügbar, das pro Monat 109 Euro kostet und sich im Laufe der ersten drei Monate nicht kündigen lässt.

Fazit

Wer mit C++ keine Probleme hat, sollte die Verwendung von Qt auf jeden Fall ins Auge fassen. Eine auf Basis des Frameworks erstellte Applikation lässt sich ohne weiteren Aufwand unter iOS, Tizen und am Desktop vertreiben. Minimaler Zusatzaufwand ermöglicht das Ansprechen von Sailfish OS und BlackBerry 10.

Gerade für kleinere und mittlere Entwickler ist dies ein nicht zu vernachlässigender Vorteil. Dank der „breiten Reichweite“ von Qt gibt es kaum einen Anwendungsfall, der sich nicht über die schon im Framework befindlichen Klassen abdecken lässt. Dadurch entfällt das Einarbeiten in die diversen nativen APIs, die gesparte Zeit kann in plattformspezifische Verbesserungen des GUI investiert werden.

Die Dokumentation von Qt für Android lässt – derzeit – an manchen Stellen zu Wünschen übrig. Digias kommerzielles Angebot begegnet diesem Problem durch das Anbieten von inkludiertem Support. Wer erfolgreiche Qt-Applikationen besitzt, sollte sich das Dreimonatsabonnement leisten; die Zusammenarbeit mit Digia kann im Ernstfall Gold wert sein und viel Zeit sparen.

Aufmacherbild: Tablet PC with text „Android“ . ands hold tablet PC with text „Android“ over wooden table von Shutterstock / Urheberrecht: Roobcio

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -