Programmier Tutorial - Apps für Android entwickeln -Hintergrund-Thread in Android mit AsyncTask

Programmier Tutorial: Apps für Android entwickeln – Teil 7: Hintergrund-Thread in Android mit AsyncTask


Daher ist es notwendig in unsere Android App einen Nebenthread einzufügen. Dies muss nicht zwangsläufig mit Hilfe der Klasse AsyncTask erfolgen. Android stellt für solche Bedürfnisse auch andere Werkzeuge zur Verfügung.

In unserer jetzigen App-Entwicklungsstufe ist das Erzeugen eines Nebenthreads mittels AsyncTask jedoch eine hervorragende Wahl.

Nun wünschen wir euch viel Spaß bei dem siebten Teil unseres großen Android Tutorials. Los geht’s!

1. Prozesse und Threads in Android

Prozesse und Threads spielen in dem Android System eine entscheidende Rolle. In diesem Abschnitt werden wir die Theorie hinter Prozessen und Threads in Android näher kennenlernen.

1.1 Prozesse in Android

Wenn in Android eine App-Komponente gestartet wird und keine anderen Komponenten dieser App bereits ausgeführt werden, startet das Android System einen neuen Linux Prozess für diese Anwendung mit einem einzelnen Ausführungsthread (thread of execution).

Standardmäßig werden alle Komponenten einer App in dem gleichen Prozess und Thread, auch Main-Thread genannt, ausgeführt.

Wenn eine weitere Komponente einer bereits ausgeführten App gestartet wird, dann wird diese neue Komponente innerhalb des App-Prozesses ausgeführt und benutzt den gleichen Ausführungsthread.

Es ist jedoch möglich verschiedene Komponenten einer App in separaten Prozessen auszuführen, sowie für jeden Prozess zusätzliche Threads zu erzeugen. Dies sollte man aber nur tun, wenn es für die Anwendung erforderlich ist. Die meisten Apps sollten mit den Standardeinstellungen ausgeführt werden.

Für Android Prozesse sind fünf Prioritäts-Level definiert. Die folgende Liste enthält die verschiedenen Prozessarten nach Wichtigkeit sortiert:

  1. Vordergrund Prozess – Ein Prozess der benötigt wird, für das was der Benutzer gerade macht.
  2. Sichtbarer Prozess – Ein Prozess der keine Vordergrundkomponenten besitzt, jedoch Auswirkungen darauf hat was der Benutzer sieht. Dies könnte bspw. der Hintergrund sein, wenn ein kleiner Dialog in den Vordergrund rückt.
  3. Service Prozess – Ein Service ist nicht direkt für den Benutzer sichtbar. Durch einen Service werden Aufgaben ausgeführt, die den Benutzer betreffen, bspw. Musik im Hintergrund abspielen oder Netzwerkdaten laden.
  4. Hintergrund Prozess – Ein Prozess einer momentan nicht sichtbaren Activity, bei der die onStop() Methode aufgerufen wurde.
  5. Leerer Prozess – Ein Prozess der keine Komponenten einer Anwendung mehr hält. Er wird nur aufgrund von Caching-Gründen am Leben (alive) gehalten, um die Startzeit der App zu verbessern.

Das Android System versucht die angelegten Prozesse so lange wie möglich auszuführen. Einige alte Prozesse müssen aber von Zeit zu Zeit gelöscht werden, um Speicher frei zu räumen für neue oder wichtigere Prozesse.

Die Prozesse werden von Android nach ihrer Wichtigkeit geschlossen. Prozesse mit der niedrigsten Wichtigkeit werden zuerst gelöscht, solange bis wieder genügend Ressourcen freigegeben wurden.

1.2 Threads in Android

Wenn eine Anwendung gestartet wird, erzeugt das Android System einen Ausführungsthread, der auch Main-Thread genannt wird. Der Main-Thread ist sehr wichtig, da er für das Versenden von Events an die entsprechenden User Interface Widgets verantwortlich ist.

Außerdem interagiert die eigene Anwendung mit Komponenten des Android User Interface Toolkits, wie den Komponenten aus dem android.widget und android.view Paketen. Aufgrund dieser Eigenschaften wird der Main-Thread auch manchmal als UI-Thread bezeichnet.

Android erzeugt nicht für jede Instanz einer Komponente einen eigenen Thread. Alle Komponenten, die in dem gleichen Prozess ausgeführt werden, teilen sich den UI-Thread. Außerdem werden system calls zu jeder Komponente aus dem UI-Thread heraus versendet. Daher werden alle Methoden die auf system callbacks antworten (wie onKeyDown() für Benutzereingaben), immer auch in diesem Thread ausgeführt.

Wenn ein Nutzer einen Button klickt, dann sendet der UI-Thread der App ein touch event an das Widget. Das Widget setzt seinen Zustand auf pressed und legt ein invalidate request in die event queue. Der UI-Thread arbeitet die event queue und somit den invalidate request ab und teilt dem Widget mit, sich neu zu zeichnen.

Diese Vorgehensweise führt zu einem ernsthaften Problem, sobald eine Anwendung sehr zeitintensive Aufgaben (wie Netzwerkzugriffe oder Datenbankanfragen) in dem UI-Thread ausführt. Dies würde den UI-Thread nämlich blockieren und es könnten keine Events mehr versendet werden.

Auf Benutzereingaben würde die App nicht mehr reagieren und für den Benutzer wie abgestürzt erscheinen. Wird der UI-Thread für mehr als einige Sekunden blockiert (ca. 5 Sekunden), dann wird der Application not responding-Dialog angezeigt. Dies würde den Benutzer enttäuschen, so dass er im schlimmsten Fall die App deinstalliert. Und das sollte unbedingt vermieden werden.

Daher sollten beim Umgang mit Threads zwei einfache Regeln stets befolgt werden:

  1. Niemals den UI-Thread (Main-Thread) blockieren.
  2. Niemals auf das Android User Interface Toolkit außerhalb vom UI-Thread zugreifen.

1.3 Hintergrund- bzw. Arbeitsthread (worker thread) in Android

Wegen dem oben beschriebenen single thread model von Android, ist es für die flüssige Bedienbarkeit unserer App notwendig, den UI-Thread nicht zu blockieren.

Da wir Online-Aktiendaten abfragen möchten und diese Abfrage nicht sofort abgeschlossen werden kann, müssen wir diese Operation in einem eigenen Thread durchführen. Und zwar in einem eigenen Hintergrund- bzw. Arbeitsthread („background“ bzw. „worker“ thread).

In Android gibt es mit AsyncTask eine Klasse, die sich speziell diesem Problem widmet. Mit AsyncTask können wir asynchrone Aufgaben bearbeiten lassen, ohne dabei den UI-Thread zu blockieren. Durch AsyncTask werden die zeitaufwendigen Aufgaben in einem eigenen Arbeitsthread durchgeführt und die Ergebnisse an den UI-Thread zurückgeliefert.

Um einen Arbeitsthread nutzen zu können, muss eine eigene Klasse von der Klasse AsyncTask abgeleitet und in ihr die Methode doInBackground() implementiert werden, die im Pool von Hintergrundthreads ausgeführt wird. Nachdem die Hintergrundberechnung ausgeführt wurde, liefert die Methode onPostExecute() die Ergebnisse von doInBackground() an die UI zurück.

Die Methode onPostExecute() wird aber nicht im Hintergrundthread ausgeführt, sondern in dem UI-Thread. Somit können wir nach Abschluss der Berechnungen unsere Benutzeroberfläche „thread safe“ aktualisieren. Der Hintergrundthread wird mit der Methode execute() aus dem UI-Thread heraus gestartet.

Hinweis: Unter bestimmten Umständen wird der Hintergrundthread aber vorzeitig beendet. Wird bspw. die Bildschirmausrichtung von dem Benutzer durch Drehen des Android Geräts verändert, startet die Activity neu. Dies sollte unbedingt berücksichtigt werden, wenn man mit AsyncTask arbeitet.

Weitere Infos zu Prozessen und Threads in Android findet ihr hier: http://developer.android.com/guide/components/processes-and-threads.html

2. Die Android Klasse AsyncTask

Wie bereits in dem vorherigen Abschnitt kurz beschrieben, ermöglicht die Klasse AsyncTask zeitaufwendige Hintergrundaufgaben in einem Arbeitsthread auszuführen. Der Arbeitsthread wird dabei von dem UI-Thread aus gestartet und liefert nach Abschluss der Berechnung das Ergebnis an den UI-Thread zurück.

Die Klasse AsyncTask ist als eine Helper-Klasse konzipiert, die kurze asynchrone Berechnungen auf einem Nebenthread ausführt. Für lange Berechnungen ist AsyncTask nicht geeignet, dafür stellt Android mächtigere und komplexere Werkzeuge zur Verfügung, diese sind im Paket java.util.concurrent enthalten.

Schauen wir uns einen asynchronen Task nun etwas genauer an. Ein asynchroner Task wird definiert als eine Berechnung die in einem Hintergrundthread ausgeführt und deren Ergebnis an den Main-Thread weitergegeben wird.

In Android besteht jeder asynchrone Task aus drei generischen Datentypen, diese sind:

  • Params – Der Datentyp der Parameter, die dem Task zu Beginn der Berechnung übergeben werden.
  • Progress – Der Datentyp der Fortschrittseinheiten (progress units) für die Ausgabe von Statusangaben während der Hintergrundberechnung.
  • Result – Der Datentyp des Ergebnisses nach Abschluss der Hintergrundberechnung.

Werden nicht alle Datentypen für die Hintergrundberechnung benötigt, kann Void als Datentyp verwendet werden.

Wenn ein asynchroner Task ausgeführt wird, werden die folgenden vier Schritte durchlaufen:

  1. onPreExecute() – Wird von dem UI-Thread aus aufgerufen, bevor der Task ausgeführt wird. Dieser Schritt wird für die Vorbereitung genutzt, um bspw. einen Fortschrittsbalken in der Benutzeroberfläche anzuzeigen.
  2. doInBackground(Params…) – Wird von Hintergrundthread aus aufgerufen, unmittelbar nachdem onPreExecute() abgeschlossen wurde. Hierin soll die zeitaufwendige Hintergrundberechnung durchgeführt werden. Die Parameter des asynchronen Tasks werden an diese Stelle übergeben. Das Berechnungsergebnis muss zurückgegeben werden und wird an onPostExecute(Result) weitergereicht. Außerdem können von hier Statuswerte, die über den Berechnungsfortschritt berichten, gesendet werden. Dazu wird die Methode publishProgress(Progress…) aufgerufen. Die Werte werden innerhalb des UI-Threads veröffentlicht, mit Hilfe von onProgressUpdate(Progress…).
  3. onProgressUpdate(Progress…) – Wird von dem UI-Thread aus aufgerufen, nachdem publishProgress(Progress…) in doInBackground(Params…) aufgerufen wurde. Der Ausführungszeitpunkt ist jedoch unbestimmt. Diese Methode wird verwendet, um den Benutzer über den Fortschritt der Hintergrundberechnung zu informieren, während die Berechnung noch im Hintergrund läuft. Die Anzeige kann als Fortschrittsbalken oder Textmeldungen erfolgen.
  4. onPostExecute(Result) – Wird von dem UI-Thread aus aufgerufen, nachdem die Hintergrundberechnung abgeschlossen wurde. Das Ergebnis der Hintergrundberechnung wird als Parameter übergeben.

Um einen asynchronen Task verwenden zu können, muss eine eigene Klasse von der Klasse AsyncTask abgeleitet werden und in ihr die Methoden doInBackground(Params…) und optional onPostExecute(Result) und onProgressUpdate(Integer… progress) überschrieben werden.

Folgendes Codebeispiel zeigt beispielhaft wie eine eigene Klasse von der AsyncTask-Klasse abgeleitet wird:

private class LadeVideoTask extends AsyncTask<URL, Integer, Long> {
    
protected Long doInBackground(URL... urls) {
         
       // Führe Download im Hintergrund durch und
       // melde Fortschritt mittels publishProgress(int)

        return totalSize;
    }

    protected void onProgressUpdate(Integer... progress) {
         
        // gebe aktuellen Fortschritt aus
    }

    protected void onPostExecute(Long result) {

        // Task abgeschlossen, Ergebnis kann verwendet werden
    }
}

Der asynchrone Task wird gestartet, indem die Methode execute(Params…) auf einer Instanz der abgeleiteten Klasse aufgerufen wird.

Siehe folgender Quellcode:

LadeVideoTask meinLadeVideoTask = new LadeVideoTask();
meinLadeVideoTask.execute(url1, url2, url3);

2.1 Regeln für die Verwendung von AsyncTask

Bei asynchronen Abläufen kann es zu schwerwiegenden Fehlern kommen, daher sollten bei der Verwendung von AsyncTask die folgenden Regeln eingehalten werden:

  • Die Task-Instanz muss in dem UI-Thread erzeugt werden.
  • Die Methode execute(Params…) muss in dem UI-Thread aufgerufen werden.
  • Die Methoden onPreExecute(), onPostExecute(Result), doInBackground(Params…) und onProgressUpdate(Progress…) dürfen nicht manuell aufgerufen werden.
  • Der Task kann nur einmal ausgeführt werden. Bei Mehrfachausführung wird eine exception erzeugt.

In der Android API könnt ihr weitere Details über die AsyncTask-Klasse finden.

3. Berechnungen im Hintergrund in Android mit AsyncTask durchführen

Jetzt haben wir die Theorie über Prozesse und Threads in Android kennengelernt. Zudem ist uns jetzt auch die Klasse AsyncTask und ihre Funktion bekannt. Daher möchten wir als Nächstes dieses Wissen in der Praxis verwenden und einen asynchronen Task in unsere Android App einfügen.

Mit diesem asynchronen Task werden wir später die Finanzdaten auf unserem Web-Server abfragen. Da diese Online-Abfrage durchaus sehr zeitaufwendig sein kann, darf durch sie nicht der UI-Thread blockiert werden. Wir benutzen daher einen eigenen Arbeitsthread extra für die Abfrage.

Dieser Arbeitsthread soll mit einem Klick auf unseren Aktualisieren-Button im Options Menu gestartet werden. Doch vorher müssen wir erst einmal eine neue Klasse für den Thread anlegen.

3.1 Innere Klasse HoleDatenTask für den asynchronen Task erstellen

Die neue Klasse muss von der Android Klasse AsyncTask abgeleitet sein und in ihr die Methoden doInBackground(), onPostExecute() und onProgressUpdate() überschrieben werden. Außerdem muss die neue Klasse eine innere Klasse unserer AktienlisteFragment-Klasse sein, damit wir auf deren Membervariablen (Klassenvariablen) zugreifen können.

Jetzt folgt der Quellcode der inneren Klasse HoleDatenTask:

// Innere Klasse HoleDatenTask führt den asynchronen Task auf eigenem Arbeitsthread aus
public class HoleDatenTask extends AsyncTask<String, Integer, String[]> {

    private final String LOG_TAG = HoleDatenTask.class.getSimpleName();

    @Override
    protected String[] doInBackground(String... strings) {

        String[] ergebnisArray = new String[20];

        for (int i=0; i < 20; i++) {

            // Den StringArray füllen wir mit Beispieldaten
            ergebnisArray[i] = strings[0] + "_" + (i+1);

            // Alle 5 Elemente geben wir den aktuellen Fortschritt bekannt
            if (i%5 == 4) {
                publishProgress(i+1, 20);
            }

            // Mit Thread.sleep(600) simulieren wir eine Wartezeit von 600 ms
            try {
                Thread.sleep(600);
            }
            catch (Exception e) { Log.e(LOG_TAG, "Error ", e); }
        }

        return ergebnisArray;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {

        // Auf dem Bildschirm geben wir eine Statusmeldung aus, immer wenn
        // publishProgress(int...) in doInBackground(String...) aufgerufen wird
        Toast.makeText(getActivity(), values[0] + " von " + values[1] + " geladen",
                Toast.LENGTH_SHORT).show();

    }

    @Override
    protected void onPostExecute(String[] strings) {

        // Wir löschen den Inhalt des ArrayAdapters und fügen den neuen Inhalt ein
        // Der neue Inhalt ist der Rückgabewert von doInBackground(String...) also
        // der StringArray gefüllt mit Beispieldaten
        if (strings != null) {
            mAktienlisteAdapter.clear();
            for (String aktienString : strings) {
                mAktienlisteAdapter.add(aktienString);
            }
        }

        // Hintergrundberechnungen sind jetzt beendet, darüber informieren wir den Benutzer
        Toast.makeText(getActivity(), "Aktiendaten vollständig geladen!",
                Toast.LENGTH_SHORT).show();
    }
}

Die neue Klasse HoleDatenTask wird an das Ende der Klasse AktienlisteFragment als innere Klasse eingefügt.

Zusätzlich muss die Import-Anweisung import android.os.AsyncTask; im Quelltext oben bei den anderen Import-Anweisungen eingefügt werden.

Weiter unten zeigen wir den gesamten Quellcode der Klasse AktienlisteFragment, so dass ihr alle notwendigen Änderungen korrekt ausführen könnt.

3.2 Asynchronen Task durch Klick auf Aktualisieren-Button starten

Bis jetzt haben wir für den asynchronen Task eine neue, innere Klasse angelegt, die von der Klasse AsyncTask abgeleitet wurde. In der neuen Klasse haben wir die Methoden doInBackground(), onPostExecute() und onProgressUpdate() überschrieben, so dass die HoleDatenTask-Klasse jetzt unsere Berechnungen im Hintergrund ausführen kann.

Damit sie dies tut, müssen wir eine Instanz von ihr erzeugen und anschließend den Startbefehl mit der Methode execute(Params…) geben. Dies werden wir in der AktienlisteFragment-Klasse vornehmen. Und zwar in der Methode onOptionsItemSelected(MenuItem item) mit der wir prüfen, ob ein Eintrag unseres Options Menus ausgewählt wurde, wie bspw. der Aktualisieren-Button.

Mit dem folgenden Quellcode prüfen wir, ob ein Menüeintrag des Options Menus ausgewählt wurde und starten den asynchronen Task falls der Aktualisieren-Button gedrückt wurde.

public boolean onOptionsItemSelected(MenuItem item) {
    // Wir prüfen, ob Menü-Element mit der ID "action_daten_aktualisieren"
    // ausgewählt wurde und geben eine Meldung aus
    int id = item.getItemId();
    if (id == R.id.action_daten_aktualisieren) {

        // Erzeugen einer Instanz von HoleDatenTask und starten des asynchronen Tasks
        HoleDatenTask holeDatenTask = new HoleDatenTask();
        holeDatenTask.execute("Aktie");

        // Den Benutzer informieren, dass neue Aktiendaten im Hintergrund abgefragt werden
        Toast.makeText(getActivity(), "Aktiendaten werden abgefragt!",
                Toast.LENGTH_SHORT).show();

        return true;
    }
    return super.onOptionsItemSelected(item);
}

Mit den Zeilen 8 und 9 erzeugen wir eine Instanz unserer HoleDatenTask-Klasse und starten den asynchronen Task mit holeDatenTask.execute("Aktie");. Dabei übergeben wir als Parameter einen String. Dieser String wird an die Hintergrundthread-Methode doInBackground(String…) übergeben, die dann mit den Berechnungen im eigenen Arbeitsthread beginnt.

3.3 Den ArrayAdapter zu einer Membervariable der äußeren Klasse AktienlisteFragment machen

Da die innere Klasse HoleDatenTask auf den ArrayAdapter zugreift, müssen wir diesen zu einer Membervariable der äußeren Klasse AktienlisteFragment machen. Dazu fügen wir den folgenden Code direkt am Anfang der AktienlisteFragment-Klasse ein:

// Der ArrayAdapter ist jetzt eine Membervariable der Klasse AktienlisteFragment
ArrayAdapter<String> mAktienlisteAdapter;

Da wir jetzt den ArrayAdapter als Membervariable zu Beginn der AktienlisteFragment-Klasse deklarieren, müssen wir jetzt noch die vorherige Deklaration rückgängig machen.

Dazu ändern wir in der Methode onCreateView alle Stellen an denen der ArrayAdapter verwendet wird, entsprechend der gelben Markierungen:

mAktienlisteAdapter =
        new ArrayAdapter<>(
                getActivity(), // Die aktuelle Umgebung (diese Activity)
                R.layout.list_item_aktienliste, // ID der XML-Layout Datei
                R.id.list_item_aktienliste_textview, // ID des TextViews
                aktienListe); // Beispieldaten in einer ArrayList


View rootView = inflater.inflate(R.layout.fragment_aktienliste, container, false);

ListView aktienlisteListView = (ListView) rootView.findViewById(R.id.listview_aktienliste);
aktienlisteListView.setAdapter(mAktienlisteAdapter);

Jetzt haben wir alle notwendigen Änderungen an unserem Projekt vorgenommen und können nun einen asynchronen Task mit einem Klick auf den Aktualisieren-Button starten.

3.4 Der gesamte Quellcode der Klasse AktienlisteFragment

Zur Übersicht haben wir den gesamten Quellcode der Klasse AktienlisteFragment mit ihrer inneren Klasse HoleDatenTask in dem folgenden Akkordion abgelegt.

Außerdem könnt ihr euch auch den gesamten Quelltext der Klasse AktienlisteFragment.java herunterladen und noch einmal in Ruhe anschauen:

AktienlisteFragment.java (als ZIP-Datei gepackt)

Wenn ihr den Quellcode nicht benötigt, könnt ihr das Akkordion zusammenklappen. Die gelb markierten Zeilen wurden in diesem Teil des Android Tutorials hinzugefügt bzw. verändert.

AktienlisteFragment.java

package de.programmierenlernenhq.aktiehq.app;

import android.os.AsyncTask;
import android.support.v4.app.Fragment;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;

import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;

import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;


public class AktienlisteFragment extends Fragment {

    // Der ArrayAdapter ist jetzt eine Membervariable der Klasse AktienlisteFragment
    ArrayAdapter<String> mAktienlisteAdapter;

    public AktienlisteFragment() {    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Menü bekannt geben, dadurch kann unser Fragment Menü-Events verarbeiten
        setHasOptionsMenu(true);
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        inflater.inflate(R.menu.menu_aktienlistefragment, menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Wir prüfen, ob Menü-Element mit der ID "action_daten_aktualisieren"
        // ausgewählt wurde und geben eine Meldung aus
        int id = item.getItemId();
        if (id == R.id.action_daten_aktualisieren) {

            // Erzeugen einer Instanz von HoleDatenTask und starten des asynchronen Tasks
            HoleDatenTask holeDatenTask = new HoleDatenTask();
            holeDatenTask.execute("Aktie");

            // Den Benutzer informieren, dass neue Aktiendaten im Hintergrund abgefragt werden
            Toast.makeText(getActivity(), "Aktiendaten werden abgefragt!",
                    Toast.LENGTH_SHORT).show();

            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {

        String LOG_TAG = AktienlisteFragment.class.getSimpleName();

        Log.v(LOG_TAG, "verbose     - Meldung");
        Log.d(LOG_TAG, "debug       - Meldung");
        Log.i(LOG_TAG, "information - Meldung");
        Log.w(LOG_TAG, "warning     - Meldung");
        Log.e(LOG_TAG, "error       - Meldung");

        String [] aktienlisteArray = {
                "Adidas - Kurs: 73,45 €",
                "Allianz - Kurs: 145,12 €",
                "BASF - Kurs: 84,27 €",
                "Bayer - Kurs: 128,60 €",
                "Beiersdorf - Kurs: 80,55 €",
                "BMW St. - Kurs: 104,11 €",
                "Commerzbank - Kurs: 12,47 €",
                "Continental - Kurs: 209,94 €",
                "Daimler - Kurs: 84,33 €"
        };

        List<String> aktienListe = new ArrayList<>(Arrays.asList(aktienlisteArray));

        mAktienlisteAdapter =
                new ArrayAdapter<>(
                        getActivity(), // Die aktuelle Umgebung (diese Activity)
                        R.layout.list_item_aktienliste, // ID der XML-Layout Datei
                        R.id.list_item_aktienliste_textview, // ID des TextViews
                        aktienListe); // Beispieldaten in einer ArrayList


        View rootView = inflater.inflate(R.layout.fragment_aktienliste, container, false);

        ListView aktienlisteListView = (ListView) rootView.findViewById(R.id.listview_aktienliste);
        aktienlisteListView.setAdapter(mAktienlisteAdapter);

        return rootView;
    }

    // Innere Klasse HoleDatenTask führt den asynchronen Task auf eigenem Arbeitsthread aus
    public class HoleDatenTask extends AsyncTask<String, Integer, String[]> {

        private final String LOG_TAG = HoleDatenTask.class.getSimpleName();

        @Override
        protected String[] doInBackground(String... strings) {

            String[] ergebnisArray = new String[20];

            for (int i=0; i < 20; i++) {

                // Den StringArray füllen wir mit Beispieldaten
                ergebnisArray[i] = strings[0] + "_" + (i+1);

                // Alle 5 Elemente geben wir den aktuellen Fortschritt bekannt
                if (i%5 == 4) {
                    publishProgress(i+1, 20);
                }

                // Mit Thread.sleep(600) simulieren wir eine Wartezeit von 600 ms
                try {
                    Thread.sleep(600);
                }
                catch (Exception e) { Log.e(LOG_TAG, "Error ", e); }
            }

            return ergebnisArray;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {

            // Auf dem Bildschirm geben wir eine Statusmeldung aus, immer wenn
            // publishProgress(int...) in doInBackground(String...) aufgerufen wird
            Toast.makeText(getActivity(), values[0] + " von " + values[1] + " geladen",
                    Toast.LENGTH_SHORT).show();

        }

        @Override
        protected void onPostExecute(String[] strings) {

            // Wir löschen den Inhalt des ArrayAdapters und fügen den neuen Inhalt ein
            // Der neue Inhalt ist der Rückgabewert von doInBackground(String...) also
            // der StringArray gefüllt mit Beispieldaten
            if (strings != null) {
                mAktienlisteAdapter.clear();
                for (String aktienString : strings) {
                    mAktienlisteAdapter.add(aktienString);
                }
            }

            // Hintergrundberechnungen sind jetzt beendet, darüber informieren wir den Benutzer
            Toast.makeText(getActivity(), "Aktiendaten vollständig geladen!",
                    Toast.LENGTH_SHORT).show();
        }
    }
}

Als Nächstes werden wir unseren asynchronen Task auf dem angeschlossenen Android Gerät testen. Dazu muss das Android Smartphone oder Tablet entsprechend der Anleitung in Teil 3 des Tutorials mit dem Rechner verbunden worden sein.

4. Android App ausführen – Testen des asynchronen Tasks

Jetzt wollen wir unsere App ausführen und den asynchronen Task testen.

Damit wir unsere App auf dem Smartphone oder Tablet starten können, müssen alle Schritte von Teil 3 des Android Tutorials befolgt worden sein.

Zuerst schließen wir unser Android Gerät an den PC an und stellen eine Verbindung über die ADB (Android Debug Bridge) her. Danach klicken wir auf das Run 'app'-Symbol. Unser Android Projekt wird dadurch neu erstellt und auf dem angeschlossenen Gerät ausgeführt.

Das Run 'app'-Symbol befindet sich in der oberen Menüleiste, siehe folgende Abbildung:

android studio project avd run

Android App über das Run App-Symbol starten

Nach einigen Momenten öffnet sich der Select Deployment Target-Dialog. In ihm nehmen wir die folgenden Einstellungen vor:

  1. Das angeschlossene Android Gerät unter Connected Devices auswählen.
  2. Mit einem Klick auf den OK-Button die Installation unserer App auf das Gerät starten.
android device run app

Auswählen des angeschlossenen Android Geräts zum Aufspielen unserer App

Der Dialog schließt sich und unsere Android App wird auf das angeschlossene Gerät übertragen und installiert. Die Installation dauert nur einen kurzen Augenblick und verläuft fast unbemerkt im Hintergrund. Danach wird unsere App automatisch gestartet.

4.1 Den asynchronen Task unsere Android App testen

In der App klicken wir auf den Overflow Menu-Button, wodurch sich das Overflow Menu öffnet. Hier klicken wir auf unseren Aktualisieren-Button und starten dadurch den asynchronen Task. Jetzt wird in Android ein Arbeitsthread angelegt, in dem die Aktiendaten später geladen werden.

In der folgenden Abbildung ist zu sehen, wie unsere App den aktuellen Fortschritt an den Benutzer meldet:

android asynctask

Der asynchrone Task lädt im Hintergrund die Beispieldaten und informiert über den Fortschritt

Zu Beginn (blaue Markierung A) sind noch die Start-Aktiendaten geladen. Nachdem der asynchrone Task die Beispieldaten im Hintergrund geladen hat, werden die neuen Aktiendaten (blaue Markierung B) im ListView angezeigt. Tipp: Zum Vergrößern einfach auf die Abbildung klicken.

4.2 Video – Den AsyncTask unser Android Anwendung testen

Auf den Bildern kann man schwer erkennen, wie unsere Android App sich auf dem Android Gerät verhält. Daher haben wir zusätzlich ein kleines Video von der App erstellt, in welchem ihr sehen könnt was auf dem Bildschirm tatsächlich passiert.

Das folgende Video zeigt wie unsere App die Beispieldaten mit Hilfe des asynchronen Tasks lädt:

Natürlich sind die angezeigten Aktiendaten bisher noch Beispieldaten. Wir kommen den echten Online-Finanzdaten aber immer näher. Mit dem hier getesteten asynchronen Task können wir im nächsten Teil des Tutorials die Finanzdaten online anfragen.

Zusammenfassung

In diesem Teil unseres großen Android Tutorials haben wir beschrieben, wie Prozesse und Threads in Android behandelt werden. Außerdem haben wir die Klasse AsyncTask vorgestellt, die wir für zeitaufwendige Hintergrundberechnungen nutzen werden.

Wir haben einen asynchronen Task in unsere App eingefügt, der in einem Arbeitsthread simulierte Aktiendaten abgefragt hat. Dazu haben wir einen neue, innere Klasse erstellt und diese von der AsyncTask-Klasse abgeleitet. Zudem haben wir die Methoden doInBackground(), onPostExecute() und onProgressUpdate() überschrieben und unseren asynchronen Task dadurch definiert.

Abschließend haben wir unsere Android Anwendung auf dem Smartphone ausgeführt und auf Funktion geprüft.

In den nächsten Teilen unseres Android Tutorials werden wir unsere App um neue Funktionen erweitern, so dass sie schließlich simulierte Online-Finanzdaten abfragt und in dem ListView anzeigt. Um dies zu erreichen, werden wir schrittweise vorgehen. Der nächste Schritt wird sein, von unserer App aus eine Anfrage an unseren Web-Server abzuschicken und die gelieferten Aktiendaten in Logcat zu überprüfen.



Comments 22

  1. Pingback: Android Tutorial: Options Menu und Action Bar in Android

  2. Pingback: Android Tutorial: Daten von einer Webseite abfragen in Android

  3. Pingback: Android Tutorial: XML-Daten auslesen und anzeigen in Android

  4. Pingback: Android Tutorial: Activities und Intents in Android

  5. Pingback: Android Tutorial: Das SwipeRefreshLayout in Android

  6. Auch die Schritte dieses Kapitels funktionieren wie beschrieben auf dem Emulator, aber nicht auf meinem angeschlossenen Smartphone.
    Habe die AktieHQ-App mal deinstalliert und wollte dann nochmal mit „Run app“ neu installieren, aber das funktioniert nicht.
    Also seit dem letzten Kapitel wird die App nicht mehr auf mein Gerät übertragen, obwohl das Gerät von adb.exe bzw. dem Gerätemanager bzw. Android Studio einwandfrei erkannt wird.
    Wo könnte es haken?

    1. Habe folgende Fehlermeldung gefunden:
      „DeviceMonitor: Adb rejected connection to client ‚1367‘: closed“ und darüber folgende Diskussion bei stackoverflow:
      http://stackoverflow.com/questions/23794089/android-adb-rejected-connection-to-client
      Da start-server/kill-server keine Lösung des Problems war, habe ich den Ansatz von Andy Boot im selben Chat verfolgt und versucht, über Run–>Edit Configurations den Launch String anzupassen. Leider kann ich die Datei AndroidManifest.xml nicht als Default Activity auswählen und erhalte die Fehlermeldung „Activity Class Not Specified“.
      Ok, hierzu die Diskussion
      http://stackoverflow.com/questions/16612548/project-in-android-studio-wont-start-activity-class-not-specified
      bei stackoverflow gefunden, und m4ch3t3 scheint einen Lösungsansatz zu haben, aber wenn ich das Open Module settings-Fenster öffne, kann ich darin nicht wie beschrieben arbeiten, ich finde darin keine Ordner oder irgendwas, das „src“ oder „gen“ heißt, und bei dem ich per Rechtsklick „Source“ auswählen könnte…
      Es scheint mir, als sei ich nah an der Lösung meines Problems mit der App-Übertragung… was mache ich falsch?

      Viele Grüße,
      Daniela

    2. Post
      Author

      Hallo Daniela,

      das Deinstallieren der App sollte ohne Probleme funktionieren. Warum die App danach bei dir nicht mehr zu installieren war, ist mir rätselhaft. Vorher konntest du sie ja installieren. Eventuell ist der USB-Treiber für dein Android Smartphone nicht vollständig kompatibel mit Android Studio.

      Eine Liste der USB-Treiber findest du hier: http://developer.android.com/tools/extras/oem-usb.html#Drivers

      Viele Grüße, Chris

    3. Hallo Daniela,

      dass „AndroidManifest.xml“ nicht als Default Activity auswählbar ist, ergibt sich daraus, dass es schlicht keine Activity ist. Hier muss eine Klasse ausgewählt werden.

      Was den von dir genannten Beitrag bei stackoverflow angeht (ich meine den von „m4ch3t3“): da scheint mir einiges durcheinander zu gehen, was die Bezeichnungen und Abläufe angeht. Oder die von ihm/ihr verwendete Version ist mit der aktuellen nicht verlgeichbar.

      Jedenfalls beruht deine ursprüngliche Fehlermeldung nicht auf einer „plötzlich“ fehlenden Activity. Eher ein plötzlich fehlendes Device. Ich habe mein Smartphone einfach mal aus dem USB-Port entfernt, als ich solch eine Fehlermeldung hatte (evtl. danach kill-server/start-server, weiß ich aber nicht mehr:-( ) und wieder eingesteckt – dann war es auch wieder erreichbar.

  7. Hi, erstmal danke für das ganze Tutorial, echt klasse!

    Es funktioniert alles einwandfrei, jedoch stürzt die App, nachdem ich eine Aktualisierung vornehme immer am Ende ab. Also es aktualisiert sich, der Status wird ausgegeben (5 von 20 und so weiter) und nach dem „20 von 20 geladen“ erscheint, wird die App angehalten und die Meldung vom System „AktieHQ angehalten“ erscheint. Ich habe die Zeit auch von 600 ms auf 200 ms geändert, das hat allerdings auch nichts gebracht.

    Woran kann es liegen?

    Danke 🙂

    1. Hallo Martin,

      da sollte der DDMS weiterhelfen. Irgendwo in den Tiefen des Logs sollte sich ein Hinweis auf den Absturz finden lassen. Riecht spontan danach, dass deine Version von Chris‘ Code mehr Elemente verarbeiten will, als das Array zur Verfügung stellt (also >20 – enthielt dein Code in der Methode „doInBackground“ evtl. die Zeile „for (int i=0; i <= 20; i++) {"? (also "i <= 20" statt "i<20")

    2. Jo – „i <= 20" erzeugt genau die Meldung "AktieHQ angehalten" 😉

    3. Post
      Author

      Hallo Martin,

      woran es liegt, ist ohne deinen Quellcode schwer zu sagen. Wenn du möchtest, kannst du mir deine Projektdateien als ZIP per E-Mail zusenden. Ich werde dann mal drüber schauen, vielleicht kann ich den Fehler finden. Die E-Mail Adresse kannst du im Impressum nachschauen.

      Viele Grüße, Chris

    4. Zu spät, hab mit dem nächsten Tutorial angefangen und hab jetzt den anderen Code drin. Ist aber nicht schlimm, denn es funktioniert momentan wieder alles 😀

      Danke trotzdem! 🙂

  8. Hallo Chris,

    klasse Tutorial. Ich habe endlich den Zusammenhang zwischen (Array)Daten, (Array)Adapter und (List)View verstanden (obwohl ich schon vor über einem Jahr irgendwo ein Beispiel abgekupfert und in einer eigenen App verwendet habe 😀 )

    Danke!

    Gruß, Reinhard

    1. Post
      Author

      Hallo Reinhard,

      danke für die lobenden Worte! Und nochmals vielen Dank für Deine anderen hilfreichen Kommentare in den anderen Beiträgen.

      Es freut mich sehr, wenn die Tutorials verständlich sind. Android kann manchmal ganz schön komplex sein…

      Viele Grüße, Chris

  9. Es tut mir leid, das klingt jetzt echt doof, aber ich bin ein ziemlicher Anfänger, und deine Tutorials sind echt super; Aber ich verstehe nicht recht, wo du die Daten nach der Aktualisierung festlegst, also wo ich den Text für ‚Aktie_1,Aktie_2‘ und so weiter, ändern kann^^

    1. Post
      Author

      Hallo Justin,

      die Daten werden in der Methode onPostExecute() dem ArrayAdapter hinzugefügt. Die onPostExecute() Methode wird automatisch aufgerufen, sobald die Daten mit Hilfe der Methode doInBackground() aus dem Internet geladen wurden.

      Dieser Prozess ist asynchron und läuft im Hintergrund ab.

      Ich hoffe meine Anmerkungen helfen Dir etwas weiter.

      Viele Grüße, Chris

    2. Aktie_1 usw kannst du im Aufruf doInBackground(args) ändern. Dort wird das ErgebnisArray befüllt und mittels postExecute „ausgeben“

      ändere den Code in doInBackground(…)

      —–> ergebnisArray[i] = strings[0] + „_“ + (i+1);

  10. Kenne die Videos vom Rheinwerk Verlag und das hier ist besser! Sehr gute Arbeit.

    Genug des Lobs und zu meiner Frage ;P

    Der Adapter wird in onPostExecute(…) neu befüllt. Ist im Hintergrund der GUI dann irgendein Listener, der weiß dass neuer Content da ist und die ListView refresht werden muss? Sehe sonst nämlich keine Anweisung, damit das geschieht.

    Beste Grüße
    Frank

    1. Post
      Author

      Hallo Frank,

      danke für Dein Lob!

      Die Methode add() informiert den verbundenen View selbständig und dieser aktualisiert sich dann auch von selbst. Das ist standardmäßig so in Android implementiert. Mit der Methode setNotifyOnChange() kann dies für den ArrayAdapter deaktiviert werden. Siehe folgender Link:

      Developer.android.com: ArrayAdapter setNotifyOnChange

      Viele Grüße, Chris

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.