Die Programmiermodelle für Android, iOS und Windows Phone 7 im Vergleich

Qual oder Wahl?
Kommentare

Gegenwärtig kristallisiert sich heraus, dass sich drei wichtige Player auf dem Markt für SmartPhones und Tablets etablieren werden. Damit stellt sich auch die Frage für Unternehmen und Softwareentwickler, auf welches Pferd als technologische Basis sie setzen sollen. Dabei sind die technologischen Grundpfeiler der verschiedenen Plattformen sehr unterschiedlich, sowohl was das Programmiermodell als auch die Werkzeuge zur Erstellung von mobilen Anwendungen betrifft. Dennoch haben diese Anwendungen im Kern viele Konzepte gemein, unabhängig von der zugrunde liegenden Technologie. Oft muss auch dieselbe Applikation parallel für mehrere Plattformen entwickelt werden.

Dieser Artikel stellt auf Basis von kleinen, aber typischen und allgemein gültigen Anwendungsbeispielen die Entwicklung von Anwendungen für diese drei interessanten Plattformen gegenüber. Wer sich für eine Zielplattform entscheiden muss, bekommt durch diesen Artikel weitere Informationen für eine Entscheidungsvorlage. Entwickler, die gleichzeitig für verschiedene Plattformen entwickeln müssen, finden Beispiele für die Abbildung von gebräuchlichen Programmiermustern zwischen den verschiedenen Technologien und Ansätzen. Da für Entwickler Beispiele in Form von Code oft mehr sagen als tausend Worte, enthält der Artikel viele Sourcecode-Ausschnitte. Im Fokus des Artikels steht die Entwicklung von nativen Anwendungen für die jeweilige Plattform – zusätzlich werden sowohl Cross-Plattform-Entwicklungsansätze mittels Interpreter als auch Generatorparadigmen mit eigener DSL (Domain Specific Language) beleuchtet.

Das Beispielszenario

Als Beispiel dient ein kleiner prototypischer News-Reader (NewsExample), der die Einträge eines RSS Feeds aus dem Internet tabellarisch in einer Liste darstellt. Ein Newseintrag kann optional einen Link auf ein Bild (im Element enclosure) enthalten. Die Datenstruktur des XML Feeds ist in Listing 1 zu sehen.

<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns ... ">
  <channel>
    <title>Die Top-Themen des Tages</title>
    <link>http: //portal.1und1.de/de/goto/3885294.html</link>
    <description>... Hintergrundberichte ... </description>
    <copyright>... nicht-kommerziellen Internet-Angeboten erlaubt, …</copyright>
    <pubDate>Sun, 31 Oct 2010 11:35:47 GMT</pubDate>
    <image>
      <title>Logo - 1&1</title>
      <url>http: //portal.1und1.de/sync/rss/logo_1und1.gif</url>
      <link>http: //portal.1und1.de/</link>
    </image>
    <item>
      <title>Schock für "Supertalent"-Jury</title>
      <link>http: //portal.1und1.de/de/goto/11487580.html</link>
      <description>Jury und Publikum ...</description>
      <category>Supertalent, RTL, Bohlen, Darnell, van der Vaart, Casting</category>
      <pubDate>Sun, 31 Oct 2010 11:35:47 GMT</pubDate>
      <guid>http: //portal.1und1.de/de/goto/11487580.html</guid>
    </item>
    <item>
      <title>Haus stürzt ein ...</title>
      <link>http: //www.portal.1und1.de/de/goto/11477942.html</link>
      <description>Tragisches Ende einer … <description>
      <enclosure url="http: //portal.1und1.de/images/11477940.jpg" />
      <category />
      <pubDate>Sun, 31 Oct 2010 11:35:47 GMT</pubDate>
      <guid>http: //www.portal.1und1.de/de/goto/11477942.html</guid>
    </item>
    ...

Das Aussehen der Tabellenzellen passt sich entsprechend dynamisch an, je nachdem, ob ein Bild verfügbar ist. Das gewählte Szenario stellt ein paar der gebräuchlichsten Komponenten bzw. Anwendungsfälle von mobilen Applikationen vor, um sich auf Basis eines einfachen, aber nicht trivialen Beispiels einen ersten Eindruck bezüglich der Unterschiede dieser Plattformen machen zu können. Dieses Beispielszenario enthält folgende technische Aspekte und Anforderungen:

  • Darstellung einer Tabelle bzw. Liste
  • Definition des Layouts einer Tabellenzelle
  • Integration eines RSS Feeds mittels HTTP GET Request
  • Parsen des XML auf dem Feed und Extraktion von Informationen
  • Optimierung der Tabelle durch Wiederverwendung von Zellen (Stichwort: View Holder Pattern)
  • Aktion bei Klick auf eine Tabellenzelle, um den Browser mit einem bestimmten URL zu öffnen
  • Dynamisches Anpassen einer Tabellenzelle gemäß Inhalt (Bild vorhanden)

Selbstverständlich sind weitere Eigenschaften und Anwendungsmöglichkeiten der Plattformen wie Persistenz, Medienwiedergabe, Kamera, aber auch die sehr plattformspezifischen APIs wie der Android-Content-Provider, die Intent-Services, das iOS CoreData oder auch das XNA-Framework in Windows Phone 7 sehr spannend. Das würde jedoch den Rahmen dieses Artikels sprengen. Ein grundlegendes Verständnis für objektorientierte Softwareentwicklung und Build- bzw. Konfigurationsmanagement werden vorausgesetzt. Die Beispiele sollten einfach genug sein, die Konzepte in den jeweiligen Programmiersprachen ersichtlich werden zu lassen. Die Programmiersprache für die Android-Entwicklung ist Java, für iOS ist es Objective-C und für Windows Phone 7 verwendet man C#/.NET. Beispiele und Beschreibungen, wie diese Umgebungen und Werkzeuge jeweils installiert und konfiguriert werden, finden Sie im Internet und spezialisierten Fachzeitschriften zur Genüge.

Aufmacherbild: Diet. Dieting concept. Healthy Food. Beautiful Young Woman choosing between Fruits and Sweets. Weight Loss von Shutterstock / Urheberrecht: Subbotina Anna

[ header = Umsetzung mit Android ]

Umsetzung mit Android

Für die Entwicklung des Android NewsExample werden das aktuelle Android SDK und die dadurch mitgelieferten Werkzeuge (z. B. adb, android, emulator) eingesetzt. Als Entwicklungsumgebung kommt Eclipse Helios mit dem Android-Plug-in (ADT-Plug-in) zum Einsatz. Das Android-Eclipse-Projekt wird entweder mittels ECLIPSE NEW PROJECT-Wizard erzeugt, oder über das android-Skript aus dem SDK-tools-Verzeichnis, was den Vorteil hat, dass damit gleich ein Ant Buildfile (build.xml) mitgeneriert wird, das in einen CI-Prozess eingebunden werden kann: android create project -n NewsExample -t 7 -p ./NewsExampleAndroid -k com.newsexample -a NewsExampleActivity.
Es wird eine komplette Android-Projektstruktur erzeugt (Abb. 1). Das Projekt kann danach einfach in Eclipse importiert und „verwaltet“ werden, aber ebenso über die Kommandozeile mit ant compile gebaut bzw. mit ant install im Android Emulator installiert werden.

Abb. 1: Android-Projektstruktur

Darstellung der Tabelle/Liste und Layout der Tabellenzelle

Basis einer Android App sind Activities, die meist eins zu eins auf Bildschirmmasken abgebildet werden. Diese Activities werden in einem AndroidManifest.xml-Deskriptor angemeldet und konfiguriert, damit sie auf Internetressourcen zugreifen können und auch, damit sie überhaupt vom Android OS gestartet werden können (LAUNCHER), (Listing 2). In Listing 3 ist ein Ausschnitt der Source für die NewsExampleActivity dargestellt.

// AndroidManifest.xml 
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http: //schemas.android.com/apk/res/android"
      package="com.oneandone.newsexample"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".NewsExampleActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
  <uses-permission android:name="android.permission.INTERNET"></uses-permission>
</manifest>
// NewsExampleActivity.java 
public class NewsExampleActivity extends ListActivity {
    static final String URL = "http: //rss.1und1.de/de/feed/themen/index.xml";
    static List<NewsItem> newsItems = new ArrayList<NewsItem>();
    @Override
    public void onCreate(Bundle icicle) {
        newsItems.clear();
        super.onCreate(icicle);
        fetchRssData();
        this.setListAdapter(new MyArrayAdapter(this));
    }
...

Die NewsExampleActivity wird von ListActivity abgeleitet, wodurch sie verschiedene Eigenschaften erbt, die die Darstellung einer Liste/Tabelle vereinfachen. Activities verfügen über einen Lebenszyklus. Die onCreate()-Methode wird im Lebenszyklus beim Erzeugen einer Activity aufgerufen. Im Beispiel wird zunächst die Liste der newsItems gelöscht, anschließend werden die neuen Daten aus dem RSS Feed mittels fetchRSSData() geholt. Aber mehr dazu später – zunächst ist die Methode setListAdapter() interessant. Diese wurde von der ListActivity vererbt und bekommt als Parameter einen ArrayAdapter, der als Bindeglied zwischen der Listendarstellung (der View) und den anzuzeigenden Daten dient und entsprechend diese Daten in eine geeignete Form bringt, damit sie wie gewünscht angezeigt werden. Das Vorgehen entspricht dem MVC-Muster (Model View Controller), das den Entwickler bei der Trennung von Daten, Logik und Darstellung unterstützt.
Als Nächstes werfen wir einen Blick auf den MyArrayAdapter, der als Inner-Class der Activity realisiert ist (Listing 4).

    // NewsExampleActivity.java inner Class
    public class MyArrayAdapter extends ArrayAdapter<String> {
        public MyArrayAdapter(Activity context) {super(context, R.layout.newscell);}
        @Override
        public int getCount() { return newsItems.size();}
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            NewsViewHolder holder;
            if (convertView == null) {
                LayoutInflater inflater = getLayoutInflater();
                convertView = inflater.inflate(R.layout.newscell, null);
                holder = new NewsViewHolder();
                holder.label = (TextView)convertView.findViewById(R.id.label);
                holder.description = 
(TextView)convertView.findViewById(R.id.description);
                holder.image = (ImageView)convertView.findViewById(R.id.image);
                convertView.setTag(holder);
            } else { holder = (NewsViewHolder)convertView.getTag();}

            NewsItem item = newsItems.get(position);
            holder.label.setText(item.title);
            holder.description.setText(item.description);

            if (item.enclosure != null && !"".equals(item.enclosure)) {
                holder.image.setImageDrawable(drawableFromUrl(item.enclosure));
                holder.image.setVisibility(View.VISIBLE);
            } else { holder.image.setVisibility(View.GONE);}
            return convertView;
        }
        class NewsViewHolder {
            TextView label;
            TextView description;
            ImageView image;
        }
        private Drawable drawableFromUrl(String url) {
            Drawable drawable = null;
            try {
                drawable = Drawable.createFromStream(
((InputStream)new URL(url).getContent()), "src name");
            } catch (Exception e) { Log. ...}
            return drawable;
        }
    }

Die zentralen Methoden, die es zu implementieren gilt, sind getCount() und getView(). In der getView()-Methode werden die Tabellenzellen für die jeweiligen Ergebnisdaten (newsItems) erzeugt. Es wird als Parameter ein convertView übergeben. Um die Erzeugung von neuen Objekten bei langen Listen gering zu halten, verfügt Android über einen Caching-Mechanismus. Dabei werden Tabellenzellen, die aus dem Bildbereich scrollen, wiederverwendet, sobald eine neue Zelle in das Sichtfeld kommt. Ein gängiges Pattern ist es dabei, einen ViewHolder zu konfigurieren, der quasi das „Binding“ der Attribute an ein Layout definiert und diesen als Tag an convertView hängt, damit das „Binding“ ebenfalls wiederverwendet werden kann. Hintergrund ist, dass das „Inflaten“ eines Layouts eine eher teure Operation ist und möglichst vermieden werden sollte. Im Sourcecode wird auch ersichtlich, wie je nachdem, ob eine enclosure mit Link auf ein Bild existiert oder nicht, das Layout entsprechend durch Ein- und Ausblenden von ImageView angepasst wird. Das wurde im Beispiel so gelöst, dass für ImageView die Sichtbarkeitseigenschaft auf View.GONE gesetzt wird, was dafür sorgt, dass dieses Element unsichtbar und im UI-Komponentenbaum nicht berücksichtigt wird. Mittels der drawableFromUrl()-Methode werden dann die Bilder nachgeladen, Drawables daraus erzeugt und in das Layout für ImageView eingesetzt.

Bei dem bereits erwähnten Layout handelt es sich um eine Beschreibung des Aussehens einer Tabellenzelle. Der Zugriff auf das Layout erfolgt über die durch das Android-Framework automatisch erzeugte Konstante R.layout.newscell. Im res/layout-Ordner des Projekts können diese Layouts und User Interfaces mittels XML spezifiziert werden. Die im Eclipse im Hintergrund laufenden Android Builder sorgen dafür, dass Layoutelemente mittels einer vergebenen id dann in Java als R-Konstante (R für Resource) adressiert werden können, z. B. LayoutElement des ImageView mit id=“@+id/image mittles Konstante R.id.image. Auf ähnliche Weise werden Textressourcen in strings.xml im res/values-Verzeichnis externalisiert. Das Beispiel der verwendeten newscell sieht man in Listing 5.

// newscell.xml unter res/layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http: //schemas.android.com/apk/res/android"
  android:layout_width="wrap_content" 
android:layout_height="wrap_content"
  android:orientation="vertical">
  <TextView android:layout_width="wrap_content"
    android:layout_height="wrap_content" 
android:id="@+id/label"
    android:textSize="16dp">
  </TextView>
  <LinearLayout android:layout_width="wrap_content"
    android:layout_height="wrap_content" 
android:orientation="horizontal"
    android:padding="2dp">
    <ImageView android:id="@+id/image" 
android:layout_height="40dp"
      android:src="@drawable/icon" 
android:layout_width="40dp"
      android:layout_marginTop="2dp" 
android:layout_marginRight="10dp"
      android:layout_gravity="top" 
android:visibility="gone">
    </ImageView>
    <TextView android:layout_width="fill_parent"
      android:layout_height="wrap_content" 
android:id="@+id/description"
      android:textSize="10dp" 
android:layout_marginTop="2dp">
    </TextView>
  </LinearLayout>
</LinearLayout>

Noch fehlt als weitere Funktion der Tabelle, dass bei einem Touch einer Tabellenzelle der Browser mit einem passenden URL aufgerufen wird. Das ist sehr einfach durch die Implementierung der ebenfalls von ListActivity vererbten Methode onListItemClick() möglich, wie folgender Codeausschnitt illustriert:

    // NewsExampleActivity.java 
    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        NewsItem item = newsItems.get(position);
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(item.link));
        startActivity(intent);
    }

Die Kommunikation mit dem Android-System und mit anderen installierten Applikationen geschieht dabei entkoppelt über Intents, die „getriggert“ werden können. Als einfache Verbildlichung kann man sich ein Intent als eine Art Event vorstellen, für das sich Applikationen registrieren können und entsprechend benachrichtigt werden, wenn ein Intent ausgelöst wurde. In unserem Beispiel wird dem Android-System signalisiert, dass eine Activity gestartet werden soll (ACTION_VIEW), die als Parameter einen URI (URL der anzuzeigenden Webseite) bekommt bzw. diesen anzeigen soll. Bei Android reagiert z. B. der Browser auf dieses ACTION_VIEW Intent mit mitgeliefertem URI.

[ header = Zugriff auf Ressourcen aus dem Internet und Parsen des XML ]

Zugriff auf Ressourcen aus dem Internet und Parsen des XML

Der Zugriff auf Daten aus dem Netz mittels HTTP wird sehr gut und einfach unterstützt. Üblicherweise ist der im Framework mitgelieferte HTTPClient bzw. die DefaultHttpClient-Implementierung für die meisten Anwendungsfälle ausreichend. Man schickt dazu einfach per execute() einen http Get Request an den Ziel-URL und erhält HttpResponse, das das Ergebnis der Anfrage enthält und auf dessen Inhalt/Body über HttpEnity.getContent() zugegriffen werden kann. In unserem Beispiel wird ein InputStream des XML Feeds erzeugt und dieser an den ebenfalls im Android-Framework mitgelieferten SAX Parser übergeben (Listing 6).

    // NewsExampleActivity.java 
    private void fetchRssData() { 
        HttpClient httpClient = new DefaultHttpClient();
        try {
            HttpResponse response = httpClient.execute(new HttpGet(URL));
            HttpEntity entity = response.getEntity();

            SAXParserFactory spf = SAXParserFactory.newInstance();
            SAXParser sp = spf.newSAXParser();
            XMLReader xr = sp.getXMLReader();

            XMLParseHandler xmlParseHandler = new XMLParseHandler();
            xr.setContentHandler(xmlParseHandler);
            xr.parse(new InputSource(entity.getContent()));
        } catch (Exception e) {
            Log. ...
        }
    }

Selbstverständlich ist es ebenso möglich PUT-, POST-, DELETE- und andere Anfragen über den HttpClient abzuschicken und entsprechend den Request zu parametrisieren bzw. die Response zu verarbeiten.

Das vorherige Beispiel der fetchRss()-Methode zeigt auch gleich, wie man den SAX XML Parser konfiguriert und aufruft. In Listing 7 sieht man den XMLParseHandler, der die eigentliche Parsing-Logik enthält und aus dem XML-Strom die relevanten Informationen extrahiert und die statische newsItems-Liste aus der NewsExamplActivity mit NewsItem-Objekten befüllt.

// XMLParseHandler.java
public class XMLParseHandler extends DefaultHandler {
    NewsItem currentItem;
    StringBuilder currentCharacters = new StringBuilder();
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        if (currentItem != null) {
            currentCharacters.append(new String(ch, start, length));
        }
    }
    @Override
    public void endElement(String uri, String localName, String qName) 
throws SAXException {
        if (currentItem != null) {
            if ("title".equals(localName)) {
                currentItem.title = getCleanedContent();
            } else if ("link".equals(localName)) {
                currentItem.link = getCleanedContent();
            } else if ("description".equals(localName)) {
                currentItem.description = getCleanedContent();
            } else if ("category".equals(localName)) {
                currentItem.category = getCleanedContent();
            } else if ("pubDate".equals(localName)) {
                currentItem.pubDate = getCleanedContent();
            } else if ("guid".equals(localName)) {
                currentItem.guid = getCleanedContent();
            } else if ("item".equals(localName)) {
                NewsExampleActivity.newsItems.add(currentItem);
                currentItem = null;
            }
        }
        currentCharacters.delete(0, currentCharacters.length()); // init Buffer
    }
    private String getCleanedContent() {
        return currentCharacters.toString().trim();
    }

    @Override
    public void startElement(String uri, String localName, 
String qName, Attributes attributes) throws SAXException {
        if ("item".equals(localName)) {
            currentItem = new NewsItem();
        } else if ("enclosure".equals(localName)) {
            currentItem.enclosure = attributes.getValue("url");
        }
    }
}
// NewsItem.java
public class NewsItem {
    String title;
    String link;
    String description;
    String enclosure;
    String category;
    String pubDate;
    String guid;
}

Das Prinzip des SAX Parsers ist es, den XML-Strom durch den Handler zu „ schieben“ und bei einem Start-Tag, einem Element Tag oder einem End-Tag die Callbacks im Handler aufzurufen. Mann spricht dabei deshalb auch von einem Push Parser. Da die Textinhalte in einzelne Tokens mit Zeilenumbrüchen und Leerzeichen aufgeteilt werden, müssen diese Texte über den currentCharacters String Buffer im charachters() Callback wieder zusammengebaut werden. Wichtig ist, dass der XMLParseHandler sich merkt, in welchem Kontext er sich gerade befindet. So wird z. B. bei jedem item-Start-Tag ein neues NewsFeed-Objekt erzeugt und bei jedem item-End-Tag dieses Objekt in die Liste für die Tabellendarstellung hinzugefügt (Abb. 2).

Abb. 2: Android NewsExample App im Emulator

Natürlich gibt es durchaus Optimierungsmöglichkeiten für das Beispiel; zudem verfügt das gesamte Android-Framework über verschiedenste wichtige APIs, die hier nicht genannt wurden. Damit sind jedoch alle wichtigen Einzelteile und Komponenten des Beispielszenarios erläutert. Auf dieser Basis lassen sich sicher auch erste Eindrücke für die Programmierung für die Android-Plattform vermitteln.

[ header = Das Gleiche mit iOS ]

Das Gleiche mit iOS

Für die iOS-Version des NewsExample wird das aktuelle iOS SDK 4.1 mit der dazu passenden Entwicklungsumgebung Xcode 3.2.4 eingesetzt. Als Programmiersprache kommt das von Apple getriebene und ursprünglich von NextStep übernommene Objective-C zum Einsatz. Mit wenigen Worten könnte man sie als eine Art Mischung zwischen C und SmallTalk beschreiben. Das Projekt wird mittels dem NEW PROJECT-Wizard mit dem Template Navigation-based Application angelegt. Hierbei wird eine komplette Projektstruktur erzeugt, die unter anderem bereits einen TableViewController mit einem TableView besitzt (Abb. 3).

Abb. 3: iOS-Projektstruktur

Darstellung der Tabelle mit den angepassten Zellen

Der Einstiegspunkt dieser iOS-Applikation ist die vom Wizard angelegte Ressource MainWindow.xib, die mit dem Interface Builder geöffnet werden kann und das Layout für die Applikation bereitstellt. Innerhalb der Ressource ist dann ein Verweis auf die Klasse RootViewController festgelegt, die den dazugehörigen Quellcode beinhaltet. Die Applikation ist jetzt theoretisch schon lauffähig, würde allerdings nur eine leere Tabelle anzeigen.

Die vom Wizard generierte Klasse RootViewController ist von UITableViewController abgeleitet, was den Zugriff auf die UI-Elemente wesentlich vereinfacht. Dadurch sind auch schon viele der benötigten Methoden (z. B. numberOfRowsInSection, cellForRowAtIndexPath) zur Steuerung der Tabelle bereits implementiert und müssen nur noch entsprechend ergänzt werden. Da die Tabelle aber nicht nur eine Zeile Text enthalten soll, sondern Überschrift, Beschreibung und eventuell ein Bild, muss dazu eine angepasste UITableViewCell (NewsCell) angelegt werden. Diese besteht aus zwei Teilen: einmal die Klasse mit dem dazugehörigen Interface und einmal die eigentliche Interface-Builder-Ressource in Form einer xib-Datei. Beide können über den Wizard ADD | NEW FILE dem Projekt hinzugefügt werden. Wird diese Aktion direkt auf dem passenden Unterordner (Classes, Resources) ausgeführt, dann landen die generierten Dateien auch schon automatisch in diesem. Das Template für die Klasse ist eine iOS Cocoa Touch Class des Typs Objective-C Class mit der Subclass UITableViewCell. Für die Ressource ist das Template ein iOS User Interface des Typs Empty XIB.

Nun „sprechen“ aber beide Teile noch nicht miteinander, da noch kein Bezug von der Klasse zur Ressource besteht. Um das zu erreichen, wird zunächst das Interface um die später anzuzeigenden Variablen ergänzt. Diese werden als IBOutlet deklariert, was bedeutet, dass diese Variablen auf ein anderes Objekt referenzieren können und auch direkt vom Interface Builder erkannt werden. In der Zelle sollen die Werte Titel, Beschreibung und Bild angezeigt werden. Die verwendeten Datentypen sind keine primitiven Typen, sondern die Objekte, auf die dann später referenziert wird. Mehr dazu später. Für einen bequemen Zugriff werden diese zudem als Properties deklariert:

// NewsCell.h
@interface NewsCell : UITableViewCell {
  IBOutlet UILabel *title;
  IBOutlet UILabel *description;
  IBOutlet UIImageView *enclosure;
}
@property (nonatomic, retain) UILabel *title;
@property (nonatomic, retain) UILabel *description;
@property (nonatomic, retain) UIImageView *enclosure;
@end

Nun geht es mit der Ressource im Interface Builder weiter. Die leere Datei wird um eine Table View Cell aus der Library (Abschnitt Cocoa Touch – Data Views) ergänzt, indem das Objekt einfach mit der Maus in die XIB-Datei gezogen wird. Jetzt muss noch die Referenz auf die zuvor erstellte Klasse festgelegt werden. Dazu wird einfach die Class Identity der neu hinzugefügten Table View Cell vom Standard UITableViewCell auf die zuvor erstellte Klasse gesetzt (Abb. 4).

Abb. 4: Ersetzen der Class Identity von „UITableViewCell“

Jetzt zeigt der Interface Builder auch die zuvor im Code angelegten Outlets in der Connections-Übersicht an und diese können mit Elementen auf der Content-View verbunden werden (Abb. 5).

Abb. 5: Anzeige der Outlets

Dazu müssen diese Objekte aber zuerst auf der Content-View angelegt werden, was sich wieder durch einfaches Drag and Drop aus der Library bewerkstelligen lässt. Benutzt werden jeweils UILabel für den Titel und die Beschreibung und UIImageView für das mögliche Bild. Die Positionierung erfolgt hier pixelgenau. An dieser Stelle wird auch schon ein kleiner Nachteil der Layoutgestaltung unter iOS deutlich: Es gibt keine Möglichkeit, fließende Layouts zu erstellen. Das bedeutet, dass die Steuerung, ob ein Bild später angezeigt wird oder auch nicht und das dazugehörige Verschieben von UILabel komplett im Code erfolgen muss. Eine praktikable Lösung hierfür wäre es, die komplette Zelle im Code zu erzeugen und gar nicht erst im Interface Builder zu beschreiben. Um die Komplexität dieses Artikels nicht unnötig zu erhöhen, und um den Überblick einfacher zu machen, wurde die Lösung mit dem Interface Builder bevorzugt. Sobald die Objekte jetzt auf der Content-View platziert sind, können sie mit den Outlets verbunden werden. Dazu kann man in gewohnter Drag-and-Drop-Manier über den kleinen Kreis neben der Connection eine Verbindung zum jeweiligen UI-Objekt herstellen. Um später ein Reuse des Zellenlayouts zu ermöglichen, muss zum Abschluss noch ein eindeutiger Identifier in den Attributen der NewsCell festgelegt werden.

[ header = Zugriff auf den RSS Feed und Parsen des XML ]

Zugriff auf den RSS Feed und Parsen des XML

Für den Zugriff auf Ressourcen aus dem Internet mittels HTTP bzw. HTTPS gibt es im Cocoa-Touch-Framework schon eine fertige und komfortable Implementierung in den Klassen NSMutableURLRequest und NSURLConnection. Für den Aufruf gibt es sowohl synchrone als auch asynchrone Methoden. Der Aufruf gibt in beiden Fällen im Erfolgsfall den Inhalt der Anfrage als NSData-Objekt (vergleichbar mit einem Byte Array) zurück. Dieses Objekt, das dann das XML unseres RSS Feeds beinhaltet, wird zur weiteren Verarbeitung an den auch im Framework enthaltenen NSXMLParser weitergegeben. Dieser ist, wie auch sein Android-„Kollege“, ein Push Parser und ruft bei jedem Start- oder End-Tag die Callbacks seines Delegate auf.

Um den Code übersichtlicher zu gestalten und den Zugriff auf die externen Daten zu kapseln, wird in diesem Beispiel ein DataController angelegt, der sowohl den Zugriff auf die Daten als auch das Parsen des XML und den Zugriff auf die verarbeiteten Daten ermöglicht. Dazu wird dem Projekt eine weitere Klasse über den NEW FILE-Wizard hinzugefügt. Template für die Klasse ist eine iOS Cocoa Touch Class des Typs Objective-C Class mit der Subclass NSObject. Um später in dieser Klasse auch das Parsen des XML zu übernehmen, wird gleich im Interface die Deklaration um das Protokoll NSXMLParserDelegate ergänzt. Als Datenspeicher zum Zugriff auf die geparsten Daten wird ein einfaches NSMutableArray benutzt. Die Elemente davon werden in Form von Key/Value-Paaren als NSMutableDictionary gehalten. Der Zugriff auf die Daten erfolgt über die Methoden countOfList und objectInListAtIndex, die die Anzahl der Elemente bzw. ein bestimmtes Element zurückgeben:

// NewsDataController.h
@interface NewsDataController : NSObject <NSXMLParserDelegate> {
  NSMutableArray *list;
 }
- (unsigned)countOfList; 
- (id)objectInListAtIndex:(unsigned)theIndex;
@end

Die Implementierung beinhaltet zum einen Teil den Zugriff auf den externen RSS Feed und zum anderen Teil das Parsen des XML über das implementierte Protokoll. Durch das Fehlen erweiterter Refakturierungsfunktionen müssen die Methoden für das Protokoll im Editor leider manuell erstellt werden.

Das Abrufen der Daten soll direkt bei der Initialisierung der Klasse erfolgen. Dazu wird zuerst der NSMutableURLRequest erzeugt und mit dem URL des RSS Feeds konfiguriert. Im nächsten Schritt wird der Request ausgeführt und abschließend werden die Daten an den NSXMLParser übergeben. Das Beispiel ignoriert bewusst den Fehlerfall, weshalb Fehlerbehandlungsroutinen auch nicht implementiert wurden. Zudem gibt es auch keine Überprüfung auf eine korrekte Antwort vom Server des RSS-Feeds. In einer produktiven Anwendung sollten diese auf keinen Fall fehlen, zur Demonstration der Technik in diesem Beispiel kann darauf verzichtet werden (Listing 8).

// NewsDataController.m
- (id)init 
{
  if (self = [super init]) 
  {
    list = [[NSMutableArray alloc] init];
    NSMutableURLRequest *request = [[[NSMutableURLRequest alloc] init] autorelease];  
    [request setURL:[NSURL URLWithString:@"http ://rss.1und1.de/de/feed/themen/index.xml"]];  
    
    NSError *error = NULL;
    NSHTTPURLResponse *response = NULL;
    NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];  
    
    if (data)   
    {  
      NSXMLParser *parser = [[[NSXMLParser alloc] initWithData:data] autorelease];
      [parser setDelegate:self];
      [parser parse];
    }   
  }
  return self;
  
}

Die Methoden für den NSXMLParser initialisieren jeweils beim Start eines <item>-Elements ein lokales NSMutableDictionary und befüllen mit den darauf folgenden End-Tags dieses mit den entsprechenden Werten. Einen Sonderfall bildet das <enclosure>-Element, da hier die abzurufende Information als Attribut des Starttags und nicht als Inhalt definiert ist. Sobald der End-Tag des <item>-Elements erreicht wird, wird das aktuelle Dictionary der lokalen Liste (NSMutableArray) hinzugefügt (Listing 9).

// NewsDataController.m
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict {
    if (!_xmlChars) {
        _xmlChars = [[NSMutableString string] retain];
    }
    [_xmlChars setString:@""];
  if ([elementName isEqualToString:@"item"]) {
    _newItem = [[NSMutableDictionary alloc] initWithCapacity:3];
    [_newItem setObject:@"" forKey:@"title"];
    [_newItem setObject:@"" forKey:@"description"];
    [_newItem setObject:@"" forKey:@"link"];
    [_newItem setObject:@"" forKey:@"enclosure"];
  }
  if ([elementName isEqualToString:@"enclosure"]) {
    NSString *urlString = [attributeDict valueForKey:@"url"];
    if (urlString) {
      [_xmlChars setString:urlString];
    }
  }
}

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
  if ([elementName isEqualToString:@"title"]) {
    [_newItem setObject:[_xmlChars copy] forKey:elementName];
  }
  else if ([elementName isEqualToString:@"description"]) {
    [_newItem setObject:[_xmlChars copy] forKey:elementName];
  }
  else if ([elementName isEqualToString:@"link"]) {
    [_newItem setObject:[_xmlChars copy] forKey:elementName];
  }
  else if ([elementName isEqualToString:@"enclosure"]) {
    [_newItem setObject:[_xmlChars copy] forKey:elementName];
  }
  else if ([elementName isEqualToString:@"item"]) {
    [list addObject:_newItem];
  }
}

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
    [_xmlChars appendString:string];
}

Darstellung der Daten in der Applikation

Zum Abschluss muss noch der zuvor erstellte NewsDataController in den RootViewController implementiert werden. Der NewsDataController kümmert sich hierbei nur um die Beschaffung der Daten und nicht um das Konfigurieren der angepassten Zelle innerhalb der Tabelle. Dazu wird zuerst versucht, aus TableView eine nicht mehr benötigte, aber bereits instanziierte Zelle mit dem Identifier NewsCellIdentifier abzurufen. Dieser wurde weiter oben im Artikel für die angepasste Zelle im Interface Builder hinterlegt. Schlägt dieser Versuch fehl, wird eine neue instanziiert. Dadurch ist ein Reusing der Zellen gewährleistet. Danach werden der entsprechende Titel und die Beschreibung gesetzt. Falls ein Bild vorhanden ist, wird es dynamisch als UIImage nachgeladen. Als sehr praktisch erweist sich hierbei das Initialisieren eines NSData-Objekts mit dem Inhalt eines angegebenen URL. Ist kein Bild vorhanden, muss das Layout der Zelle, wie bereits weiter oben erwähnt, im Code angepasst werden. In diesem Beispiel wird einfach das Label mit der Beschreibung um ein paar Pixel nach links verschoben (Listing 10).

// RootViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  static NSString *CellIdentifier = @"NewsCellIdentifier";
    NewsCell *cell = (NewsCell*)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
    cell = [[[NSBundle mainBundle] loadNibNamed:@"NewsCell" owner:self options:nil] objectAtIndex:0];
    } 
  NSMutableDictionary *currentItem = [newsDataController objectInListAtIndex:indexPath.row];
  cell.title.text = [currentItem valueForKey:@"title"];
  cell.description.numberOfLines = 0;
  cell.description.text = [currentItem valueForKey:@"description"];
  NSString *enclosure = [currentItem valueForKey:@"enclosure"];
  if (![enclosure isEqualToString:@""]) {
    cell.enclosure.image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:enclosure]]];
  } else {
    cell.enclosure.hidden = YES;
    [cell.description setFrame:CGRectMake(14.0, 17.0, cell.description.bounds.size.width, cell.description.bounds.size.height)];
  }
    return cell;
}

[ header = Öffnen des Browsers mit dem entsprechenden URL ]

Öffnen des Browsers mit dem entsprechenden URL

Um den Browser mit dem gewünschten URL zu öffnen, kann auf die didSelectRowAtIndexPath-Methode der UITableView zurückgegriffen werden. Dort wird dann im Code die entsprechende Zelle ausgewertet und der dem Eintrag hinterlegte URL mittels der Methode openURL des UIApplication-Objekts geöffnet (Abb. 6).

Abb. 6: iOS NewsExample App im Emulator

Natürlich gibt es auch für dieses Beispiel durchaus Optimierungsmöglichkeiten. Alle wichtigen Einzelteile und Komponenten des Beispielszenarios sind damit aber erläutert und lassen einen ersten Eindruck für die Entwicklung unter der iOS-Plattform zu.

Wie geht das mit Windows Phone 7?

Die WP7-Version des NewsExample setzt die aktuell erhältliche Version der Windows Phone 7 Developer Tools, bestehend unter anderem aus Visual Studio 2010 Express, Expression Blend 4 for Windows Phone und dem Windows Phone Emulator ein. Das Projekt wird mittels dem NEW PROJECT-Wizard mit der Windows-Phone-Application-Vorlage aus der Silverlight-for-Windows-Phone-Rubrik erstellt. Auch hier wird wieder eine komplette und lauffähige Projektstruktur erzeugt, die unter anderem bereits einen Einstiegspunkt, die Page MainPage.xaml besitzt. Diese ist als Task definiert, der eine vordefinierte Aktion, in etwa vergleichbar mit den Intents aus dem Android-Framework, darstellt. In der Datei WMAppManifest.xml ist dieser Task als Standard definiert und somit öffnet sich die Page nach dem Starten der Applikation (Abb. 7).

Abb. 7: Windows-Phone-7-Projektstruktur

Eine Windows-Phone-7-Applikation kann entweder auf Silverlight oder auf XNA Game Studio basieren. Letzteres ist eher für den Gebrauch in Spielen gedacht und wird in diesem Artikel auch nicht weiter beleuchtet. Eine Mischung zwischen beiden Ausprägungen ist nicht möglich, man muss sich entweder für das eine oder für das andere entscheiden. Ein Zugriff auf Teile des APIs des jeweils anderen Frameworks ist aber durchaus möglich.

Die Basis für Silverlight Apps auf Windows Phone bilden Pages, die in der Beschreibungssprache XAML (Extensible Application Markup Language) deklariert werden und die Oberfläche der Bildschirmmasken beschreiben. Die Programmiersprache C# kommt in der zur jeweiligen XAML-Datei zugehörigen Code-Behind-Datei zum Einsatz. Hier findet die Verknüpfung zu den Objekten im Markup statt. Sowohl der Code als auch das Markup können innerhalb von Visual Studio bearbeitet werden. Das Markup kann direkt in XAML oder auch per WYSIWYG-Editor erstellt und bearbeitet werden. Für komplexere Oberflächen (z. B. mit deklarierten Animationen etc.) ist der in Visual Studio integrierte Editor nur bedingt geeignet. Für diesen Fall kommt das in den Developer-Tools mitgelieferte Expression Blend zum Einsatz. Für die visuelle Gestaltung der Zellen innerhalb der Tabelle wird es später in diesem Artikel noch verwendet werden, aber ansonsten nicht mehr großartig erwähnt, da eine Erklärung der kompletten Funktionen den Rahmen dieses Artikels sprengen würde.

Darstellung der Tabelle mit den angepassten Zellen

Durch die gewählte Vorlage ist der Einstiegspunkt der Applikation nur eine leere Seite mit einem Titel. Um darin eine Tabelle mit Einträgen anzuzeigen, zieht man in Drag-and-Drop-Manier aus der Toolbox am linken Rand eine ListBox auf das leere ContentPanel im Markup der MainPage. Das funktioniert sowohl in der WYSIWYG als auch in der XML-Ansicht. Damit diese Tabelle dann nicht nur jeweils eine Zeile Text pro Zelle anzeigen kann, muss hier, wie bei Android und iOS auch, das Aussehen der Zelle angepasst werden. Das ist in Silverlight sehr komfortabel durch DataTemplates realisierbar. An dieser Stelle kommt, wie weiter oben erwähnt, Expression Blend zum Einsatz, da man innerhalb von Visual Studio diese Templates nicht visuell anlegen oder editieren kann. Durch einen Klick auf OPEN IN EXPRESSION BLEND… im Kontextmenü des Projekts öffnet sich der mächtige grafische Editor. Dort kann nach Auswahl der hinzugefügten ListBox über das Menü OBJECT | EDIT ADDITIONAL TEMPLATES | EDIT GENERATED ITEMS (ITEMTEMPLATE) | CREATE EMPTY ein neues DataTemplate erzeugt und natürlich auch grafisch angepasst werden. Auch diese Vorlage soll wieder jeweils einen TextBlock für Titel und Beschreibung und ein Image für das möglicherweise vorhandene Bild beinhalten. Die Unterstützung von fließenden Layouts und die Möglichkeit, direkt die Eigenschaften der UI-Elemente mit Objekten zu verbinden, macht die Lösung des Problems im Fall eines nicht vorhandenen Bildes relativ einfach. Dazu wird einfach die Visibility-Eigenschaft des Image-Elements mit einem weiteren Binding versehen, das später die Sichtbarkeit über einen Wert im Datenobjekt steuert. Das erzeugte Template kann alternativ auch ohne einen grafischen Editor direkt im Markup angelegt werden. Zur Beschreibung sind nur wenige Zeilen XAML notwendig (Listing 11).

// MainPage.xaml (DataTemplate)
<phone:PhoneApplicationPage.Resources>
  <DataTemplate x:Key="NewsItemDataTemplate">
    <Grid>
      <StackPanel Orientation="Vertical">
        <TextBlock FontSize="24" FontWeight="Bold" Text="{Binding Title}" />
        <StackPanel Orientation="Horizontal" VerticalAlignment="Top">
          <Image Source="{Binding Enclosure}" Width="50" Height="50" Stretch="UniformToFill" Margin="0,0,5,0" Visibility="{Binding HasEnclosure}" />
          <TextBlock FontSize="16" TextWrapping="Wrap" Foreground="LightGray" Width="430" Text="{Binding Description}"/>
        </StackPanel>    
      </StackPanel>
    </Grid>
  </DataTemplate>
</phone:PhoneApplicationPage.Resources>

Die Bindings innerhalb des Templates sind nicht stark typisiert und müssen später bei der Verknüpfung in Form von Public Properties zur Verfügung gestellt werden. Das ist eine potenzielle Fehlerquelle, kann aber z. B. durch die Verwendung von Beispieldatenobjekten innerhalb der Editoren vermieden werden. Dieses Template wird der in der MainPage angelegten ListBox dann noch als ItemTemplate gesetzt, damit diese die Zellen korrekt anzeigen kann.

[ header = Zugriff auf den RSS Feed, Parsen des XML und Darstellung der Daten ]

Zugriff auf den RSS Feed, Parsen des XML und Darstellung der Daten

Auch unter Windows Phone 7 gibt es bereits fertige und komfortable Implementierungen für den Zugriff auf Ressourcen aus dem Internet mittels HTTP bzw. HTTPS. Im Beispiel wird der RSS Feed direkt nach dem Laden der Page abgeholt und verarbeitet. Das ist zwar nicht ideal, aber für dieses Beispiel völlig ausreichend:

// MainPage.xaml.cs
private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
{
    WebClient news = new WebClient();
    news.DownloadStringCompleted += new DownloadStringCompletedEventHandler(news_DownloadStringCompleted);
    news.DownloadStringAsync(new Uri("http: //rss.1und1.de/de/feed/themen/index.xml"));
}

Beim Parsen des XML aus dem RSS Feed hebt sich Windows Phone 7 sehr stark von seinen Kollegen Android und iOS ab, da hier die mächtige LINQ-(Language-Integrated-Query-)Komponente des .NET Frameworks zum Einsatz kommt. Die Abfrage erfolgt dabei ähnlich wie bei SQL, nur in diesem Fall direkt auf Elemente im XML, und das Ergebnis wird wiederum in neue Objekte geschrieben. Die Besonderheit hierbei ist aber, dass die Abfragesprache nicht in einem String, sondern direkt im Code geschrieben ist. Dadurch ist schon eine Überprüfung zur Entwicklungszeit möglich. Die verwendete Ergebnisklasse dient als Datenobjekt zum zuvor erstellten Template. Durch die Festlegung der ItemTemplate-Eigenschaft in der ListBox und des entsprechenden Bindings im Template wird die Verknüpfung dann automatisch durch das Framework erledigt. Die Klasse kann einfach über den ADD CLASS-Wizard hinzugefügt und um die benötigten Public Properties ergänzt werden. Das Nachladen eines Bilds und die Steuerung, ob es vorhanden ist, ist auch hier als Eigenschaft implementiert (Listing 12).

// NewsItem.cs
public class NewsItem
{
    public string Title { get; set; }
    public string Link { get; set; }
    public string Description { get; set; }
    public string EnclosureURL { get; set; }
    public Visibility HasEnclosure
    {
        get
        {
            return !EnclosureURL.Equals("") ? Visibility.Visible : Visibility.Collapsed;
        }
    }
    public BitmapImage Enclosure
    {
        get
        {
            BitmapImage encImage = null;
            if (HasEnclosure == Visibility.Visible)
            {
                encImage = new BitmapImage(new Uri(EnclosureURL));
            }
            return encImage;
        }
    }
} 

Das eigentliche Parsen des XML und das Befüllen der Ergebnisobjekte wird direkt im asynchronen Ereignis-Handler des Webclients erledigt. Dort wird – dank LINQ mit nur wenigen Zeilen Code – das XML ausgewertet, in die Ergebnisobjekte geschrieben und diese gleich an die ListBox im Markup weitergegeben. Um LINQ aber auch verwenden zu können, muss es zuvor noch über den ADD REFERENCE-Wizard dem Projekt hinzugefügt werden (Abb. 8).

Abb. 8: Hinzufügen der Verweise für LINQ

Der folgende Code zeigt den DownloadStringCompletedEventHandler des Webclients. Er bildet fast die komplette Businesslogik der Applikation (Listing 13).

// MainPage.xaml.cs
private void news_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
    if (e.Error != null)
    {
        return;
    }
    XElement xmlNews = XElement.Parse(e.Result);
    listBox1.ItemsSource = from newsItem in xmlNews.DescendantsAndSelf("item")
                            select new NewsItem
                            {
                                Title = newsItem.Element("title").Value,
                                Description = newsItem.Element("description").Value,
                                Link = newsItem.Element("link").Value,
                                EnclosureURL = newsItem.Element("enclosure") != null ? newsItem.Element("enclosure").Attribute("url").Value : ""
                            };
} 

Da die Inhaltsobjekte der ListBox gleich zu Anfang komplett befüllt werden, findet das dynamische Nachladen eines möglichen Bilds innerhalb einer Public Property in der NewsItem-Klasse statt. Das Reusing der angezeigten Spalten wird komplett vom Framework übernommen.

[ header = Öffnen des Browsers mit dem entsprechenden URL ]

Öffnen des Browsers mit dem entsprechenden URL

Auch in diesem Beispiel soll sich der Browser nach dem Antippen der entsprechenden Zelle öffnen und den hinterlegten URL aufrufen. Dazu wird im SelectionChanged-Ereignis der ListBox die ausgewählte Zelle über die Eigenschaft SelectedItem ausgelesen und als NewsItem ausgewertet. Die Anzeige des URLs erfolgt über die WebBrowserTask-Klasse, die die aktuelle Applikation beendet und den Browser öffnet. Nach einem Druck auf die BACK-Taste am Gerät wird die NewsExample-Applikation wieder gestartet (Abb. 9).

Abb. 9: WP7 NewsExample App im Emulator

Auch bei diesem Beispiel ist viel Raum für Optimierungen vorhanden. Eine ausführliche Fehlerbehandlung wurde aus Gründen der Übersichtlichkeit komplett weggelassen. Trotzdem sind alle wichtigen Komponenten einer Windows-Phone-7-Applikation damit erklärt und lassen einen ersten Eindruck auf diese recht neue Plattform zu.

[ header = Ein Interpreter-Ansatz mit Titanium Mobile ]

Ein Interpreter-Ansatz mit Titanium Mobile

Neben der Entwicklung von nativen Apps in der direkten Host-Sprache etabliert sich ein weiterer Ansatz, der auf der Nutzung von interpretierten Sprachen beruht. Ein prominenter Kandidat eines Frameworks für diesen Ansatz ist Titanium Mobile von Appcelerator. Titanium liefert ein JavaScript-API und ein SDK, um native mobile Applikationen in JavaScript zu schreiben. Dieser Ansatz dürfte vor allem bei Webentwicklern großen Anklang finden, da die Lernkurve entsprechend flach ausfällt. Titanium basiert darauf, dass die mobilen Geräte eine JavaScript Runtime bereitstellen, die diese Applikationsskripte interpretiert. Das JavaScript-API ist jedoch quasi durch nativen Code „gespiegelt“ und Titanium Mobile stellt in der Host-Sprache entwickelte Module zur Verfügung, die dann tatsächlich zur Laufzeit aufgerufen werden und native Elemente erzeugen. Aus diesem Grund wirken diese Anwendungen vom Look and Feel auch nativ. Mit diesem Ansatz ist es möglich, aus dem JavaScript Sourcecode gleichzeitig sowohl Apps für iPhone und Android mit keinen oder nur geringen Anpassungen zu bauen. Aktuell hinkt das API für Android noch etwas dem für iOS hinterher und für das iPhone funktioniert das Framework einfach am besten. Jedoch ist Ende des Jahres 2010 eine stark verbesserte Android-Unterstützung mit dem Titanium 1.5 Release bereitgestellt worden, sodass beide APIs gleich aufziehen. Eine BlackBerry-Unterstützung ist ebenfalls vorhanden, aber noch nicht sehr ausgereift. Listing 14 zeigt den kompletten JavaScript-Code mit dem Titanium-Framework, der notwendig ist, um dasselbe Beispielszenario zu verwirklichen (Abb. 10). 

// newsexample.js
var tableview = Titanium.UI.createTableView({});
Titanium.UI.currentWindow.add(tableview);
var xhr = Ti.Network.createHTTPClient();
var url = "http: //rss.1und1.de/de/feed/themen/index.xml";

xhr.open("GET",url);
xhr.onload = function() {
    var doc = this.responseXML.documentElement;
    var items = doc.getElementsByTagName("item");
    for (var c=0;c<items.length;c++) {
        var item = items.item(c);
var title = item.getElementsByTagName("title").item(0).text;
        var description = item.getElementsByTagName("description").item(0).text;
        var link = item.getElementsByTagName("link").item(0).text;
        var media = null;
        var thumbnails = item.getElementsByTagName("enclosure");
if (thumbnails && thumbnails.length > 0) {
            media = thumbnails.item(0).getAttribute("url");
        
            var row = Ti.UI.createTableViewRow({
                height:100,
                className: 'rowWithImage'
            });     
            var cellLabel = Titanium.UI.createLabel({
                                                text:title,
                                                height:18,
                                                width: 'auto',
                                                color:'black',
                                                font:{fontSize:16},
                                                left:5,
                                                top:2
                                            });
            var cellDescription = Titanium.UI.createLabel({
                                                text:description,
                                                height:'auto',
                                                width:'auto',
                                                color:'black',
                                                font:{fontSize:12},
                                                left:70,
                                                top:20
                                            });

            var imgView = null;
            if (Titanium.Platform.name == 'android') {
                imgView = Ti.UI.createImageView({
                                        url: media,
                                        top:20,
                                        left:10,
                                        width:50,
                                        height:50,
                                        canScale:true
                                    });
            } else {
                imgView = Ti.UI.createImageView({
                                        image: media,
                                        top:20,
                                        left:10,
                                        width:50,
                                        height:50
                                    });
            }
            row.add(cellLabel);
            row.add(cellDescription);
            row.add(imgView);
            row.addEventListener('click',function(e) {
                    Titanium.Platform.openURL(link);
                });
            tableview.appendRow(row);
        } else {
            var row = Ti.UI.createTableViewRow({
                height:100,
                className: 'rowNoImage'
            });
var cellLabel = Titanium.UI.createLabel({
                                                text:title,
                                                height:18,
                                                width: 'auto',
                                                color:'black',
                                                font:{fontSize:16},
                                                left:5,
                                                top:2
                                            });
            var cellDescription = Titanium.UI.createLabel({
                                                text:description,
                                                height:'auto',
                                                width:'auto',
                                                color:'black',
                                                font:{fontSize:12},
                                                left:10,
                                                top:20
                                            });
            row.add(cellLabel);
            row.add(cellDescription);
            row.addEventListener('click',function(e) {
                    Titanium.Platform.openURL(link);
                });    
            tableview.appendRow(row);
        }    
    }
};
xhr.send({"a":"€漢字"});

Abb. 10: Vergleich der selben Titanium App im iPhone und Android Emulator

Das Beispiel ist sehr flach und nicht sehr „komponentenorientiert“ aufgebaut. Es wird zunächst eine TableView erzeugt und dem aktuell sichtbaren Fenster hinzugefügt. Danach wird der HttpClient erzeugt und ein GET Request ausgelöst. Das Titanium-Framework und die APIs sind weitestgehend asynchron aufgebaut. Auch in diesem Beispiel bekommt der HttpClient per Callback die Antwort, also den News-Feed, der die xhr.onLoad()-Methode zurückliefert. Im nächsten Schritt wird aus dem XML ein DOM aufgebaut. Über diesen DOM wird iteriert und die entsprechenden Informationen extrahiert (z. B. var title = item.getElementsByTagName(„title“).item(0).text;). Sobald die Daten für eine Tabellenzelle komplett sind, wird diese Zelle der TableView mit appendRow() hinzugefügt. Im Code wird zwischen zwei Typen von Tabellenzellen unterschieden. Einmal mit Bild und einmal ohne Bild. Für die Erzeugung der ImageView gibt es mittels Titanium.Platform.name == ‚android‘ nochmals die Unterscheidung zwischen iOS und Android, da an dieser Stelle das Titanium API nicht ganz konsistent ist und das Bild-Link-Attribute für Android url und ansonsten image heißt. Die Reaktion für das Klicken auf eine Tabellenzelle übernimmt ein Event-Listener, der auf der row per row.addEventListener() registriert wird und dann einen URL öffnet.

Sollten bestimmte Funktionalitäten und Fähigkeiten der Host-Plattform im Titanium-API fehlen, steht ein Module SDK zur Verfügung, um das Framework zu erweitern. Hierbei können native Anteile selbst entwickelt und per JavaScript-API analog zum Titanium-JavaScript-API bereitgestellt werden. Das gilt sowohl für iOS als auch für Android. Eine Windows-Phone-7-Unterstützung ist aktuell noch nicht absehbar.

Bemerkung: Leider hat der HttpClient von Titanium noch ein Encoding-Problem, weshalb bei der Android-Version nicht verlässlich Umlaute erscheinen. Das ist jedoch ein bekannter Bug, der hoffentlich im nächsten Release behoben sein wird. Ebenso ist erwähnenswert, dass es keine Debugger-Unterstützung bei Titanium Mobile gibt und man ausschließlich mit Logausgaben auskommen muss. Solange das API und das Framework noch unbekannt sind, ist die Fehleranfälligkeit sehr hoch und das Finden eines Bugs extrem zeitaufwendig. 

[ header = Multiplattformfähigkeit auf Basis von Generatoren und Domain Specific Languages + Fazit ]

Multiplattformfähigkeit auf Basis von Generatoren und Domain Specific Languages

Eine weitere Möglichkeit um multiplattformfähige Software zu entwickeln oder aber auch eine höhere Automatisierung bei der Entwicklung von mobilen Apps zu erreichen, liegt darin, generative Ansätze zu verwenden. Im weiteren Verlauf des Artikels wird dieser Lösungsansatz nur kurz skizziert. Hierbei können z. B. die Modeling-Werkzeuge aus dem Eclipse-Projekt gute Dienste leisten (EMF, Xtext, Xpand, Xtend etc.). Ein vielversprechendes Vorgehen ist es, eine eigene domänenspezifische Sprache zu entwickeln (oder zu verwenden), die bestimmte Konzepte der mobilen Anwendungen sehr treffend spezifiziert und dabei möglichst technische oder plattformspezifische Details ausblendet. Für jede gewünschte Zielplattform werden dann Generatoren bzw. die Transformations-Templates entwickelt, die nativen Code erzeugen. Konzepte leben länger als Technologien – die Wahl eines geeigneten Abstraktionsgrades für die Modellierung sorgt dafür, dass die DSL-Modelle gut wiederverwendet werden können. Für eine typische Anwendung, die XML-Daten in einer Tabelle anzeigt, genügen recht wenige Informationen für die Spezifikation. Ein Beispiel für solch eine plattformunabhängige DSL (bzw. ein textuelles Modell, das der DSL-Grammatik genügt) ist in Listing 15 skizziert.

// newsExample.mdysdl
model NewsItem {
    title : String
    link : String
    description : String
    enclosure fromAttribute "enclosure@url" : String 
    category : String
    pubDate : String
  guid : String
}
data NewsFeedProvider : NewsItem[] {
fetchXML "rss/channel/item" from "http: //rss.1und1.de/de/feed/themen/index.xml"
} 
ListView NewsExampleList "1&1 News ..." loop NewsItem[] as item {  
  simpleRow NewsCell {
      title= item.title
      description= item.description
      image= item.enclosure
      action= openURL(item.link) 
  } 
}  

Dieses Vorgehen mittels DSLs und eigenen Generatoren eignet sich besonders gut bei Produktlinien oder wenn bestimmte Typen von Anwendungen generiert werden sollen. In diesem Beispiel wäre es sicherlich auch denkbar, verschiedene Tabellenzellentypen mit verschiedenen Layouts (hier exemplarisch simpleCell) anzubieten. Die Logik für das Ein- und Ausblenden von Bildern wäre für dieses Beispiel im generierten Code versteckt. Die Schablonen für die resultierenden iPhone, Android oder Windows Phone 7 Apps lassen sich dann entsprechend aus Referenzimplementierungen für die Zielplattformen ableiten und die dynamischen Anteile durch Informationen aus dem textuellen Modell (newsexample.mydsl) ersetzen. Die Eclipse-MDSD-Werkzeugketten stellen hierfür Parser und Editoren zur Verfügung und sorgen dafür, dass der Modellbaum für die Generator-Templates (z. B. Xpand) als EMF-Modell traversier- und auswertbar zur Verfügung steht.

Was ist das Fazit?

Um nicht „Äpfel mit Birnen zu vergleichen“ ist ganz klar, dass die Gegenüberstellungen auf Basis des Beispielszenarios keine absolute Aussage zulassen, welches Framework und welcher Ansatz der beste ist. Allerdings sind erste Eindrücke ableitbar, die vor allem für einen Einstieg in die eine oder andere Technologie relevant sein könnten. Möglicherweise konnte die Gegenüberstellung des Beispielszenarios auch einige Hürden und Berührungsängste mit einer „fremden“ Technologie nehmen. Wie immer hängt die Wahl einer geeigneten Plattformstrategie von den Anforderungen und Rahmenbedingungen ab. Folgende Beobachtungen sind oberflächlich zu machen:

  • Der Interpreter-Ansatz mittels Titanium bedeutet zunächst den geringsten Aufwand im Code und bietet ferner den Vorteil, dass man aktuell gleich zwei Plattformen adressiert (Android, iOS). Bei der Erweiterung auf weitere Plattformen ist man allerdings auf den Hersteller angewiesen.
  • Die Lernkurven bei Titanium und Windows Phone 7 sind niedriger, da hier bekannte bzw. gebräuchlichere Programmiersprachen eingesetzt werden.
  • Die Lernkurve bei iOS ist – bedingt durch die Programmiersprache – höher als bei den „Konkurrenten“. Apple-Entwickler haben hier natürlich einen Vorteil.
  • Die Toolunterstützung und der Debugging-Support von Titanium ist nicht sehr gut und die Entwicklung für JavaScript-Neulinge zunächst fehleranfällig. Die Konkurrenz ist dagegen mit sehr mächtigen IDEs ausgestattet.
  • Die unterschiedliche Reife des Titanium-APIs für die Unterstützung von iOS und Android stellt große Hürden dar. Für iOS ist das Framework heute schon gut einsetzbar, bei Android sollte man vielleicht lieber auf das Titanium-Release 1.5 warten.
  • Sofern ein Generator und eine DSL bestehen, ist eine Entwicklung von mobilen Apps damit am einfachsten, da der Abstraktionsgrad der Modellierung idealerweise frei von plattformspezifischen Aspekten ist und sich auf die Inhalte bezieht.
  • Der DSL- und Generator-Ansatz ist extrem mächtig, wenn es darum geht „gleichartige“ Probleme zu lösen. Bei der Entwicklung von Produktfamilien oder dem Aufbau einer eigenen Mobile-Referenzarchitektur (mit Abbildungen auf verschiedene Plattformen) kann ein generativer Ansatz sehr lohnenswert sein und hohe Kosten und Zeitersparnisse bieten.
  • Die Erstellung und Pflege eines Generators und einer DSL (beides hier nicht gezeigt) kann recht aufwendig werden und verlangt ein gewisses Spezial-Know-how für den Einstieg.
  • Für die Entwicklung mit Java (Android) oder Objective-C (iOS) zeigt sich zunächst, dass die Unterschiede in der Verwendung der Frameworks gar nicht zu groß sind (zumindest für unser Beispiel). Natürlich weichen die Syntax und die APIs voneinander ab, jedoch denken wir, dass der Einstieg von der einen Plattform/Technologie in die andere überschaubar ist.
  • Bei allen Plattformen hat man mit der nativen Entwicklung den Vorteil, alle gerätespezifischen Features am besten ausnutzen zu können.
  • Unter iOS hat man bei der nativen Entwicklung zudem den Vorteil, schon vorhandenen C-Quellcode zu nutzen, da auch dieser verarbeitet werden kann.
  • Bei der Entwicklung in C# (Windows Phone 7) kann man aktuell auf sehr mächtige und leistungsfähige APIs und Frameworks zurückgreifen, was die Entwicklung erleichtert. Leider sind noch nicht alle Geräteeigenschaften in den APIs berücksichtigt, was aber sicherlich in einem der nächsten Releases folgen wird.
  • Windows Phone 7 sticht auch etwas heraus, was sicher daran liegen könnte, dass es die jüngste Plattform im Vergleich ist. Das bereitgestellte Tooling und die Kombination mit Silverlight und mächtigen APIs (LINQ) erlauben einen sehr schnellen Einstieg in die Windows-Phone-7-Entwicklung.
  • Bei den Fähigkeiten und Möglichkeiten der Zielplattform hat Windows Phone 7 sicher gegenüber iOS und Android einiges aufzuholen, jedoch sieht der Grundstein für die Softwareentwicklung für diese Plattform sehr solide und vielversprechend aus.
  • Geschwindigkeitstechnisch hat unserer Meinung nach iOS bei nativen Apps einen Vorsprung, da man hier die Möglichkeit hat, auf einer kompatiblen Plattform sehr hardwarenah zu entwickeln.
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -