www.android360.de
Vol.1
Fakten fr Android-Entwickler
MOBILE COMMANDER
SONDER
HEFT
Caching fr Pros
Caching mit den Content-Providern
EDITORIAL
VOLUTION IST DA, DIE MOBILE RE EHT KEIN ZWEIFEL DARAN BEST
Android Remoting
von Gregor Roth
Liebe Leserinnen
und Leser,
angeknder Print-Ausgabe wie ich bereits in mehr zu bieten, droid sehr viel digt habe, hat An knnten. Daziges Heft packen s wir in ein ein al viel Wissen n darin schon so bei haben wir Ihne n in Andron die Neuerunge geboten: Sie habe hren, wie nt, Sie haben erfa id 2.2 kennengeler nur in der tt n-Source-OS nich sich Googles Ope z sichert s einen festen Plat ne Welt der Smartpho vider von Android erheitskonzept aching-Content-Pro und wie das Sich ben Sie einen C ber hinaus ha hwarz funktioniert. Dar nsentwicklung von Ronan Sc in die Applikatio Anlten sten Einblick er R erha Jeder, der mobile h-Technologie AI alityen entwickelt, auf Basis der Flas d-Re wendung rch die Augmente r oder aber und einen Blick du wird sich frhe fen. Viel Wissen e Welt gewor g belBrille auf di spter mit Cachin t. Deswegen erha g aus unserer Sich n. Eine eitere nicht genu schftigen msse eausgabe zwei w mit dieser Onlin e Daten ten Sie Applikation, di er bespannende Artikel. bezogene Sonvon einem Serv ste themen VPN, prosehen also: Das er den Sie scheinen ternet oder einem vierteljhrlich er zieht, sei es vom In em Cache. derheft unseres das sich ber Anlen Fllen von ein chnology, tiert in nahezu al mit ContentwichHefts Mobile Te etet eite d-Framework bi der vollen Bandbr gien Das Androi stungsfhige droid hinaus mit gien und Strate einfache und lei iler Technolo ovidern eine tiger mob angekom- Pr Problem zu lsen. ch nicht am Ende Mglichkeit, das beschftigt, ist no men
viel Spa mit dem In diesem Sinne Android 360 Onlinebonus von
Thomas Wieeck Redakteur el
ANDROID360
www.android360.de
REST
Android Remoting
Die zunehmend bessere Versorgung mit mobilen, breitbandigen Datendiensten sowie die immer leistungsfhigeren Endgerte schaffen immer bessere Voraussetzungen fr die Entwicklung hochwertiger, mobiler Apps auf Basis verteilter Anwendungsarchitekturen. Typischerweise kommunizieren solche Apps ber den RESTful-HTTP-Ansatz mit serverseitigen Diensten, um bestimme Use Cases umzusetzen. Dieser Artikel beleuchtet unterschiedliche Anstze fr die clientseitige Implementierung der Serverkommunikation.
entsprechenden XML-Layoutdatei bergeben. Danach werden die dynamischen Oberflchenelemente mithilfe der Methode findViewById() ermittelt und mit den entsprechenden Werten gesetzt. Bei verteilten Anwendungen sind die relevanten Daten hufig auf einem Server gespeichert und werden dort verwaltet. Das heit, um die bentigten Daten fr die Darstellung zu erhalten, muss zunchst eine Remote-Abfrage beim Server erfolgen (Listing 1).
RESTful HTTP
Bei Android-Applikationen stellt in der Regel der RESTful-HTTP-Ansatz die prferierte Architektur fr die Serverkommunikation dar. Die RESTful-HTTP-Architektur besagt im Wesentlichen, dass die Methoden des HTTP-Protokolls wie GET, POST, DELETE oder PUT protokollkonform angewendet werden, um mit dem Server Informationen auszutauschen. GET dient dazu, Daten von der Serverseite abzuholen, POST bzw. PUT
www.android360.de
ANDROID360
REST
dazu, um Daten an die Serverseite zu bertragen bzw. zu aktualisieren. Eine detailliertere Betrachtung von REST bzw. RESTfull HTTP ist unter [1] zu finden. Im Gegensatz zum SOAP-Protokoll wird REST durch Android direkt untersttzt. Die Android-Plattform beinhaltet alle Bibliotheken, die typischerweise fr eine REST-basierte Kommunikation bentigt werden. So sind beispielsweise der Apache Commons HTTPClient und ein JSON-Parser auf Basis des org.json-APIs Teil der Android-Distribution. Artefakte fr das eher schwergewichtigere SOAP-Protokoll sind in der aktuellen Implementierung von Android nicht zu finden. In diesem Fall
Listing 1: Activity-Klasse
public class UserInfoActivity extends Activity { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.user_info); UserInfo userInfo = TextView nameView = (TextView) findViewById(R.id.name); nameView.setText(userInfo.getName()); // } }
Listing 2: REST-Aufruf
public class UserRESTClient { private final AndroidHttpClient httpClient = AndroidHttpClient.newInstance("MyUserAgent"); public UserInfo getUserInfo(String userURI) { HttpUriRequest request = new HttpGet(userURI); request.setHeader("Accept", "application/json"); request.setHeader("Accept-Encoding", "GZIP"); HttpResponse response = httpClient.execute(request); int status = response.getStatusLine().getStatusCode(); if (status == HttpStatus.SC_OK) { InputStream instream = AndroidHttpClient.getUngzippedContent(response.getEntity()); String charset = EntityUtils.getContentCharSet(response.getEntity()); String jsonString = IOUtils.toString(instream, charset); JSONObject jsonObject = new JSONObject(userInfoString); UserInfo userInfo = new UserInfo(); userInfo.setUri(jsonObject.getString("uri")); // return userInfo; } // } }
muss dann auf Bibliotheken wie kSOAP2 zurckgegriffen werden. Da die Android-Plattform jedoch nicht das komplette Java-5.0-API bereitstellt, sind einige JavaBibliotheken nicht ohne Weiteres einsetzbar. Um auf die Besonderheiten der Android-Plattform einzugehen, sind von manchen Bibliotheken spezielle Versionen fr Android entstanden. Dies trifft auch fr kSOAP2 zu. Aufgrund der Inaktivitt des kSOAP2-Projekts sind die Patches jedoch auerhalb des Projekts entstanden. Im Gegensatz zu stationren PCs, die hufig mit einer hohen Bandbreite an das Internet angeschlossen sind, spielen bei mobilen Endgerten die Netzabdeckung, die verfgbare Bandbreite und die limitierten Hardwareressourcen eine wichtige Rolle. Oft verfgen die mobilen Endgerte nur ber eine geringe Bandbreite, was zu langen Ladezeiten fhren kann. Des Weiteren kann ein erhhter Bandbreitenbedarf, je nach Vertragstyp des Nutzers, schnell zu hohen Kosten fhren. Eine Reduzierung des bentigten Datenstroms spart aufgrund der verkrzten Kommunikationszeit wichtige Batterielaufzeit. Daher ist bei der Remote-Kommunikation darauf zu achten, dass mglichst nur die wirklich bentigten Daten bertragen werden sowie dass die Hufigkeit der Remote-Aufrufe mglichst niedrig gehalten wird. Bei der Wahl der Encodierung der zu bertragenden Daten ist ein kompaktes Format zu whlen. Flexible REST-Services erlauben es dem Client, den gewnschten MIME-Typ der Antwortstruktur im Request mitzuteilen. HTTP definiert hierfr den Header Accept, der den gewnschten MIME-Typ deklariert. Hufig untersttzen REST-Services die MIME-Typen application/json bzw. application/xml. Manche RESTServices bieten des Weiteren binre MIME-Typen an. Aus dem Gesichtspunkt der Bandbreitenbetrachtung gilt folgende Tendenz: Binrdaten vor JSON vor XML. Jedoch fhrt die Nutzung binrer Formate hufig zu erhhten Aufwendungen und Komplexitt bei der Implementierung des (De-)Marshallings. Im Gegensatz zu XML oder JSON kann beim Umgang mit den binren Typen in der Regel nicht auf hherwertige Bibliotheken in der Android-Plattform zurckgegriffen werden. Zudem existieren nur wenige, nicht proprietre binre Datenformate mit einer nennenswerten Verbreitung. Daher wird oft auf JSON zurckgegriffen. Um trotzdem ein gutes Bandbreitenverhltnis zu erreichen, sollten die zu bertragenden Daten komprimiert werden. In der Regel knnen bei application/json und application/xml sehr gute Kompressionsraten erzielt werden. Ein Vergleich der bentigten Bandbreite und der Performance fr unterschiedliche MIME-Typen ist unter [2] zu finden. Im Codebeispiel wird der Request mit dem Header Accept-Encoding: GZIP angereichert, um dem Server mitzuteilen, dass dieser mglichst die Antwortdaten komprimieren soll. Fr den Aufruf des Servers wird im Beispiel der AndroidHttpClient verwendet, der seit dem Release 2.2 von Android zu Verfgung steht. Der AndroidHttpClient basiert auf dem Apache Commons HTTPClient und setzt bei diesem typische Kon-
ANDROID360
www.android360.de
REST
figurationswerte, passend fr die Android-Umgebung. Beispielsweise wird durch den AndroidHttpClient der connectionTimeout und der soTimeout des zugrunde liegenden HttpClient auf 20 Sekunden gesetzt. Die Securityarchitektur von Android erfordert fr den Zugriff auf bestimmte Systemressourcen spezielle Privilegien. Um die bentigten Berechtigungen fr den Zugriff auf das Internet zu erhalten, ist in der AndroidManifest. xml die Permission android.permission.INTERNET zu setzen. Die der Applikation zugeordneten Permissions aus AndroidManifest.xml werden dem Anwender bei der Installation der Applikation angezeigt. Der Benutzer hat dann die Mglichkeit, die Installation abzubrechen, falls ihm die Permissions zu weitreichend sind.
das Android-Laufzeitsystem das Antwortverhalten von Applikationen. Erfolgt beispielsweise innerhalb einer gewissen Zeit keine Reaktion auf den Userevent der Oberflche, wird ein Application Not Responding (ANR) ausgelst. Der ANR ffnet einen Dialog, der den Benutzer auf eine nicht mehr reagierende Applikation hinweist und ihm die Mglichkeit gibt, die Applikation zu schlieen, d. h. das Terminieren des zugrunde liegenden Prozesses zu erzwingen. Potenziell langlaufende Aktivitten wie Serveraufrufe sollten daher immer innerhalb dedizierter Java Threads ausgefhrt werden. Typischerweise wird der Serveraufruf als ein eigenstndiger Aktionscode implementiert. Fr die Prozessierung des Serveraufrufs innerhalb eines Worker Threads bietet sich das Executor-Framework aus dem java.util.concurrent Package an. Das ExecutorFramework stellt verschiedene Thread-Pools zur Verwaltung und Ausfhrung von Threads bereit. Auf Basis der Ergebnisdaten des Serveraufrufs ist hufig die Benutzeroberflche zu aktualisieren. Diese Aktualisierung darf jedoch nur im Rahmen des Main Threads erfolgen. Analog zum Serveraufruf wird die Aktion zur Oberflchenaktualisierung typischerweise als ein eigenstndiges Codefragment implementiert. Da die Oberflchenaktualisierung durch den Main Thread stattfinden muss, ist der Oberflchenaktualisierungscode in die MessageQueue des Main Threads einzustellen. Hierzu kann auf die Handler-Klasse des Android-APIs zurckgegriffen werden. Die Handler-Klasse stellt unter anderem die post()-Methode zu Verfgung. Beim Aufruf dieser Methode wird ein Runnable Object bergeben, das in eine Message verpackt und anschlieend in die Queue eingestellt wird. Innerhalb der Endlosschleife des
www.android360.de
ANDROID360
REST
Main Threads wird dann die Message bzw. das Runnable-Objekt aus der MessageQueue gelesen und ber den Aufruf deren run()-Methode innerhalb des Main Threads prozessiert. Im Rahmen der Oberflchenaktualisierungsaktion kann es auch passieren, dass die Activity zwischenzeitlich zerstrt wurde. Beispielsweise lst ein Drehen des Endgerts ein zerstren der Activity aus. Die Verbindung zwischen dem Handler-Objekt und der MessageQueue des Main Threads erfolgt im Konstruktor der Handler-Klasse. Innerhalb des Konstruktors greift das Handler-Objekt auf eine Threadlocal-Variable des aktuellen Main Threads zu und speichert dessen MessageQueue als member-Variable. Die MessageQueue wird im Rahmen der Initialisierung des Main Threads als Threadlocal-Variable an den Main Thread gebunden. Aus diesem Grund ist der Handler immer innerhalb des Main Threads zu instanziieren. Aufgrund des Android-Prozess-/Thread-Modells besteht ein typischer Serveraufruf aus zwei Aktionen. Dem eigentlichen Serveraufruf, der in einem dedizierten Worker Thread stattfinden sollte, sowie aus einer Aktualisierungsaktion, die wiederum im Kontext des Main Threads ausgefhrt werden muss. Alternativ zum obigen Codebeispiel kann statt der Handler-Klasse auch die ConvenienceMethode runOnUiThread() der Activity verwendet werden. Diese Methode nutzt intern analog dem Codebeispiel, einen Handler, der das Activity-Objekt implizit bei deren Initialisierung erstellt. Bei der Nutzung der Convenience-Methode muss im Applikationscode kein Handler-Objekt mehr fr die Synchronisation erzeugt werden. Mit der Version 1.5 wurde Android zudem um die Klasse AsyncTask erweitert. Diese Klasse ist auf das Szenario Hintergrundaktion mit anschlieender Modifikation der Oberflche zugeschnitten. Zur Verwendung dieser Klasse ist von ihr abzuleiten
Listing 4: Local-Service-Implementierung
public class MyService extends Service { public int onStartCommand(Intent intent, int flags, int startId) { // } public IBinder onBind(Intent intent) { return binder; } private final MyLocalServiceBinder binder = new MyLocalServiceBinder(); public class MyLocalServiceBinder extends Binder implements IMyService { public UserInfo getUserInfo(String userURI) throws IOException { return restClient.getUserInfo(userURI); } } }
ANDROID360
www.android360.de
REST
hohe Prioritt, wenn die Activity nicht mehr sichtbar ist, der enthaltene Service aber noch luft. Um die Fairness zwischen den Applikationen aufrechtzuerhalten, sollten Services nur dann gestartet werden, wenn diese auch wirklich bentigt werden. Ebenso sollten Services mglichst zeitnah wieder beendet werden, wenn sie nicht mehr bentigt werden.
userURI, IResultCallback<UserInfo> callback) verwendet. Der Main Thread blockiert nur fr eine sehr kurze Zeit, da der Service den Request lediglich zur Prozessierung annimmt, um ihn dann asynchron ber einen dedizierten Thread zu verarbeiten. Die Verwendung eines AsyncTask auf der Clientseite wird berflssig. Lediglich die Aktualisierung der Oberflche auf Basis des Ergebnisses muss wieder im Rahmen des Main Threads stattfinden. Zur bermittlung des Ergebnisses ruft der Service die entsprechende CallbackMethode des IResultCallback<UserInfo>-Objekts auf, beispielsweise onResult(UserInfo userInfo) oder onError(IOException ioe). Ein Callback Pattern wird ebenfalls beim Aufruf der bindService()-Methode an der Context-Klasse verwendet. Fr den Aufbau einer Kommunikationsverbindung zum Service bergibt der Client neben dem Intent und dem start Flag auch ein applikationsspezifisches ServiceConnection-Objekt. Die ServiceConnection-Klasse definiert Callback-Methoden, um Statusnderungen der Kommunikationsverbindung zu erkennen. Die bindService(...)-Methode gibt also weder ein Binder- noch ein ServiceConnectionObjekt direkt zurck. Das notwendige Binder-Objekt zur Kommunikation mit dem Service wird erst in einem nachfolgenden Loopzyklus des Main Threads durch den Aufruf der Callback-Methode onServiceConnected() des ServiceConnection-Objekts bergeben.
www.android360.de
ANDROID360
REST
Die Implementierungen von local-Services und remote-Services sind sich sehr hnlich. Der wesentliche Unterschied ist die Binder-Implementierung, die vom Service in der onBind()-Methode bereitgestellt wird. Im Gegensatz zum Binder des local-Service untersttzt der Binder des remote-Service eine Inter Process Communication (IPC). Der Begriff Binder steht bei Android nicht nur fr einen Interface- bzw. Klassenamen, sondern definiert auch eine Architektur fr Inter Process Communication. Die Binder-Architektur von Android hat ihren Ursprung in Open Binder [5] und findet hufige Anwendung innerhalb des Laufzeitsystems von Android. Im Beispiel des local-Service gibt die onBind(...)-Methode ein MyLocalServiceBinder-Objekt an die Android-Laufzeitumgebung zurck. Genau diese Objektinstanz wird dann dem Client mittels der Call-
back-Methode onServiceConnected() bereitgestellt. Im Fall des remote-Service gibt die onBind()-Methode stattdessen den MyRemoteServiceBinder zurck. Diese Klasse implementiert sowohl das fachliche Interface als auch die schnittstellenspezifische Logik zur Untersttzung des Remote-Aufrufs. Leider untersttzt die IPCArchitektur keine anwendungsspezifischen Exceptions. Fr die Exception-Behandlung knnte beispielsweise die Antwort in ein Result-Objekt eingebettet werden, das ein zustzliches Feld fr die Fehlermeldung beinhaltet. Alternativ kann auch ein Callback Pattern implementiert werden:
private final MyRemoteServiceBinder binder = new MyRemoteServiceBinder(); public class MyRemoteServiceBinder extends IMyService.Stub { public UserInfo getUserInfo(String userURI) throws RemoteException { return restClient.getUserInfo(userURI); } }
Listing 7: Content-Provider-Implementierung
public class MyContentProvider extends ContentProvider { // public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { switch (uriMatcher.match(uri)) { case USER_INFO: String userURI = uri.getLastPathSegment(); UserInfo userInfo = restClient.getUserInfo(userURI); MatrixCursor cursor = new MatrixCursor(new String[] { "uri", "name", "city", "zip" }); cursor.addRow(new Object[] { userInfo.getUri(), userInfo.getName(), userInfo.getCity(), userInfo.getZip() } ); return cursor; // } // }
Der IPC-relevante Code wird im Beispiel von der generierten Klasse IMyService.Stub geerbt.Typischerweise wird der IPC-relevante Code nicht per Hand implementiert, sondern es wird auf die Generierung der entsprechenden Klassen zurckgegriffen. Die AndroidPlattform stellt hierfr eine eigene Schnittstellenbeschreibungssprache aidl zur Verfgung. Mithilfe der aidl kann die Remote-Schnittstelle definiert werden. Die Syntax ist stark an Java angelehnt, jedoch existieren einige Einschrnkungen. Werden applikationsspezifische Datenstrukturen bertragen, so sind diese ebenfalls in einer aidl-Datei zu deklarieren. Des Weiteren muss die Implementierung der Datenstruktur wie die UserInfo-Bean-Hilfskonstrukte fr die Serialisierung und Deserialisierung bereitstellen. Die IPC von Android verwendet aus Effizienzgrnden nicht den Java-Serializable-Ansatz, sondern definiert mit dem Parcelable einen eigenen Serialisierungsmechanismus. Das ADT-EclipsePlug-in generiert automatisch die entsprechenden JavaKlassen und -Interfaces im /gen/-Verzeichnis. Im Fall des Remote-Service kann in der onServiceConnected()-Methode das bergebene Binder-Objekt nicht mehr unmittelbar verwendet werden. Im Gegensatz zum local-Service ist immer die statische Methode asInterface() der generierten Binder-Klasse zu verwenden, um das Proxy-Objekt fr den Serviceaufruf zu erhalten. Diese Methode erkennt, ob der RemoteService im gleichen Prozess wie auch der Aufrufer luft. Falls dies nicht zutrifft, wird ein Remote-Proxy-Objekt zurckgegeben, das die Serialisierung der Aufrufdaten vornimmt, den remote-Service aufruft und die Deserialisierung der Antwort durchfhrt. Ansonsten wird, wie auch beim local-Service, das konkrete Binder-Objekt zurckgegeben.
ANDROID360
www.android360.de
REST
der implementiert werden. Ein Content-Provider wird typischerweise verwendet, um auf Daten der lokalen Datenbank zuzugreifen. Die Android-Umgebung beinhaltet beispielweise eine SQLite-Datenbank. Obwohl die Schnittstelle des Content-Providers datenbankorientiert ist, werden Content-Provider nicht nur fr Datenbankzugriffe verwendet. Ein Content-Provider kann auch genutzt werden, um Remote-Aufrufe zu einem nachgelagerten Server zu kapseln. Wie auch die REST-Architektur ist der Content-Provider ressourcenorientiert. Die Entitten werden wie beim RESTful-HTTP-Ansatz ber URIs referenziert. Eine solche Content-URI besteht aus drei Teilen: dem Standardprefix content://, der Authority wie org.example.mycontentprovider, die den Content-Provider identifiziert, sowie dem verbleibenden Teil, der die Entitt identifiziert. Analog zur AsncTask-Klasse stellt Android eine ConvenienceKlasse AsyncQueryHandler bereit, die das Threading bzw. die Thread-Synchronisierung bernimmt. In der startQuery()-Methode des AsyncQueryHandler knnen dann neben dem Content-URI auch die Parameter projection, selection und orderBy bergeben werden. Das Ergebnis der Abfrage wird mithilfe eines Cursors bereitgestellt. Die applikationsspezifische Implementierung eines Content-Providers muss von der Klasse ContentProvider ableiten. Im Wesentlichen sind hierbei die Methoden query(), insert(), delete(), update() und getType() zu implementieren. Zur Auflsung des bergebenen URIs auf die betroffene Entitt wird in der Regel auf die UriMatcher-Klasse zurckgegriffen. Das Ergebnis der Remote-Abfrage des nachgelagerten Servers muss ber einen Cursor bereitgestellt werden. Im Codebeispiel wird hierfr die Android-Klasse MatrixCursor verwendet. Gibt der Server komplexe, verschachtelte Datenstrukturen zurck, so sind diese in der Regel nur schwer in einer effizienten Art und Weise auf den Cursor-Ansatz abbildbar. Hufig sind weitere Subabfragen am Content-Provider notwendig, um das komplette Ergebnis des Serveraufrufs zu erhalten.
eingebunden, geht die Tendenz dazu, die Aufrufe ber einen Android-Service oder einen Content-Provider zu kapseln. Dies gilt insbesondere, wenn ein applikationsspezifisches Caching der Serveraufrufe implementiert werden soll oder Server-Push-Architekturen umgesetzt werden mssen. Obwohl der Content-Provider ressourcenorientiert ist, darf er nicht mit der RESTful-HTTPArchitektur gleichgesetzt werden. Bei der Abbildung einer RESTful-HTTP-Schnittstelle auf die ContentProvider-Schnittstelle knnen insbesondere komplexe Datenstrukturen Probleme bereiten. Des Weiteren ist die Abbildung eines HTTP-Service-URIs auf einen Content-Provider-URI nicht trivial. Beim RESTful-HTTP-Ansatz ist es ein schlechter Stil, auf der Clientseite HTTP-URI-Strukturen zu interpretieren. Der zunchst naheliegende Ansatz, die Struktur eines URIs des HTTP-Service wie http://myserrver/srv/UserInfo/455 auf eine Content URI wie content://org.example.myprovider/UserInfo/4554 abzubilden, verstt gegen das Hypermedia-Prinzip von REST [7], da Teilstrukturen des HTTP URIs clientseitig interpretiert werden. Die Kapselung von HTTP-Services ber einen Android-Service lsst gegenber dem Content-Provider mehr Spielraum, stellt dafr aber keine (teil-)standardisierte Schnittstelle bereit. Wird ein Service nur innerhalb einer Applikation verwendet, so ist es in der Regel einfacher, einen localService zu nutzen bzw. zu implementieren.
Gregor Roth arbeitet als Softwarearchitekt bei der 1&1 Internet AG. Er verfgt ber langjhrige Erfahrungen im Bau verteilter, hochskalierbarer Anwendungssysteme und betreut architektonisch die Entwicklung der webbasierten Applikationen der Marken WEB.DE, GMX und 1&1. Zuvor war er knapp zehn Jahre als Softwarearchitekt im Finanzdienstleistungsbereich ttig.
Fazit
Die hier vorgestellen Anstze zur Implementierung von Remote-Abfragen nachgelagerter Server stellen nur einen Ausschnitt der mglichen Lsungen dar. Des Weiteren wurde in diesem Artikel das Caching von Applikationsdaten nicht betrachtet. Insbesondere bei hochverteilten Systemarchitekturen entwickelt sich das Caching auf der Applikationsebene schnell zu einem wichtigen Thema. Eine gute Betrachtung der Implementierungsanstze auf Basis von Android Services und Content-Proivder ist auch im Video Podcast [6] zu finden. Fr einfache, vereinzelte Serveraufrufe ist es oft ausreichend, auf ein richtiges Threading bzw. auf die Thread-Synchronisierung zu achten und sich der Prozesspriorisierung und deren Folge bewusst zu sein. Werden jedoch umfangreichere HTTP-Schnittstellen wie ein HTTP-basierter Mail-Service oder ein Contact-Service
www.android360.de
ANDROID360
CaChing
Caching-ContentProvider
Jeder, der mobile Anwendungen entwickelt, wird sich frher oder spter mit Caching beschftigen mssen. Eine Applikation, die Daten von einem Server bezieht, sei es vom Internet oder einem VPN, profitiert in nahezu allen Fllen von einem Cache. Denn anders als bei einer Desktopanwendung sind im Mobile-Bereich Verbindungsabbrche oder gar der Totalausfall des Netzwerks an der Tagesordnung. Das betreten einer U-Bahn-Station mag gengen. Die Daten fr einen gewissen Zeitraum, selbst wenn dieser nur wenige Minuten umfasst, auf dem Gert lokal zu speichern, wird den Workflow fr den Anwender deutlich erleichtern. Das Android-Framework bietet mit Content-Providern eine einfache und leistungsfhige Mglichkeit, das Problem zu lsen.
die Sicherstellung einer sinnvollen Struktur und der entsprechenden Datenintegritt kann sich jedoch als kompliziert erweisen. Datenbanken haben den Vorteil der klaren Struktur und sind lange erprobte Tools, mit denen sich wohl jeder Programmierer auskennt. Allerdings muss die Frontseite entweder einiges an Wissen ber die Struktur und den Zugriff auf die Daten mitbringen (und man hat SQL-Statements im GUI-Code), oder man muss zustzlich das Datenmodell in Klassen abbilden (was fr eine mobile Plattform ein nicht unbedingt wnschenswerter Overhead ist). ContentProvider bieten ein klares, URL-basiertes Interface, die Struktur und Mchtigkeit einer Datenbank und
10
ANDROID360
www.android360.de
CaChing
schirmen das GUI von der physikalischen Speicherung ab. Content-Provider bieten von vornherein auch die Mglichkeit, Daten mit anderen Applikationen zu teilen. Mchte man das nicht oder nur in Teilen, so kann man im Manifest File die entsprechenden Permissions setzen. Einen berblick ber Content-Provider anderer Apps, die zur Verfgung stehen, bietet die OpenIntents Registry [1].
Caching-Content-Provider
Das sich ein Content Provider als Cache verwenden lsst, liegt auf der Hand. Der groe Vorteil von Content-Providern ist jedoch nicht so offensichtlich: die Content Observer. Ein Content Observer ist eine Android-spezifische Implementierung des Observer-Patterns fr Datenbanken. Wann auch immer sich ein bestimmtes Datum in der Datenbank ndert, werden alle vorher registrierten Observer darber informiert. Dies gilt gleichermaen fr Content-Provider. Mit dieser im Grunde sehr einfachen Idee lassen sich GUI, Content-Provider und Backend so verbinden, dass sich die angezeigten Daten automatisch aktualisieren. Als GUI-Komponente bietet sich eine ListActivity an. Eine ListActivity kann bereits mit wenigen Zeilen Code die in einem Cursor enthaltenen Daten anzeigen. Sie bietet zudem eine einfache Mglichkeit, einen Content Observer zu implementieren (zu lassen). Der sorgt dann dafr, dass die in der Liste angezeigten Eintrge jeder-
zeit synchron zu den Daten des Content-Providers sind. Die Klasse CursorAdapter und ihre abgeleiteten Klassen stellen diese Funktionalitt bereits zur Verfgung. Es reicht also aus, in der ListActivity einen passenden Adapter zu bergeben. Nun muss noch der Cache mit den Daten aus dem Backend befllt werden. Dafr benutzen wir einen Service. Ein Service in Android ist ein Hintergrundprozess, hnlich einem Daemon in Unix. Der entscheidende Unterschied ist, dass ein Service nicht konstant luft, sondern hchstens in Intervallen vom System gestartet werden kann. Wie alle Prozesse kann auch ein Service vom Garbage Collector gestoppt werden, falls das System mehr Leistung fr den momentanen Vordergrundprozess braucht. Das ermglicht es Daten, im Hintergrund zu synchronisieren und schont gleichzeitig die Batterie. Fr mehr Informationen zu Android-Prozessen und dem Garbage Collector empfiehlt sich das exzellente Posting Multitasking the Android Way von Diane Hackborn im Android-Developer-Blog [2].
Implementierung
Fr einen Caching-Content-Provider werden also drei Komponenten bentigt: eine ListActivity zur Anzeige, ein Service, um die Daten vom Web zu lesen, und ein Content-Provider fr die Speicherung. Diese werden nun anhand eines Beispielprojekts exemplarisch implementiert und besprochen. Wir versuchen, eine Reihe
www.android360.de
ANDROID360
11
CaChing
von Kunden mit Name und E-Mail aus dem Backend zu laden und anzuzeigen. Da es in Wirklichkeit keinen Server gibt, von dem die Daten kommen knnten (und die Anbindung an einen realen den Umfang dieses Artikels sprengen wrde), wird der Service dies simulieren. Aber der Reihe nach.
halten, zeigt die ListActivity die TextView an; sobald sich die Liste fllt, verschwindet diese wieder. All das wird bereits fr uns erledigt und bedarf keines weiteren Codes. Wir erzeugen einen Cursor mittels eines Querys auf den Default-URL des Content-Providers, der alle Customer-Eintrge liefert. Die Funktion startMana gingCursor(mCursor) sorgt dafr, dass automatisch ein Requery ausgefhrt wird, wenn sich am Zustand des Cursors etwas ndert z. B. auch dann, wenn die Activity in den Hintergrund geht. Der SimpleCursor Adapter verwendet das angegebene Layout fr die Anzeige und ordnet den Spalten die jeweiligen TextViews
@Override public Uri insert(Uri uri, ContentValues initialValues) { if (sUriMatcher.match(uri) != CUSTOMERS) { throw new IllegalArgumentException("Unknown URI " + uri); } ContentValues values; if (initialValues != null) { values = new ContentValues(initialValues); } else { values = new ContentValues(); } // Make sure that the fields are all set if (values.containsKey(Customers.NAME) == false) { Resources r = Resources.getSystem(); values.put(Customers.NAME, r.getString(android.R.string.untitled)); } if (values.containsKey(Customers.EMAIL) == false) { values.put(Customers.EMAIL, ""); } SQLiteDatabase db = mOpenHelper.getWritableDatabase(); //the name field will be assigned a null value if the row is empty. long rowId = db.insert(CUSTOMERS_TABLE_NAME, Customers.NAME, values); if (rowId > 0) { Uri rowUri = ContentUris.withAppendedId(Customers.CONTENT_URI, rowId); getContext().getContentResolver().notifyChange(rowUri, null); //return the uri of the newly created dataset return rowUri; } throw new SQLException("Failed to insert row into " + uri); } }
12
ANDROID360
www.android360.de
CaChing
zu. Fr komplexere Layouts, z. B. mit Bildern, sollte eine eigene Adapterklasse implementiert werden. Wir weisen der ListActivity den Adapter zu und erzeugen somit die Liste und ihre enthaltenen Elemente.
Der Content-Provider
Der Content-Provider enthlt eine Menge an zustzlichem Code, der sich mit Aufbau und Versionierung der darunter liegenden SQLite-Datenbank sowie dem berprfen der aufgerufenen URLs beschftigt. Diese sind immer hnlich, weshalb wir uns auf die wichtigen Teile konzentrieren. Es sei allerdings noch darauf hingewiesen, dass sich die Datenbank keineswegs auf dem internen Speicher befinden muss; der Pfad kann durchaus auch auf der SD-Karte liegen. Man kann auch ganz auf eine Datenbank verzichten solange ein valider Cursor zurckgegeben wird, sind der Phantasie fast keine Grenzen gesetzt. Die entscheidenden Methoden des Content-Providers sind query(), insert() und update(). In query starten wir den Service, nachdem wir einen Cursor zurckgeben, der entweder leer ist oder veraltete Daten enthlt. Mit c.setNotificationUri(getContext(). getContentResolver(), uri); sorgen wir dafr, dass der Cursor aktualisiert wird, wenn sich die Daten im Content-Provider ndern. Das erreichen wir, wenn wir in in sert und update die Content Observer benachrichtigen.
int r3=(int)Math.floor((Math.random() *4)); String name=FIRST_NAMES[r1]+" "+LAST_NAMES[2]; String email=name+DOMAINS[r3]; cv.put(Customers.NAME,name); cv.put(Customers.EMAIL,email); return cv; } // Set an alarm that will restart out service private void scheduleNextRun(){ Intent intent=new Intent(); intent.setClass(getApplicationContext(), DataService.class); PendingIntent pendingIntent= PendingIntent.getService(getApplicationContext(), 1, intent, PendingIntent.FLAG_ONE_SHOT); AlarmManager alarmManager= (AlarmManager)getSystemService(ALARM_SERVICE); long now= System.currentTimeMillis(); //start after 1.5 minutes/ 90 seconds alarmManager.set(AlarmManager.RTC_WAKEUP, (now+ 90000), pendingIntent); } }
Die Datenbank muss sich keineswegs auf dem internen Speicher befinden.
Listing 4: Proof-of-Concept-Code
public class DataService extends Service{ @Override public void onStart(Intent intent, int startId) { super.onStart(intent, startId); Log.d(TAG,"started"); //normally all of this should be loaded off to another thread //a ~30% chance of deleteing all data in the cursor if((int)Math.floor((Math.random() *100))>66) { getContentResolver().delete(Customers.CONTENT_URI, null,null); } //write some random data for (int i=0;i<10;i++) { ContentValues cv=getImportantDataFromTheInternet(); getContentResolver().insert(Customers.CONTENT_URI, cv); } //make sure we run again. scheduleNextRun(); //done. stopSelf(); } private ContentValues getImportantDataFromTheInternet(){ ContentValues cv= new ContentValues(); //get us three random numbers to create a customer int r1=(int)Math.floor((Math.random() *8)); int r2=(int)Math.floor((Math.random() *4));
www.android360.de
ANDROID360
13
CaChing
Ob man das auch in delete() macht, hngt davon ab, ob dieser Event wirklich bentigt wird. In unserem Fall folgt auf jedes delete ein insert, um neue Daten einzufgen; die Aktualisierung des GUI mit einem leeren Cursor zwischen beiden Operationen wird eher verwirren, als ntzen. Damit der Service auch wei, welche Daten gefragt sind, bergibt man einen passenden Parameter als Extra in den Intent. Der URL bietet sich hier besonders an. Der Content-Provider verwendete bereits eine Hilfsklasse vom Typ UriMatcher, um einem URL eine eindeutige ID zuzuordnen, auf die man entsprechend reagiert. Der Code kann einfach in den Service kopiert werden. Wenn man obendrein noch eine REST-Schnittstelle anspricht, kann man eigentlich auch die REST-URLs als Content Urls verwenden und somit direkt an den Service weitergeben. Die entscheidende Zeile, die unsere Cursor von den nderungen in Kenntnis setzt, ist getContext().getContent Resolver().notifyChange(uri, null);. Die null-Variable ist der Content Observer, der die nderungen ausgelst hat. In diesem Fall nicht existent und damit null. uri ist die ContentUrl des Datensatzes, der sich gendert hat. Bei einem insert muss dieser URL erzeugt werden. Die insert-Funktion der Datenbank liefert uns die ID der genderten Zeile, sie wird einfach mit ContentUris.with AppendedId(Customers.CONTENT_URI, rowId); an die passende ContentUri angehngt. In query oder up date nehmen wir dieselbe URI, die als Parameter beim Aufruf bergeben wurde.
knnten vom Garbage Collector beendet werden, wenn das System mehr Ressourcen braucht. Dessen Instanz holen wir uns vom Context (also der Serviceklasse) mit getSystemService(ALARM_SERVICE), eine direkte Instanzierung ist nicht mglich. Wir erzeugen einen Intent und wandeln diesen in einen PendingIntent. Pending Intent.getService(..) sorgt dafr, dass der Service vom System gestartet wird. Wir setzen den nchsten Start mit AlarmManager.set(..) und benutzen RTC_WAKE UP, um sicherzustellen, dass der Intent ausgelst wird auch wenn das Device im Sleep Mode ist (z. B. bei ausgeschaltetem Bildschirm). Die Funktion Set setzt nur einen einzigen Alarm, stattdessen knnte man auch set Repeating(..) verwenden, um eine Serie zu setzten. Fr dieses Beispiel gengt es, den gesamten Code in onStart auszufhren. In der Realitt ist das keine so gute Idee, es empfiehlt sich stattdessen, z. B. Worker Threads zu verwenden.
Fazit
Damit sind nun alle Komponenten zusammen. Wir haben eine effiziente und einfache Struktur, um Caching in unsere Anwendung zu integrieren, das eine saubere Trennung von Backend, Speicher und Darstellung gewhrleistet. Die Anzeige aktualisiert sich von alleine, wenn neue Daten bereit stehen. Der Cache kann im Hintergrund mit neuen Daten gefllt werden, wann immer die Mglichkeit und/oder Notwendigkeit besteht. Das Beispielprojekt setzt auf Android 1.6 auf, sollte aber problemlos auf ltere Versionen angepasst werden knnen. Die vorgestellte Methodik ist auf nahezu alle Projekte anwendbar. Der gesamte Beispielcode steht unter der Apache License 2 und kann mit folgendem Befehl heruntergeladen werden:
svn checkout http://openintents.googlecode.com/svn/trunk/samples/ CachingContentProvider openintents-read-only
Ronan Schwarz wuchs mit der Digitalisierung auf. heute ist er ein Teil davon und arbeitet als mobile-Softwareentwickler bei TiC-mobile. Ronan ist ein Experte fr android und einer der ersten Entwickler fr das neue Samsung-OS Bada.
14
ANDROID360
www.android360.de
NEU
Das neue Magazin fr
Jetzt abonnieren
iPhone, iPad Android Mobile Web Windows Phone 7 Tools & Best Practices App Stores Mobile Commerce & Marketing Business & Strategien
go: mobiletechnologymag.de