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 einfachheitshalber 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 Interface. 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. 30 MB auf eine Subdomain (http://progs.draeger-it.blog/example.file) geladen. Ich kann & möchte nicht garantieren, dass 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.
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 einen 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.