web-dev-qa-db-fra.com

Tâche en arrière-plan, dialogue de progression, changement d'orientation - existe-t-il une solution à 100% qui fonctionne?

Je télécharge des données d’Internet en tâche de fond (j’utilise AsyncTask) et j’affiche une boîte de dialogue de progression pendant le téléchargement. L'orientation change, l'activité est redémarrée, puis ma tâche asynchrone est terminée. Je souhaite fermer la boîte de dialogue de progression et commencer une nouvelle activité. Mais appeler licencierDialog génère parfois une exception (probablement parce que l'activité a été détruite et que la nouvelle activité n'a pas encore démarré).

Quel est le meilleur moyen de gérer ce type de problème (mise à jour de l'interface utilisateur à partir d'un fil de travail en arrière-plan qui fonctionne même si l'utilisateur change d'orientation)? Quelqu'un de Google a-t-il fourni une "solution officielle"?

233
fhucho

Étape n ° 1: Faites de votre AsyncTask une static classe imbriquée, ou une classe entièrement séparée, mais pas une classe interne (imbriquée non statique).

Étape n ° 2: Demandez au AsyncTask de conserver le Activity via un membre de données, défini via le constructeur et un configurateur.

Étape n ° 3: lors de la création de AsyncTask, fournissez le Activity actuel au constructeur.

Étape n ° 4: Dans onRetainNonConfigurationInstance(), renvoyez le AsyncTask, après l'avoir détaché de l'activité d'origine, qui a maintenant disparu.

Étape 5: Dans onCreate(), si getLastNonConfigurationInstance() n'est pas null, transmettez-le à votre classe AsyncTask et appelez votre opérateur pour associer votre nouvelle activité. avec la tâche.

Étape n ° 6: Ne faites pas référence au membre de données d'activité de doInBackground().

Si vous suivez la recette ci-dessus, tout fonctionnera. onProgressUpdate() et onPostExecute() sont suspendus entre le début de onRetainNonConfigurationInstance() et la fin de la suivante onCreate().

Voici un exemple de projet démontrant la technique.

Une autre approche consiste à abandonner le AsyncTask et à déplacer votre travail dans un IntentService. Ceci est particulièrement utile si le travail à effectuer peut être long et doit continuer quelle que soit l'activité de l'utilisateur (par exemple, le téléchargement d'un fichier volumineux). Vous pouvez utiliser une diffusion ordonnée Intent pour que l'activité réponde au travail en cours (si elle est toujours au premier plan) ou pour déclencher un Notification afin de permettre à l'utilisateur de savoir si le travail a été effectué. été fait. Voici un article de blog avec plus sur ce modèle.

335
CommonsWare

La réponse acceptée a été très utile, mais elle n’a pas de dialogue de progression.

Heureusement pour vous, lecteur, j'ai créé un exemple extrêmement complet et très utile d'un AsyncTask avec un dialogue de progression !

  1. La rotation fonctionne et le dialogue survit.
  2. Vous pouvez annuler la tâche et la boîte de dialogue en appuyant sur le bouton Précédent (si vous souhaitez ce comportement).
  3. Il utilise des fragments.
  4. La disposition du fragment situé en dessous de l'activité change correctement lorsque l'appareil est en rotation.
13
Timmmm

Je travaille depuis une semaine pour trouver une solution à ce dilemme sans avoir à éditer le fichier manifeste. Les hypothèses de cette solution sont:

  1. Vous devez toujours utiliser un dialogue de progression
  2. Une seule tâche est effectuée à la fois
  3. La tâche doit persister lors de la rotation du téléphone et la boîte de dialogue de progression doit être automatiquement supprimée.

Mise en oeuvre

Vous devrez copier les deux fichiers situés au bas de cet article dans votre espace de travail. Assurez-vous simplement que:

  1. Tous vos Activitys devraient s'étendre BaseActivity

  2. Dans onCreate(), super.onCreate() devrait être appelé après l'initialisation des membres auxquels votre ASyncTasks a besoin d'accéder. En outre, remplacez getContentViewId() pour fournir l'ID de présentation du formulaire.

  3. Remplacez onCreateDialog()comme d'habitude pour créer des boîtes de dialogue gérées par l'activité.

  4. Voir le code ci-dessous pour un exemple de classe interne statique pour créer vos tâches asynchrones. Vous pouvez stocker votre résultat dans mResult pour y accéder plus tard.


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

Et enfin, pour lancer votre nouvelle tâche:

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

Voilà! J'espère que cette solution robuste aidera quelqu'un.

BaseActivity.Java (organiser les importations vous-même)

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;
    }
}
8
Oleg Vaskevich

Quelqu'un de Google a-t-il fourni une "solution officielle"?

Oui.

La solution est plutôt une proposition d'architecture d'application plutôt que seulement du code.

Ils ont proposé 3 modèles de conception permettant à une application de fonctionner en synchronisation avec un serveur, quel que soit son état. (Cela fonctionnera même si l'utilisateur termine l'application, l'utilisateur change d'écran, l'application se termine, tout autre état possible dans lequel une opération de données en arrière-plan pourrait être interrompue, cela le couvre)

La proposition est expliquée dans le Android REST applications clientes discours pendant Google I/O 2010 de Virgil Dobjanschi. Il dure 1 heure, mais il est extrêmement intéressant à regarder.

La base de cette opération est d’abstraire les opérations réseau sur un Service qui fonctionne indépendamment pour n’importe quel Activity de l’application. Si vous travaillez avec des bases de données, l'utilisation de ContentResolver et Cursor vous donnera un modèle d'observateur prêt à l'emploi il est pratique de mettre à jour l’UI sans aucune logique supplémentaire, une fois que vous avez mis à jour votre base de données locale avec les données distantes extraites. Tout autre code après opération serait exécuté via un rappel transmis à la Service (j'utilise une sous-classe ResultReceiver pour cela).

Quoi qu'il en soit, mon explication est en fait assez vague, vous devriez absolument regarder le discours.

4

Bien que la réponse de Mark (CommonsWare) fonctionne effectivement pour les changements d’orientation, elle échoue si l’activité est détruite directement (comme dans le cas d’un appel téléphonique).

Vous pouvez gérer les modifications d'orientation ET les rares événements d'activité détruits en utilisant un objet Application pour référencer votre ASyncTask.

Il y a une excellente explication du problème et de la solution ici :

Le mérite revient entièrement à Ryan pour avoir trouvé celui-ci.

2
Scott Biggs

Après 4 ans, Google a résolu le problème en appelant simplement setRetainInstance (true) dans Activity onCreate. Cela préservera votre instance d'activité pendant la rotation du périphérique. J'ai aussi une solution simple pour le vieil Android.

1
Singagirl

vous devez appeler toutes les actions d'activité à l'aide du gestionnaire d'activité. Donc, si vous êtes dans un fil de discussion, vous devez créer un Runnable et le publier à l’aide de Activitie's Handler. Sinon, votre application plantera parfois avec une exception fatale.

0
xpepermint

Voici ma solution: https://github.com/Gotchamoh/Android-AsyncTask-ProgressDialog

Fondamentalement, les étapes sont les suivantes:

  1. J'utilise onSaveInstanceState pour enregistrer la tâche si elle est en cours de traitement.
  2. Dans onCreate je récupère la tâche si elle a été enregistrée.
  3. Dans onPause, je supprime le ProgressDialog s'il est affiché.
  4. Dans onResume, je montre le ProgressDialog si la tâche est en cours de traitement.
0
Gotcha