Im Teil 1 des Workshops zeigten die Anpassungen am Manifest sowie die neuen Fragmente und Layoutvorgaben im Detail. Der zweite Teil unseres Workshops zeigt die Anpassungen an der Datenbank, den neuen ActionBar und das modifizierte Preference-System.
Bis einschließlich Android 2 war der SQLiteOpenHelper das Werkzeug der Wahl. Hier wurden vom Datenbank-Entwickler eines Projekts an zentraler Stelle alle Zugriffsmethoden erstellt auf die dann die Entwickler der grafischen Oberflächen zugreifen konnten. Der SQLiteOpenHelper stellt alle Mechanismen rund um die SQLite Datenbank zur Verfügung. Vom Anlegen und Upgrade der Datenbank selbst, bis hin zum Manipulieren der Datenbankinhalte lässt sich alles mit einer Subclass dieser Klasse erledigen. In den Activities wiederum wurden seitens der UI-Entwickler AsyncTasks genutzt um Daten unabhängig vom sensiblen UI-Thread zu manipulieren. Ab Android 3 gibt es hierfür die Loader.
Auf Heft-CD
Teil 1 des Workshops sowie den Quellcode und Binaries der hier besprochenen App finden Sie auf der Heft-CD.
CursorLoader
Leider wurden auch hier die Programmierer bestehender Anwendungen vermeintlich nicht mitgenommen. Das gesamte Klassengerüst funktioniert nur noch mit einem ContentProvider und nicht mehr mit einem Cursor. Das ist insofern verständlich, da das Cursor-Objekt erst durch die Loader geladen werden soll. In der Zeit vor dem CursorLoader hat der Entwickler den Cursor selbst laden müssen und, wenn er anständig gearbeitet hat, dazu eigenständige Tasks verwendet. Diese Bürde wird dem Entwickler nun mit den neuen CursorLoadern abgenommen.
Obwohl in der Android-Dokumentation nach wie vor an vielen Stellen explizit darauf hingewiesen wird, dass ContentProvider eigentlich nur für Zugriffe von außen gedacht sind, ist die Übergabe eines Query-String an den CursorLoader nicht möglich. Somit ist der Zugriff mittels CursorLoader auf lokale und App-interne Datenbestände ohne ContentProvider nicht möglich.
In unserer Beispiel-App (auf der Heft-CD) griffen wir zu einem Kniff, um nicht auch noch das komplette Datenbank-Handling in einen ContentProvider verlagern zu müssen. Eine Kopie der neuen AsyncTaskLoader Klasse (MyCursorLoader) mit einem minimalen Eingriff hilft dabei, auch ohne ContentProvider die Liste aller Einträge zu lesen. So ganz nebenbei macht die neue App dadurch von dem vorzüglichen Komfort der Loader Gebrauch, wie Listing 1 zeigt.
Listing 1
CursorLoader ohne ContentProvider
public class MyCursorLoader extends AsyncTaskLoader<Cursor> { // Ein neuer Konstruktor ... public MyCursorLoader(final Context context, final String selection, final String[] selectionArgs) { super(context); this.observer = new ForceLoadContentObserver(); this.selection = selection; this.selectionArgs = selectionArgs; } // ... und eine gapatchte Methode @Override public Cursor loadInBackground() { Cursor cursor = null; if (uri != null) { cursor = getContext().getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder); } else if (selection != null) { // Neu cursor = MyApplication.getSqliteDatabase().rawQuery(selection, selectionArgs); } if (cursor != null) { cursor.getCount(); registerContentObserver(cursor, observer); } return cursor; } }
Die neuen Loader kümmern sich um die asynchrone Datenbeschaffung, benachrichtigen mittels Callbacks über den aktuellen Status und sind auch für die Verwaltung der Cursor zuständig. Die unter Android 2 eminent wichtige Methode startManagingCursor()
ist somit ebenfalls obsolet. Ein Beispiel zeigt Listing 2.
Listing 2
Die neuen CursorLoader
public class FragmentList extends ListFragment implements LoaderManager.LoaderCallbacks<Cursor> { @Override public void onActivityCreated(Bundle bundle) { getActivity().getLoaderManager().initLoader(MyConstants.LDR_TABLE1LIST, null, this); adapter = new SimpleCursorAdapter(context, R.layout.fragmentlist_row, null, new String[] { Table1.DESCRIPTION }, new int[] { R.id.fragmentlist_row_description }, 0); setListAdapter(adapter); } @Override public Loader<Cursor> onCreateLoader(int id, Bundle bundle) { MyCursorLoader loader = null; switch (id) { case MyConstants.LDR_TABLE1LIST: loader = new MyCursorLoader(context, MySQLiteOpenHelper.TABLE1_FETCH, null); break; } return loader; } @Override public void onLoaderReset(Loader<Cursor> loader) { adapter.swapCursor(null); } @Override public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { adapter.swapCursor(cursor); setListShown(true); } }
Ein SimpleCursorAdapter wird angelegt und mit der Liste verbunden. Den Parameter für den Cursor lassen wir auf null, denn die neuen CursorLoader erlauben den dynamischen Austausch von Cursorn. Mit initLoader()
wird der Datenbeschaffungsvorgang in Gang gesetzt. Der Eintrag onCreateLoader()
fordert das Fragment auf, den CursorLoader zu liefern. Sobald die Daten anstehen, benachrichtigt der onLoadFinished()
das Fragment und der Cursor wird dem Adapter bekannt gegeben.
Die Datenbank
Durch den Kniff mit dem eigenen CursorLoader befinden sich das Anlegen und Verwalten der Datenbank sowie alle Zugriffsmethoden nach wie vor in einer Unterklasse des SQLiteOpenHelper. Die Datenbank wird in der Tablet-App nach wie vor an zentraler Stelle geöffnet und wieder geschlossen. Dazu bietet sich eine Subclass von Application (MyApplication) an. Damit Android diese Klasse berücksichtigt, muss es einen entsprechenden Eintrag im application
Tag des Manifest geben (android:name="MyApplication"
). Wie bei Java üblich gibt es auch bei Android immer verschiedene Wege zum Ziel. Ob man die Application-Klasse oder eigenständige Singletons zur zentralen Verwaltung der Datenbank nimmt oder die Datenbank in jedem Fragment oder jeder Activity öffnet, bleibt dem Entwickler überlassen. Die hier genutzte Methode hat sich im jahrelangen Praxistest bestens bewährt.
Letztendlich hebelt man mit einer zentralen Datenbankinstanz und dem Ignorieren der ContentProvider das auf Wiederverwendbarkeit getrimmte Konzept der Fragmente wieder aus. Das Fragment muss in unserem Beispiel Zugriff auf die Datenbankinstanz haben während die Verwendung von ContentProvidern dies besser kapseln könnte. Die zuvor gestellte Frage nach den Gründen für die Einschnitte bei der Datenbank wird somit an dieser Stelle beantwortet. Für eine Kleinstanwendung wie die Beispiel-App ist die gezeigte Herangehensweise durchaus in Ordnung. Jeder Entwickler sollte sich aber zum Start eines neuen Projektes genau überlegen wie re-usable seine Fragmente werden sollen. Gibt es nur den Hauch eines Zweifels dann sind ContentProvider ab sofort ein absolutes Muss.
Die ActionBar
Schon mit den wenigen bereits aufgeführten Handgriffen an der Manifest-Datei sowie in den Menü-Deklarationen erhält eine "alte" App die ActionBar.
Die ActionBar ist eine Kombination aus dem bekannten Optionenmenü, einer Titelzeile sowie einigen weiteren Features. Wichtige Menüeinträge lassen sich so prominent und permanent sichtbar positionieren – und das mit oder ohne Icons. Weitere Optionen, die aus Platzgründen nicht an prominenter Stelle zu finden sind, blenden Android-4-Apps durch einen Klick auf das Overflow-Menü (das Symbol mit den drei vertikal angeordneten Punkten) ein.
Menüeinträge lassen sich wie gewohnt zu Gruppen zusammengefassen. Zusätzlich kann das Umschalten von Views mittels Spinner aus der ActionBar heraus initiiert werden. Ein Beispiel ist der Mailaccount: An einem kleinen Dreieck in der unteren rechten Ecke des Spinners erkennt man die Auswahlliste, die sich nach dem Anklicken öffnet. Im Mailprogramm schaltet man damit zwischen verschiedenen Accounts um.
Listing 3
Optionen im neuen ActionBar
<item android:icon="@drawable/ic_menu_preferences" android:id="@+id/men_preferences" android:showAsAction="ifRoom|withText" android:title="@string/txt_preferences" />
Das neue – optionale – Attribut showAsAction
steuert die Sichtbarkeit in der ActionBar. Die Option ifRoom
zeigt, wie der Name schon sagt, diese Option nur dann im ActionBar an wenn noch ausreichend Platz vorhanden ist. Ansonsten ist sie nur sichtbar wenn der Benutzer das Overflow-Menü anwählt (Listing 3).
Aber Achtung: Die neue ActionBar besitzt gegenüber dem alten Optionenmenü einen gravierenden Unterschied – sie ist immer sichtbar. Während ohne ActionBar nach Drücken des Menüknopfes zunächst das Menü geladen (onCreateOptionsMenu()
) und dann vorbereitet (onPrepareOptionsMenu()
) wird, erfolgt dies für die permanent sichtbaren ActionBar-Einträge nicht mehr. Das dynamische Ein- und Ausblenden von Menüeinträgen muss der Entwickler also explizit vornehmen. Es reicht beim ActionBar nicht, ausschließlich passiv auf die beiden Callbacks zu warten.
Listing 4
Alt vs. neu – das Laden des Menüs
// Alt @Override public boolean onPrepareOptionsMenu(Menu menu) { // Dynamische Anpassungen return super.onPrepareOptionsMenu(menu); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); MenuInflater menuInflater = new MenuInflater(this); menuInflater.inflate(R.menu.activitydetails, menu); return true; } // Neu setHasOptionsMenu(true); @Override public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) { super.onCreateOptionsMenu(menu, menuInflater); menu.clear(); menuInflater.inflate(R.menu.fragmentdetails, menu); }
Die Preferences
Auch bei den Einstellungen gibt es passend zum neuen Fragment-Paradigma eine neue Klasse PreferenceFragment
. Getreu dem Motto "Alles wird re-usable" kann die bislang auf einen Screen ausgerichtete PreferenceActivity nun ebenfalls in einzelne Blöcke zerschnitten werden. Diese einzelnen UI Teile lassen sich dann wieder nach Belieben zusammenführen.
Um dieses neue Schema vollständig auszunutzen, bedarf es zusätzlich sogenannter preference-header
. Jeder dieser Header repräsentiert ein eigenes Fragment, das sich um seine eigenen Einstellungen kümmert. Durch die Nutzung dieser Header entsteht das attraktive, von aktuellen Tablet-Apps gewohnte, Aussehen (Abbildung 1).
Im Beispiel in Listing 5 lädt die ActivityPreference die einzelnen Header aus einer XML-Datei.
Listing 5
PreferenceHeader laden
// Activity @Override public void onBuildHeaders(List<Header> target) { loadHeadersFromResource(R.layout.activitypreference, target); } // XML Ressource <preference-headers xmlns:android="http://schemas.android.com/apk/res/android"> <header android:fragment="de.asltd.androiduser.a4.FragmentPreference1" android:title="@string/txt_header1_short" android:summary="@string/txt_header1_long" /> <header android:fragment="de.asltd.androiduser.a4.FragmentPreference2" android:title="@string/txt_header2_short" android:summary="@string/txt_header2_long" /> </preference-headers>
Dies löst das Laden der beiden Fragmente aus die wiederum ihre eigenen Layouts selbst aus den passenden XML Ressourcen laden (fragmentpreference1.xml
, etc.).
Diese Aufteilung hat natürlich einen Nachteil. Drei verschiedene Code-Bestandteile kümmern sich um Teile des Ganzen. Im Beispiel wird das gelöst, indem die ActivityPreference die Anlaufstelle für alle Abfragen bleibt. Die Prüfung, ob eine bestimmte Checkbox aktiviert ist, erfolgt nach wie vor über die Activity, von der wiederum diese Abfrage an das jeweilige Fragment weitergeleitet wird, wie Listing 6 zeigt.
Listing 6
Preferences abfragen
// Activity public static boolean isItem1() { return FragmentPreference1.isItem1(); } // Fragment public static boolean isItem1() { return item1; }
Fazit
Mit wenigen, genauer gesagt drei, minimalen Eingriffen macht man aus einer alten Phone-App optisch eine neue App. Eine Tablet-App ist diese dann aber noch lange nicht. Die in diesem Workshop aufgebaute Tablet-App zeigt viele der neuen Techniken auf. Abschließend hält man eine moderne App in den Händen. Allerdings ist der Umbau von einer reinen Phone-App hin zu einer Tablet-App nicht mit ein paar kleineren Änderungen erledigt. In diesem Workshop haben wir zum Beispiel das Datenbanksystem nicht auf einen ContentProvider umgestellt. Würde man diese Änderung noch zusätzlich durchführen, dann wäre letztendlich kein Stein mehr auf dem anderen. Es lohnt sich also eventuell, "from scratch" zu beginnen.
Das aktuelle Android-SDK unterstützt alle Geräteklassen: Egal, ob Telefon oder Tablet, jeder Entwickler ist in der Lage Apps gezielt für eine bestimmte Geräteklasse zu produzieren. Aber auch kombinierte Apps sind mit Hilfe des statisch einzubindenden Compatibility-Packs möglich.
Leider hat Google den existierenden Android-Klassenbaum nicht einfach nur erweitert sondern gleichzeitig erheblich modifiziert. Die Anzahl der nicht mehr unterstützten, schlichtweg "deprecated" Klassen und Methoden ist immens. Selbst einfachste Phone-Anwendungen sind beim Wechsel hin zur Android-4App davon betroffen. Auf Grund der möglichen Komplexität würde sich aber eher eine getrennte Entwicklung für diese unterschiedlichen Geräteklassen anbieten. Gemeinsame Funktionen werden hierzu in Android-Library-Projekte ausgelagert und zwei getrennte Projekte präsentieren optimal auf das Zielgerät angepasste Apps.
Fortsetzung folgt??
Google legt bei der Entwicklung von Android ein ungeheures Tempo vor. Zusätzlich gibt es noch die Contributions, die von freien Entwicklern beigesteuerten Änderungswünsche am Android System. Es ist deshalb zu befürchten, dass die Android-Entwicklung in diesem Tempo weitergehen wird. Als Entwickler hetzt man den Neuerungen hinterher – ohne Google wirklich auf Augenhöhe begegnen zu können. Statt des nächsten Android-Major-Releases wünscht man sich als Entwickler deshalb mit Jelly Bean eher eine Konsolidierungsphase.