Samstag, 18. Februar 2012

AsyncTask und AsyncTaskLoader

Sobald man größere Anwendungen auf der Android-Plattform entwickelt, wird man relativ schnell feststellen, dass sich das System weigert, besonders laufzeitzeit-intensive Aufgaben im UI-Thread abzuarbeiten. Dazu zählen Zugriffe über das Netzwerk, aber auch auf das Filesystem, auf Preferences oder auf die Datenbank. Abhängig vom API-Level führt das zu einer entsprechenden Meldung („Application not responding“) oder sogar zu einer Exception.

Die entsprechende Aufgabe ist also in einen asynchronen Task auszulagern. Nicht selten wird so eine Aufgabe nach erfolgreicher Abarbeitung aber wieder auf das UI zugreifen müssen, beispielsweise um die Ergebnisse darzustellen. Da von diesem Task nicht wieder auf den UI-Thread zugegriffen werden darf, ist die Synchronisation Teil des Problems.

Die für einen Java-Entwickler vertraute Herangehensweise wäre, einfach einen Java-Thread zu starten. Aus diesem Thread kann allerdings nicht auf die UI zugegriffen werden. Dafür stellt die Android-Activity eine „runOnThread“-Methode zur Verfügung. Dieser wird ein Runnable übergeben, dass die Aufgaben im UI-Thread erledigt. Schematisch würde das so aussehen:


Thread thread = new Thread(new Runnable() {
   public void run() {
      // ...
      // do long running tasks here
      // …

      // running in Activity context
      runOnUiThread(new Runnable() {
         public void run() {
            // ...
            // update UI here
            // ...
         }
      });
   }
});
thread.start();


Ein wenig eleganter und mehr „Android-like“ ist die Verwendung des "AsyncTask<>". Davon wird eine Subklasse gebildet und einige Methoden sind zu implementieren. Über die Template-Parameter werden die Parameter des „doInBackground“-Aufrufs, der Parameter der „onProgressUpdate“-Methode und der Typ des Rückgabewertes für die „onPostExecute“-Methode parametriert. Letztere wird dann im Kontext des UI-Threads aufgerufen.


public void useTask() {
   new FooTask().execute("param1", "param2");
}

class FooTask extends AsyncTask {
   protected String doInBackground(String... params) {
      int progress = 0;
      String result = params.length > 0 ? params[0] : "";
      // ...
      // do long running tasks here
      // ...
      publishProgress(progress);
      // ...
      // work
      // ...
      return result;
   }
   protected void onProgressUpdate(Integer... progress) {
      // ...
      // propagate progress to UI
      //
   }
   protected void onPostExecute(String result) {
      // ...
      // update UI here
      // ...
   }
}


Eine weitere Möglichkeit zur Lösung des Problems würde für die Aufgabe einen speziellen Service implementieren. Die Kommunikation zwischen Service und UI-Thread würde dann allerdings über Intents/BroadcastReceiver erfolgen, was die Implementierung recht komplex und unübersichtlich macht.

Alle diese Vorgehensweisen haben den Nachteil, dass berücksichtigt werden muss, ob die Activity vorzeitig beendet wird. Das kann ja beispielsweise schon durch Drehen des Devices schnell mal der Fall sein. Dann würde die ausgelagerte Aufgabe in ihrem Thread weiterlaufen. Die Verarbeitung der Ergebnisse führt dann schnell zu unerwünschten Ergebnissen.

Ab Android 3.0, API-Level 11 kommt mit dem „Loader“-Konzept eine neue Möglichkeit ins Spiel. Unter Verwendung der V4-Support-Library ist das allerdings auch für frühere API-Level verwendbar. Loader werden von Android durch einen Loader-Manager verwaltet, der beispielsweise auch den erwähnten Fall des vorzeitigen Beendens der Activity berücksichtigen soll. Der Name deutet es bereits an, der Loader ist speziell für solche Fälle vorgesehen, wo durch Zugriff auf (externe) Ressourcen Daten geladen werden sollen. Für den beschriebenen Fall ist speziell der "AsyncTaskLoader<>" interessant.

Es wird wieder eine Subklasse gebildet, mit dem Typ des Returnwertes der Ladeoperation als Parameter des Templates. Die verwendende Klasse sollte das Interface "LoaderManager.LoaderCallbacks<>" implementieren, dass auch die Methoden für den Callback definiert. Außerdem ist dort eine „onCreateLoader“-Methode vorgesehen, über die abhängig von einem Parameter der passende Loader instantiiert werden muss. Die eigentliche Instanz wird nur über den LoaderManager erzeugt.

Der eigentliche Loader muss nur die „loadInBackground“-Methode implementieren. Parameter können der Loader-Klasse über den Konstruktor übergeben werden. Schematisch sieht das dann folgendermaßen aus:


class FooLoader extends AsyncTaskLoader {
   public FooLoader(Context context, Bundle args) {
      super(context);
      // do some initializations here
   }
   public String loadInBackground() {
      String result = "";
      // ...
      // do long running tasks here
      // ...
      return result;
   }
}

class FooLoaderClient implements LoaderManager.LoaderCallbacks {
   Activity context;
   // to be used for support library:
   // FragmentActivity context2;
   public Loader onCreateLoader(int id, Bundle args) {
      // init loader depending on id
      return new FooLoader(context, args);
   }
   public void onLoadFinished(Loader loader, String data) {
      // ...
      // update UI here
      //
   }
   public void onLoaderReset(Loader loader) {
      // ...
   }
   public void useLoader() {
      Bundle args = new Bundle();
      // ...
      // fill in args
      // ...
      Loader loader =
         context.getLoaderManager().initLoader(0, args, this);
      // with support library:
      // Loader loader =
      //    context2.getSupportLoaderManager().initLoader(0, args, this);
      // call forceLoad() to start processing
      loader.forceLoad();
   }
}


Welche der beschriebenen Möglichkeiten gewählt wird, hängt natürlich nicht zuletzt vom jeweiligen Kontext ab. Der Loader-Mechanismus ist in einigen Fällen vielleicht zu unflexibel, da er speziell für das Laden von Daten vorgesehen ist. Da er aber auf einem universellen Konzept von Android aufsetzt, wird er sicherlich zukünftig vermehrt in den Fokus rücken.

Keine Kommentare:

Kommentar veröffentlichen