Hintergrundaufgabe, Fortschrittsdialog, Orientierungsänderung - gibt es eine 100% funktionierende Lösung?

235

Ich lade einige Daten aus dem Internet im Hintergrund-Thread herunter (ich verwende sie AsyncTask) und zeige beim Herunterladen einen Fortschrittsdialog an. Die Ausrichtung ändert sich, die Aktivität wird neu gestartet und dann wird meine AsyncTask abgeschlossen. Ich möchte den Fortschrittsdialog schließen und eine neue Aktivität starten. Das Aufrufen von entlassenDialog löst jedoch manchmal eine Ausnahme aus (wahrscheinlich, weil die Aktivität zerstört wurde und eine neue Aktivität noch nicht gestartet wurde).

Was ist der beste Weg, um diese Art von Problem zu lösen (Aktualisieren der Benutzeroberfläche über einen Hintergrundthread, der auch dann funktioniert, wenn der Benutzer die Ausrichtung ändert)? Hat jemand von Google eine "offizielle Lösung" bereitgestellt?

fhucho
quelle
4
Mein Blogbeitrag zu diesem Thema könnte helfen. Es geht darum, lang laufende Aufgaben über Konfigurationsänderungen hinweg beizubehalten.
Alex Lockwood
1
Diese Frage ist auch verwandt.
Alex Lockwood
Nur FTR, hier gibt es ein ähnliches Rätsel. Stackoverflow.com/q/23742412/294884
Fattie

Antworten:

336

Schritt 1: Machen Sie Ihre Klasse zu AsyncTaskeiner staticverschachtelten Klasse oder zu einer völlig separaten Klasse, nur nicht zu einer inneren (nicht statisch verschachtelten) Klasse.

Schritt 2: AsyncTaskHalten Sie das ActivityVia über ein Datenelement fest, das über den Konstruktor und einen Setter festgelegt wurde.

Schritt 3: Wenn Sie das erstellen AsyncTask, versorgen Sie Activityden Konstruktor mit Strom .

Schritt 4: Geben Sie onRetainNonConfigurationInstance()die zurück AsyncTask, nachdem Sie sie von der ursprünglichen Aktivität entfernt haben.

Schritt 5: onCreate()Wenn dies getLastNonConfigurationInstance()nicht der Fall ist null, übertragen Sie es in Ihre AsyncTaskKlasse und rufen Sie Ihren Setter an, um Ihre neue Aktivität mit der Aufgabe zu verknüpfen.

Schritt 6: Verweisen Sie nicht auf das Aktivitätsdatenmitglied von doInBackground().

Wenn Sie das obige Rezept befolgen, funktioniert alles. onProgressUpdate()und onPostExecute()werden zwischen dem Beginn onRetainNonConfigurationInstance()und dem Ende des folgenden suspendiert onCreate().

Hier ist ein Beispielprojekt , das die Technik demonstriert.

Ein anderer Ansatz besteht darin AsyncTask, die Arbeit aufzugeben und in eine zu verschieben IntentService. Dies ist besonders nützlich, wenn die zu erledigende Arbeit langwierig sein kann und unabhängig davon fortgesetzt werden soll, was der Benutzer in Bezug auf Aktivitäten tut (z. B. Herunterladen einer großen Datei). Sie können eine geordnete Sendung verwenden Intent, um die Aktivität entweder auf die ausgeführte Arbeit reagieren zu lassen (sofern sie noch im Vordergrund steht), oder eine Notification, um den Benutzer darüber zu informieren, ob die Arbeit erledigt wurde. Hier ist ein Blog-Beitrag mit mehr zu diesem Muster.

CommonsWare
quelle
8
Vielen Dank für Ihre großartige Antwort auf dieses häufig auftretende Problem! Um gründlich zu sein, können Sie Schritt 4 hinzufügen, dass wir die Aktivität in der AsyncTask trennen (auf null setzen) müssen. Dies wird jedoch im Beispielprojekt gut veranschaulicht.
Kevin Gaudin
3
Aber was ist, wenn ich Zugang zu den Mitgliedern von Activity haben muss?
Eugene
3
@ Andrew: Erstellen Sie eine statische innere Klasse oder etwas, das mehrere Objekte enthält, und geben Sie sie zurück.
CommonsWare
11
onRetainNonConfigurationInstance()ist veraltet und die vorgeschlagene Alternative ist die Verwendung setRetainInstance(), gibt jedoch kein Objekt zurück. Ist es möglich, asyncTaskKonfigurationsänderungen mit zu bearbeiten setRetainInstance()?
Indrek Kõue
10
@ SYLARRR: Auf jeden Fall. Haben die Fragmenthalten die AsyncTask. Haben Sie den FragmentAnruf setRetainInstance(true)auf sich. Habe das AsyncTaskeinzige Gespräch mit dem Fragment. Bei einer Konfigurationsänderung wird das Fragmentnicht zerstört und neu erstellt (obwohl die Aktivität ausgeführt wird), sodass das AsyncTaskwährend der Konfigurationsänderung beibehalten wird.
CommonsWare
13

Die akzeptierte Antwort war sehr hilfreich, hat aber keinen Fortschrittsdialog.

Zum Glück für Sie, Leser, habe ich ein äußerst umfassendes und funktionierendes Beispiel für eine AsyncTask mit einem Fortschrittsdialog erstellt !

  1. Die Drehung funktioniert und der Dialog bleibt erhalten.
  2. Sie können die Aufgabe und den Dialog abbrechen, indem Sie die Zurück-Taste drücken (wenn Sie dieses Verhalten wünschen).
  3. Es werden Fragmente verwendet.
  4. Das Layout des Fragments unter der Aktivität ändert sich ordnungsgemäß, wenn sich das Gerät dreht.
Timmmm
quelle
Die akzeptierte Antwort bezieht sich auf statische Klassen (keine Mitglieder). Und diese sind notwendig , um zu vermeiden, dass die AsyncTask einen (versteckten) Zeiger auf die Instanz der äußeren Klasse hat, der beim Zerstören der Aktivität zu einem Speicherverlust wird.
Bananeweizen
Ja, ich bin mir nicht sicher, warum ich das über statische Mitglieder gesagt habe, da ich sie tatsächlich auch benutzt habe ... komisch. Antwort bearbeitet.
Timmmm
Könnten Sie bitte Ihren Link aktualisieren? Ich brauche das wirklich.
Romain Pellerin
Entschuldigung, ich bin noch nicht dazu gekommen, meine Website wiederherzustellen - ich werde es bald tun! Aber in der Zwischenzeit ist es im Grunde der gleiche wie der Code in dieser Antwort: stackoverflow.com/questions/8417885/…
Timmmm
1
Der Link ist falsch; führt nur zu einem nutzlosen Index ohne Angabe, wo sich der Code befindet.
FractalBob
9

Ich habe eine Woche lang gearbeitet, um eine Lösung für dieses Dilemma zu finden, ohne die Manifestdatei bearbeiten zu müssen. Die Annahmen für diese Lösung sind:

  1. Sie müssen immer einen Fortschrittsdialog verwenden
  2. Es wird jeweils nur eine Aufgabe ausgeführt
  3. Die Aufgabe muss bestehen bleiben, wenn das Telefon gedreht wird, und der Fortschrittsdialog wird automatisch geschlossen.

Implementierung

Sie müssen die beiden Dateien am Ende dieses Beitrags in Ihren Arbeitsbereich kopieren. Stellen Sie einfach sicher, dass:

  1. Alle Ihre Activitys sollten sich verlängernBaseActivity

  2. In onCreate(), super.onCreate()sollten aufgerufen werden , nachdem Sie Mitglieder zu initialisieren , dass Bedarf von Ihrem zugegriffen werden ASyncTasks. Überschreiben Sie außerdem getContentViewId(), um die Formularlayout-ID anzugeben.

  3. Überschreiben Sie onCreateDialog() wie gewohnt , um von der Aktivität verwaltete Dialoge zu erstellen.

  4. Im folgenden Code finden Sie ein Beispiel für eine statische innere Klasse zum Erstellen Ihrer AsyncTasks. Sie können Ihr Ergebnis in mResult speichern, um später darauf zugreifen zu können.


final static class MyTask extends SuperAsyncTask<Void, Void, Void> {

    public OpenDatabaseTask(BaseActivity activity) {
        super(activity, MY_DIALOG_ID); // change your dialog ID here...
                                       // and your dialog will be managed automatically!
    }

    @Override
    protected Void doInBackground(Void... params) {

        // your task code

        return null;
    }

    @Override
    public boolean onAfterExecute() {
        // your after execute code
    }
}

Und schließlich, um Ihre neue Aufgabe zu starten:

mCurrentTask = new MyTask(this);
((MyTask) mCurrentTask).execute();

Das ist es! Ich hoffe, diese robuste Lösung wird jemandem helfen.

BaseActivity.java (Importe selbst organisieren)

protected abstract int getContentViewId();

public abstract class BaseActivity extends Activity {
    protected SuperAsyncTask<?, ?, ?> mCurrentTask;
    public HashMap<Integer, Boolean> mDialogMap = new HashMap<Integer, Boolean>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(getContentViewId());

        mCurrentTask = (SuperAsyncTask<?, ?, ?>) getLastNonConfigurationInstance();
        if (mCurrentTask != null) {
            mCurrentTask.attach(this);
            if (mDialogMap.get((Integer) mCurrentTask.dialogId) != null
                && mDialogMap.get((Integer) mCurrentTask.dialogId)) {
        mCurrentTask.postExecution();
            }
        }
    }

    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
    super.onPrepareDialog(id, dialog);

        mDialogMap.put(id, true);
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        if (mCurrentTask != null) {
            mCurrentTask.detach();

            if (mDialogMap.get((Integer) mCurrentTask.dialogId) != null
                && mDialogMap.get((Integer) mCurrentTask.dialogId)) {
                return mCurrentTask;
            }
        }

        return super.onRetainNonConfigurationInstance();
    }

    public void cleanupTask() {
        if (mCurrentTask != null) {
            mCurrentTask = null;
            System.gc();
        }
    }
}

SuperAsyncTask.java

public abstract class SuperAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
    protected BaseActivity mActivity = null;
    protected Result mResult;
    public int dialogId = -1;

    protected abstract void onAfterExecute();

    public SuperAsyncTask(BaseActivity activity, int dialogId) {
        super();
        this.dialogId = dialogId;
        attach(activity);
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        mActivity.showDialog(dialogId); // go polymorphism!
    }    

    protected void onPostExecute(Result result) {
        super.onPostExecute(result);
        mResult = result;

        if (mActivity != null &&
                mActivity.mDialogMap.get((Integer) dialogId) != null
                && mActivity.mDialogMap.get((Integer) dialogId)) {
            postExecution();
        }
    };

    public void attach(BaseActivity activity) {
        this.mActivity = activity;
    }

    public void detach() {
        this.mActivity = null;
    }

    public synchronized boolean postExecution() {
        Boolean dialogExists = mActivity.mDialogMap.get((Integer) dialogId);
        if (dialogExists != null || dialogExists) {
            onAfterExecute();
            cleanUp();
    }

    public boolean cleanUp() {
        mActivity.removeDialog(dialogId);
        mActivity.mDialogMap.remove((Integer) dialogId);
        mActivity.cleanupTask();
        detach();
        return true;
    }
}
Oleg Vaskevich
quelle
4

Hat jemand von Google eine "offizielle Lösung" bereitgestellt?

Ja.

Die Lösung ist eher ein Vorschlag für eine Anwendungsarchitektur als nur ein Teil des Codes .

Sie schlugen drei Entwurfsmuster vor , mit denen eine Anwendung unabhängig vom Anwendungsstatus synchron mit einem Server arbeiten kann (dies funktioniert auch dann, wenn der Benutzer die App beendet, der Benutzer den Bildschirm ändert, die App beendet wird und jeder andere mögliche Status, in dem) eine Hintergrunddatenoperation könnte unterbrochen werden, dies deckt es ab)

Der Vorschlag wird in der Rede der Android REST-Clientanwendungen während der Google I / O 2010 von Virgil Dobjanschi erläutert. Es ist 1 Stunde lang, aber es ist sehr sehenswert.

Die Basis davon ist die Abstraktion von Netzwerkoperationen zu einem Service, der unabhängig von jedem Activityin der Anwendung funktioniert . Wenn Sie mit Datenbanken arbeiten, erhalten Sie durch die Verwendung von ContentResolverund erhalten Cursorein sofort einsatzbereites Observer-Muster , mit dem Sie die Benutzeroberfläche ohne zusätzliche Logik aktualisieren können, sobald Sie Ihre lokale Datenbank mit den abgerufenen Remote-Daten aktualisiert haben. Jeder andere Code nach der Operation würde über einen Rückruf ausgeführt, der an die übergeben wird Service(ich verwende hierfür eine ResultReceiverUnterklasse).

Wie auch immer, meine Erklärung ist eigentlich ziemlich vage, Sie sollten auf jeden Fall die Rede sehen.

Christopher Francisco
quelle
2

Während Marks (CommonsWare) Antwort tatsächlich für Orientierungsänderungen funktioniert, schlägt sie fehl, wenn die Aktivität direkt zerstört wird (wie im Fall eines Telefonanrufs).

Sie können die Orientierungsänderungen UND die seltenen zerstörten Aktivitätsereignisse verarbeiten, indem Sie ein Anwendungsobjekt verwenden, um auf Ihre ASyncTask zu verweisen.

Es gibt eine ausgezeichnete Erklärung des Problems und die Lösung hier :

Ryan ist es zu verdanken, dass er das herausgefunden hat.

Scott Biggs
quelle
1

Nach 4 Jahren löste Google das Problem, indem er setRetainInstance (true) in Activity onCreate aufrief. Dadurch bleibt Ihre Aktivitätsinstanz während der Gerätedrehung erhalten. Ich habe auch eine einfache Lösung für ältere Android.

Singagirl
quelle
1
Das Problem, das Menschen beobachten, tritt auf, weil Android eine Aktivitätsklasse bei Rotation, Tastaturerweiterung und anderen Ereignissen zerstört, eine asynchrone Aufgabe jedoch weiterhin eine Referenz für die zerstörte Instanz enthält und versucht, diese für UI-Updates zu verwenden. Sie können Android anweisen, Aktivitäten weder im Manifest noch pragmatisch zu zerstören. In diesem Fall bleibt eine asynchrone Aufgabenreferenz gültig und es wird kein Problem festgestellt. Da die Rotation einige zusätzliche Arbeiten zum erneuten Laden von Ansichten usw. erfordern kann, empfiehlt Google nicht, die Aktivität beizubehalten. Also entscheidest du.
Singagirl
Danke, ich wusste über die Situation Bescheid, aber nicht über setRetainInstance (). Was ich nicht verstehe, ist Ihre Behauptung, dass Google dies verwendet hat, um die in der Frage gestellten Probleme zu lösen. Können Sie die Informationsquelle verknüpfen? Vielen Dank.
JJ_
onRetainNonConfigurationInstance () Diese Funktion wird nur als Optimierung aufgerufen, und Sie dürfen sich nicht darauf verlassen, dass sie aufgerufen wird. <Aus derselben Quelle: developer.android.com/reference/android/app/…
Dhananjay M
0

Sie sollten alle Aktivitätsaktionen mit dem Aktivitätshandler aufrufen. Wenn Sie sich in einem Thread befinden, sollten Sie eine ausführbare Datei erstellen und mit Activities Handler veröffentlichen. Andernfalls stürzt Ihre App manchmal mit schwerwiegenden Ausnahmen ab.

xpepermint
quelle
0

Dies ist meine Lösung: https://github.com/Gotchamoh/Android-AsyncTask-ProgressDialog

Grundsätzlich sind die Schritte:

  1. Ich onSaveInstanceStatespeichere die Aufgabe, wenn sie noch bearbeitet wird.
  2. In onCreatebekomme ich die Aufgabe, wenn sie gespeichert wurde.
  3. In onPauseverwerfe ich das ProgressDialogwenn es angezeigt wird.
  4. In onResumezeige ich, ProgressDialogob die Aufgabe noch bearbeitet wird.
Erwischt
quelle