Attività in background, finestra di dialogo di avanzamento, cambio di orientamento: esiste una soluzione operativa al 100%?


235

Scarico alcuni dati da Internet nel thread in background (che uso AsyncTask) e visualizzo una finestra di avanzamento durante il download. L'orientamento cambia, l'attività viene riavviata e quindi il mio AsyncTask è completato: desidero chiudere la finestra di dialogo dei progressi e iniziare una nuova attività. Ma la chiamata di dismissDialog talvolta genera un'eccezione (probabilmente perché l'attività è stata distrutta e la nuova attività non è ancora stata avviata).

Qual è il modo migliore per gestire questo tipo di problema (aggiornamento dell'interfaccia utente dal thread in background che funziona anche se l'utente cambia orientamento)? Qualcuno di Google ha fornito una "soluzione ufficiale"?


4
Il mio post sul blog su questo argomento potrebbe essere d'aiuto. Si tratta di conservare attività di lunga durata tra le modifiche alla configurazione.
Alex Lockwood,

1
Anche questa domanda è collegata.
Alex Lockwood,

Basta FTR c'è un mistero correlato qui .. stackoverflow.com/q/23742412/294884
Fattie

Risposte:


336

Passaggio n. 1: crea AsyncTaskuna staticclasse nidificata o una classe completamente separata, non solo una classe interna (nidificata non statica).

Passaggio 2: AsyncTasktenere premuto Activitytramite un membro dati, impostare tramite il costruttore e un setter.

Passaggio n. 3: durante la creazione di AsyncTask, fornire la corrente Activityal costruttore.

Step # 4: onRetainNonConfigurationInstance()Ritorna AsyncTask, dopo averlo staccato dall'attività originale, ora in corso.

Passaggio n. 5: in onCreate()caso getLastNonConfigurationInstance()contrario null, esegui il cast nella tua AsyncTaskclasse e chiama il tuo setter per associare la tua nuova attività all'attività.

Passaggio n. 6: non fare riferimento al membro dei dati dell'attività da doInBackground().

Se segui la ricetta sopra, funzionerà tutto. onProgressUpdate()e onPostExecute()sono sospesi tra l'inizio onRetainNonConfigurationInstance()e la fine del successivo onCreate().

Ecco un esempio di progetto che dimostra la tecnica.

Un altro approccio è quello di abbandonare AsyncTaske spostare il tuo lavoro in un IntentService. Ciò è particolarmente utile se il lavoro da svolgere può essere lungo e deve continuare indipendentemente da ciò che l'utente fa in termini di attività (ad esempio, il download di un file di grandi dimensioni). È possibile utilizzare una trasmissione ordinata Intentper fare in modo che l'attività risponda al lavoro svolto (se è ancora in primo piano) o sollevare un Notificationper far sapere all'utente se il lavoro è stato svolto. Ecco un post sul blog con ulteriori informazioni su questo modello.


8
Grazie mille per l'ottima risposta a questo problema comune! Per essere precisi, è possibile aggiungere al passaggio n. 4 che dobbiamo staccare (impostato su null) l'attività in AsyncTask. Questo è ben illustrato nel progetto di esempio, però.
Kevin Gaudin,

3
Ma cosa succede se devo avere accesso ai membri di Activity?
Eugene,

3
@Andrew: crea una classe interna statica o qualcosa che trattiene più oggetti e restituiscilo.
CommonsWare,

11
onRetainNonConfigurationInstance()è obsoleto e l'alternativa suggerita è di utilizzare setRetainInstance(), ma non restituisce un oggetto. È possibile gestire asyncTaskle modifiche alla configurazione con setRetainInstance()?
Indrek Kõue,

10
@SYLARRR: Assolutamente. Tieni Fragmentpremuto il AsyncTask. Hanno la Fragmentchiamata setRetainInstance(true)su se stesso. Hanno il AsyncTasksolo a parlare con il Fragment. Ora, su una modifica della configurazione, Fragmentnon viene distrutta e ricreata (anche se l'attività è), e quindi AsyncTaskviene mantenuta per tutta la modifica della configurazione.
Commons War il

13

La risposta accettata è stata molto utile, ma non ha una finestra di avanzamento.

Fortunatamente per te, lettore, ho creato un esempio estremamente completo e funzionante di un AsyncTask con una finestra di dialogo sullo stato di avanzamento !

  1. La rotazione funziona e il dialogo sopravvive.
  2. È possibile annullare l'attività e la finestra di dialogo premendo il pulsante Indietro (se si desidera questo comportamento).
  3. Usa frammenti.
  4. Il layout del frammento sotto l'attività cambia correttamente quando il dispositivo ruota.

La risposta accettata riguarda le classi statiche (non i membri). E quelli sono necessari per evitare che AsyncTask abbia un puntatore (nascosto) all'istanza della classe esterna che diventa una perdita di memoria nel distruggere l'attività.
Bananeweizen,

Sì, non so perché l'ho messo sui membri statici, dal momento che li ho effettivamente usati ... strano. Risposta modificata.
Timmmm,

Potresti per favore aggiornare il tuo link? Ne ho davvero bisogno.
Romain Pellerin,

Siamo spiacenti, non ho intenzione di ripristinare il mio sito Web: lo farò presto! Ma nel frattempo è fondamentalmente lo stesso come il codice in questa risposta: stackoverflow.com/questions/8417885/...
Timmmm

1
Il collegamento è falso; porta solo a un indice inutile senza alcuna indicazione di dove sia il codice.
FractalBob,

9

Ho lavorato duramente per una settimana per trovare una soluzione a questo dilemma senza ricorrere alla modifica del file manifest. I presupposti per questa soluzione sono:

  1. È sempre necessario utilizzare una finestra di dialogo di avanzamento
  2. Viene eseguita una sola attività alla volta
  3. È necessario che l'attività persista quando il telefono viene ruotato e la finestra di dialogo di avanzamento viene automaticamente chiusa.

Implementazione

Dovrai copiare i due file trovati in fondo a questo post nel tuo spazio di lavoro. Assicurati solo che:

  1. Tutti i tuoi messaggi Activitydovrebbero estendersiBaseActivity

  2. In onCreate(), super.onCreate()dovrebbe essere chiamato dopo aver inizializzato tutti i membri a cui è necessario accedere dai propri messaggi ASyncTask. Inoltre, eseguire getContentViewId()l' override per fornire l'ID del layout del modulo.

  3. Sostituisci onCreateDialog() come al solito per creare finestre di dialogo gestite dall'attività.

  4. Vedi il codice sotto per una classe interna statica di esempio per creare i tuoi AsyncTasks. È possibile memorizzare i risultati in mResult per accedervi in ​​un secondo momento.


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
    }
}

E infine, per avviare la tua nuova attività:

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

Questo è tutto! Spero che questa soluzione solida possa aiutare qualcuno.

BaseActivity.java (organizza tu stesso le importazioni)

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;
    }
}

4

Qualcuno di Google ha fornito una "soluzione ufficiale"?

Sì.

La soluzione è più una proposta di architettura dell'applicazione che solo un po 'di codice .

Hanno proposto 3 modelli di progettazione che consentono a un'applicazione di funzionare in sincronia con un server, indipendentemente dallo stato dell'applicazione (funzionerà anche se l'utente termina l'app, l'utente cambia schermata, l'app viene terminata, ogni altro stato possibile in cui un'operazione di dati in background potrebbe essere interrotta, questo lo copre)

La proposta è spiegata nel discorso sulle applicazioni client Android REST durante l' I / O Google 2010 di Virgil Dobjanschi. Dura 1 ora, ma vale la pena guardarlo.

La base di ciò sta nell'astrarre le operazioni di rete in modo Serviceche funzionino indipendentemente da qualsiasi Activityapplicazione. Se si lavora con database, l'uso ContentResolvere Cursorfornirebbe un modello Observer pronto all'uso che è conveniente per aggiornare l'interfaccia utente senza alcuna logica adizionale, una volta aggiornato il database locale con i dati remoti recuperati. Qualsiasi altro codice post-operazione verrebbe eseguito tramite un callback passato al Service(io uso una ResultReceiversottoclasse per questo).

Comunque, la mia spiegazione è in realtà piuttosto vaga, dovresti assolutamente guardare il discorso.


2

Mentre la risposta di Mark (CommonsWare) funziona davvero per i cambiamenti di orientamento, fallisce se l'attività viene distrutta direttamente (come nel caso di una telefonata).

È possibile gestire le modifiche all'orientamento E i rari eventi di attività distrutti utilizzando un oggetto Applicazione per fare riferimento a ASyncTask.

C'è un'eccellente spiegazione del problema e della soluzione qui :

Il merito va completamente a Ryan per averlo capito.


1

Dopo 4 anni Google ha risolto il problema chiamando semplicemente setRetainInstance (true) in Activity onCreate. Conserverà l'istanza dell'attività durante la rotazione del dispositivo. Ho anche una soluzione semplice per Android precedente.


1
Il problema che l'osservatore delle persone si verifica perché Android distrugge una classe di attività in rotazione, estensione della tastiera e altri eventi, ma un'attività asincrona mantiene ancora un riferimento per l'istanza distrutta e cerca di usarla per gli aggiornamenti dell'interfaccia utente. Puoi dare istruzioni ad Android di non distruggere l'attività né in manifest né in modo pragmatico. In questo caso, un riferimento all'attività asincrona rimane valido e nessun problema osservato. Poiché la rotazione può richiedere un lavoro aggiuntivo come ricaricare le visualizzazioni e così via, Google non consiglia di preservare l'attività. Quindi decidi tu.
Singagirl,

Grazie, sapevo della situazione ma non di setRetainInstance (). Quello che non capisco è la tua affermazione che Google ha usato questo per risolvere i problemi posti nella domanda. Puoi collegare la fonte di informazione? Grazie.
jj_


onRetainNonConfigurationInstance () Questa funzione è chiamata puramente come ottimizzazione e non devi fare affidamento sul fatto che sia chiamata. <Dalla stessa fonte: developer.android.com/reference/android/app/…
Dhananjay M

0

dovresti chiamare tutte le azioni di attività usando il gestore attività. Quindi, se sei in qualche thread, dovresti creare un Runnable e pubblicato usando Activitie's Handler. Altrimenti la tua app andrà in crash a volte con un'eccezione fatale.


0

Questa è la mia soluzione: https://github.com/Gotchamoh/Android-AsyncTask-ProgressDialog

Fondamentalmente i passaggi sono:

  1. Uso onSaveInstanceStateper salvare l'attività se è ancora in elaborazione.
  2. In onCreateottengo l'attività se è stata salvata.
  3. In onPausescarto il ProgressDialogse è mostrato.
  4. In onResumemostro ProgressDialogse l'attività è ancora in elaborazione.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.