Android Programmierung: AsyncTask mit ProgressDialog (Lösung mit einem Callback)

In diesem Tutorial möchte ich beschreiben wie man eine asynchrone Aufgabe auslagert und parallel dazu einen Dialog anzeigt. Den ProgressDialog habe ich bereits im Tutorial Android, ProgressDialog für lange Operationen erläutert.

Es gibt bereits einige Beispiele dazu im Internet und die Lösungen sind zumeist praktikabel und funktionell. Jedoch sind diese zumeist mit einer inneren Klasse (wie in der offiziellen Dokumentation https://developer.android.com/reference/android/os/AsyncTask) und blähen dadurch den Quellcode unnötig auf. Ich möchte gerne einen anderen Weg gehen und den AsyncTask auslagern und mit einem Callback versehen so halten wir unseren Quellcode schlank und haben immer das Wesentliche im blick.

Projekt erstellen

Für die nachfolgenden Schritte benötigen wir ein einfaches, leeres Android Projekt mit einer Activity (EmptyActivity). 

Gerne möchte ich dir der einfachheithalber ein Download für ein Projekt anbieten welches du dir in Android Studio importieren kannst.

 

Layout erstellen

Für das Ausführen des asynchronen Task benötigen wir eine Schaltfläche und für die Anzeige der Daten  TextView Elemente. Diese Elemente fügen wir über den Designer auf das Layout „activity_main.xml“.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/startBtn"
        android:layout_width="206dp"
        android:layout_height="63dp"
        android:layout_marginStart="16dp"
        android:layout_marginTop="38dp"
        android:text="Starten"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/errorMsgTextView" />

    <TextView
        android:id="@+id/dateinameTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="12dp"
        android:layout_marginTop="85dp"
        android:textColor="@android:color/holo_blue_dark"
        app:layout_constraintStart_toEndOf="@+id/textView4"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/dateigroesseTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="12dp"
        android:layout_marginTop="34dp"
        android:textColor="@android:color/holo_blue_dark"
        app:layout_constraintStart_toEndOf="@+id/textView5"
        app:layout_constraintTop_toBottomOf="@+id/dateinameTextView" />

    <TextView
        android:id="@+id/textView4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:text="Dateiname: "
        android:textColor="@android:color/black"
        app:layout_constraintBaseline_toBaselineOf="@+id/dateinameTextView"
        app:layout_constraintStart_toStartOf="parent" />

    <TextView
        android:id="@+id/textView5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:text="Dateigröße:"
        android:textColor="@android:color/black"
        app:layout_constraintBaseline_toBaselineOf="@+id/dateigroesseTextView"
        app:layout_constraintStart_toStartOf="parent" />

    <TextView
        android:id="@+id/errorMsgTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:text="Fehlermeldung:"
        android:textColor="@android:color/holo_red_light"
        app:layout_constraintBaseline_toBaselineOf="@+id/textView7"
        app:layout_constraintStart_toStartOf="parent" />

    <TextView
        android:id="@+id/textView7"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginTop="29dp"
        android:textColor="@android:color/holo_red_light"
        app:layout_constraintStart_toEndOf="@+id/errorMsgTextView"
        app:layout_constraintTop_toBottomOf="@+id/dateigroesseTextView" />

</android.support.constraint.ConstraintLayout>

Interface ICallback erzeugen

Für die Verarbeitung des Ergebnisses unseres asynchronen Tasks benötigen wir ein Interfaces. Dieses ermöglicht es uns späterer mehrere Implementationen für eventuelle verschiedene Ausführungen zu implementieren.

package de.draegerit.asynctaskcallbackapp;

public interface ICallback {

    void handleResult(Result result);
}

Nun müssen wir uns eine innere Klasse schreiben welche das Interface „ICallback“ implementiert. Mit dem implementieren des Interfaces müssen wir zusätzlich die Methode „handleResult“ implementieren.

class CallbackImpl implements ICallback {
   @Override
   public void handleResult(Result result) {

   }
}
package de.draegerit.asynctaskcallbackapp;

public class Result {

    private int size;
    
    private String filename;
    
    private String message;

    public int getSize() { return size; }
    public void setSize(int size) { this.size = size; }

    public String getMessage() { return message; }
    public void setMessage(String message) { this.message = message; }

    public String getFilename() { return filename; }
    public void setFilename(String filename) { this.filename = filename; }
}

asynchronen Task erstellen

Nun können wir uns eine öffentliche Klasse für den asynchronen Task erstellen welchen wir mit der Schaltfläche starten wollen.

In diesem Beispiel möchte ich eine einfache Datei aus dem Internet laden. Da ich in diesem Tutorial beschreiben möchte wie ein asynchroner Task mit einem Callback ausgestattet werden kann überspringe ich die Erläuterungen für das herunterladen von Dateien.

In dem Kontruktors des asynchronen Task wird der Context und zusätzlich ein Callback übergeben.

package de.draegerit.asynctaskcallbackapp;

import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.util.Log;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.net.URLConnection;

public class RequestAsyncTask extends AsyncTask<Void, Void, Result> {

    //TAG für den Logger
    private static final String TAG = "RequestAsyncTask";

    //Zieldatei
    private static final String ADDRESS = "http://progs.draeger-it.blog/example.file";

    //Schwache Referenz auf den Context
    private WeakReference<Context> contextRef;

    //ProgressDialog für die Fortschrittsanzeige
    private ProgressDialog progressDialog;

    //Das Ergebniss des asynchronen Tasks
    private Result result;

    //Der Callback welcher zum schluss ausgeführt werden soll.
    private ICallback callback;

    //Variable welche gesetzt wird wenn die Schaltfläche "Abbrechen" im ProgressDialog betätigt wird.
    private boolean abortDownload;

    /**
     * Konstruktor
     * @param ctx - der Context
     * @param callback - der Callback welcher zum Schluss ausgeführt werden soll
     */
    public RequestAsyncTask(Context ctx, ICallback callback) {
        contextRef = new WeakReference<>(ctx);
        this.callback = callback;
    }

    @Override
    protected Result doInBackground(Void... voids) {
        result = new Result();
        try {
            //Die Progressbar soll den Fortschritt in Prozent anzeigen.
            progressDialog.setMax(100);

            DataInputStream stream = null;

            //Dateiname generieren
            String filename = String.valueOf(System.currentTimeMillis()).concat(".file");
            result.setFilename(filename);
            //Referenz des Context laden
            Context ctx = contextRef.get();
            try (FileOutputStream outputStream = ctx.openFileOutput(filename, Context.MODE_PRIVATE);){
                File privateFileDirectory = ctx.getFilesDir();
                Log.i(TAG, privateFileDirectory.getAbsolutePath());
                //Aufbau der Verbindung
                URL u = new URL(ADDRESS);
                URLConnection conn = u.openConnection();
                //ermitteln der Dateigröße
                int contentLength = conn.getContentLength();
                //ablegen der Dateigröße in unseren Result
                result.setSize(contentLength);
                //Datenstream öffnen
                stream = new DataInputStream(u.openStream());

                byte[] buffer = new byte[1024];
                int count;
                int total = 0;
                int percent;
                //Solange der Stream noch Daten hat und die Variable abortDownload nicht Boolean.True ist, mache...
                while (((count = stream.read(buffer)) != -1) && !abortDownload) {
                    outputStream.write(buffer, 0, count);
                    total += count;
                    percent = (total * 100) / contentLength;
                    progressDialog.setProgress(percent);
                }
            } catch (Exception e) {
                //Wenn ein Fehler auftritt so soll dieser in unser Result gespeichert werden.
                result.setMessage(e.getMessage());
                e.printStackTrace();
            } finally {
                //Zum Schluss den Datenstream schließen
                if (stream != null) {
                    try {
                        stream.close();
                    } catch (IOException e) {
                        //Wenn ein Fehler auftritt so soll dieser in unser Result gespeichert werden.
                        result.setMessage(e.getMessage());
                        e.printStackTrace();
                    }
                }
            }
        } catch (Exception e) {
            //Wenn ein Fehler auftritt so soll dieser in unser Result gespeichert werden.
            result.setMessage(e.getMessage());
            e.printStackTrace();
        }
        //Rückgabe unseres Ergebnisses.
        return result;
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        //Anzeigen des ProgressDialoges,
        //dieses geschieht noch bevor der Download gestartet wird.
        progressDialog = getWaitDialog();
        progressDialog.show();
    }

    @Override
    protected void onPostExecute(Result result) {
        super.onPostExecute(result);
        //nach dem Download (erfolgreich oder nicht)
        //soll der ProgressDialog geschlossen werden.
        progressDialog.dismiss();
        //Ausführen des Callbacks
        callback.handleResult(result);
    }

    /**
     * Liefert einen ProgressDialog
     * @return ein ProgressDialog
     */
    private ProgressDialog getWaitDialog() {
        Context context = contextRef.get();
        String titel =  context.getResources().getString(R.string.msg_loaddialog_titel);
        String message = context.getResources().getString(R.string.msg_loaddialog_message);

        ProgressDialog progressDialog = new ProgressDialog(context);
        progressDialog.setTitle(titel);
        progressDialog.setMessage(message);
        progressDialog.setCancelable(false);
        progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
        progressDialog.setButton(ProgressDialog.BUTTON_POSITIVE, context.getResources().getString(R.string.abort), new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                abortDownload = true;
            }
        });
        return progressDialog;
    }

}

Für dieses Beispiel habe ich eine Datei mit ca. 30MB auf eine Subdomain (http://progs.draeger-it.blog/example.file) geladen. Ich kann & möchte nicht garantieren das diese Datei auf ewig bereitgestellt wird. Die Datei selbst habe ich mit dem Befehl „fsutil file createnew example.file 3000000“ unter Microsoft Windows 10 erstellt.

ProgressDialog - Download im Vorgang
ProgressDialog – Download im Vorgang

Wichtig ist hier die Methode „onPostExecute“, diese Methode wird zum Schluss ausgeführt und in diesem Beispiel werde ich hier den Callback aufrufen.

@Override
protected void onPostExecute(Result result) {
    super.onPostExecute(result);
    //nach dem Download (erfolgreich oder nicht)
    //soll der ProgressDialog geschlossen werden.
    progressDialog.dismiss();
    //Ausführen des Callbacks
    callback.handleResult(result);
}

Ausführen des asynchronen Task

Für das Ausführen des asynchronen Task haben wir im ersten Schritt eine Schaltfläche erzeugt. Nun wollen wir an diese Schaltfläche einen Listener hängen und in diesem den Task starten.

public class MainActivity extends AppCompatActivity {

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);

       Button startBtn = findViewById(R.id.startBtn);
       startBtn.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View v) {
             RequestAsyncTask requestAsyncTask = new RequestAsyncTask(MainActivity.this, new CallbackImpl());
             requestAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
          }
      });
   }

Nun müssen wir noch ein Callback implementieren. In diesem Callback werde ich 2 Textfelder (jeweils mit Dateiname, Dateigröße) und / oder ein Feld mit der Fehlermeldung einer Exception befüllen.

class CallbackImpl implements ICallback {
   @Override
   public void handleResult(Result result) {
     //Wenn das Feld "message" leer bzw. NULL ist dann ist kein Fehler aufgetreten.
     boolean showErrorMessage = result.getMessage() == null;

     TextView errorMsgTextView = findViewById(R.id.errorMsgTextView);
     //Wenn keine Fehlermeldung aufgetreten ist dann soll das TextView Element ausgeblendet werden.
     errorMsgTextView.setVisibility(showErrorMessage ? View.VISIBLE : View.INVISIBLE);
     if(showErrorMessage){
        errorMsgTextView.setText(result.getMessage());
     }

     TextView dateinameTextView = findViewById(R.id.dateinameTextView);
     dateinameTextView.setText(result.getFilename());

     TextView dateigroesseTextView = findViewById(R.id.dateigroesseTextView);
     dateigroesseTextView.setText(String.valueOf(result.getSize()).concat(" byte"));
  }
}

Download

Fazit

Das auslagern des asynchronen Task hat mir geholfen ein bereits bestehendes Projekt deutlich zu verschlanken. Man könnte nun statt einem Callback auch ein funktionales Interface nutzen jedoch geht dieses erst ab Java 8. 

 

 

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.