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

235

Je télécharge des données sur Internet en arrière-plan (j'utilise AsyncTask) et 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 AsyncTask est terminée - je veux fermer la boîte de dialogue de progression et démarrer une nouvelle activité. Mais l'appel à shutDialog lève parfois une exception (probablement parce que l'activité a été détruite et que la nouvelle activité n'a pas encore commencé).

Quelle est la meilleure façon de gérer ce type de problème (mise à jour de l'interface utilisateur à partir du thread d'arrière-plan qui fonctionne même si l'utilisateur change d'orientation)? Quelqu'un de Google a-t-il fourni une "solution officielle"?

fhucho
la source
4
Mon article de blog sur ce sujet pourrait aider. Il s'agit de conserver les tâches de longue durée lors des changements de configuration.
Alex Lockwood
1
Cette question est également liée.
Alex Lockwood
Juste FTR, il y a un mystère lié ici .. stackoverflow.com/q/23742412/294884
Fattie

Réponses:

336

Étape # 1: Faites votre AsyncTaskunstatic classe classe imbriquée ou une classe entièrement distincte, mais pas une classe interne (imbriquée non statique).

Étape # 2: Ayez la AsyncTaskprise sur leActivity via un membre de données, défini via le constructeur et un setter.

Étape # 3: lors de la création du AsyncTask, fournissez le courantActivity au constructeur.

Étape # 4: Dans onRetainNonConfigurationInstance(), retournez leAsyncTask , après l'avoir détaché de l'activité d'origine, maintenant en cours.

Étape # 5: Dans le onCreate()cas getLastNonConfigurationInstance()contraire null, lancez-le sur votreAsyncTask classe et appelez votre setter pour associer votre nouvelle activité à la tâche.

Étape # 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 onRetainNonConfigurationInstance()et la fin du suivant onCreate().

Voici un exemple de projet démontrant la technique.

Une autre approche consiste à abandonner le AsyncTasket à déplacer votre travail dans un IntentService. Ceci est particulièrement utile si le travail à effectuer peut être long et doit se poursuivre indépendamment de ce que l'utilisateur fait en termes d'activités (par exemple, télécharger un gros fichier). Vous pouvez utiliser une diffusion ordonnée Intentpour que l'activité réponde au travail en cours (si elle est toujours au premier plan) ou déclencher un Notificationpour faire savoir à l'utilisateur si le travail a été effectué. Voici un article de blog avec plus d'informations sur ce modèle.

CommonsWare
la source
8
Merci beaucoup pour votre excellente réponse à ce problème commun! Juste pour être plus approfondi, vous pouvez ajouter à l'étape 4 que nous devons détacher (défini sur null) l'activité dans la tâche AsyncTask. Ceci est bien illustré dans l'exemple de projet, cependant.
Kevin Gaudin du
3
Mais que faire si j'ai besoin d'avoir accès aux membres de l'activité?
Eugene
3
@Andrew: Créez une classe interne statique ou quelque chose qui tient sur plusieurs objets et renvoyez-la.
CommonsWare
11
onRetainNonConfigurationInstance()est obsolète et l'alternative suggérée est d'utiliser setRetainInstance(), mais il ne retourne pas d'objet. Est-il possible de gérer asyncTaskle changement de configuration avec setRetainInstance()?
Indrek Kõue
10
@SYLARRR: Absolument. Faites Fragmenttenir le AsyncTask. Ayez l' Fragmentappel setRetainInstance(true)sur lui-même. Ayez le AsyncTaskseul entretien avec le Fragment. Maintenant, lors d'un changement de configuration, le Fragmentn'est pas détruit et recréé (même si l'activité l'est), et donc le AsyncTaskest conservé tout au long du changement de configuration.
CommonsWare
13

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

Heureusement pour vous, lecteur, j'ai créé un exemple extrêmement complet et fonctionnel d'une AsyncTask avec une boîte de 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 de retour (si vous souhaitez ce comportement).
  3. Il utilise des fragments.
  4. La disposition du fragment sous l'activité change correctement lorsque l'appareil tourne.
Timmmm
la source
La réponse acceptée concerne les classes statiques (pas les membres). Et ceux-ci sont nécessaires pour éviter que la tâche AsyncTask ait un pointeur (caché) vers l'instance de classe externe qui devient une fuite de mémoire lors de la destruction de l'activité.
Bananeweizen
Ouais, je ne sais pas pourquoi j'ai parlé des membres statiques, car je les ai aussi utilisés ... bizarre. Réponse modifiée.
Timmmm
Pourriez-vous mettre à jour votre lien? J'ai vraiment besoin de ça.
Romain Pellerin
Désolé, je n'ai pas pu restaurer mon site Web - je le ferai bientôt! Mais en attendant, c'est fondamentalement le même que le code dans cette réponse: stackoverflow.com/questions/8417885/…
Timmmm
1
Le lien est faux; conduit juste à un index inutile sans aucune indication de l'endroit où se trouve le code.
FractalBob
9

J'ai travaillé pendant une semaine pour trouver une solution à ce dilemme sans avoir recours à l'édition du fichier manifeste. Les hypothèses de cette solution sont les suivantes:

  1. Vous devez toujours utiliser une boîte de dialogue de progression
  2. Une seule tâche à la fois
  3. Vous devez que la tâche persiste lorsque le téléphone pivote et que la boîte de dialogue de progression soit automatiquement ignorée.

la mise en oeuvre

Vous devrez copier les deux fichiers trouvés au bas de ce message dans votre espace de travail. Assurez-vous simplement que:

  1. Tous vos Activitys devraient s'étendreBaseActivity

  2. Dans onCreate(), super.onCreate()doit être appelé après avoir initialisé tous les membres auxquels votre ASyncTasks. Doit accéder . Remplacez également getContentViewId()pour fournir l'ID de mise en page 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 AsyncTasks. 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();

C'est tout! J'espère que cette solution robuste aidera quelqu'un.

BaseActivity.java (organisez 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;
    }
}
Oleg Vaskevich
la source
4

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

Oui.

La solution est davantage une proposition d'architecture d'application plutôt qu'un simple code .

Ils ont proposé 3 modèles de conception qui permettent à une application de fonctionner en synchronisation avec un serveur, quel que soit l'état de l'application (cela fonctionnera même si l'utilisateur termine l'application, l'utilisateur change d'écran, l'application se termine, tous les autres états possibles où une opération de données de fond pourrait être interrompue, cela le couvre)

La proposition est expliquée dans le discours sur les applications client Android REST lors de Google I / O 2010 par Virgil Dobjanschi. Il dure 1 heure, mais il vaut vraiment la peine d'être regardé.

La base de celui-ci est d'abstraire les opérations réseau à un Servicequi fonctionne indépendamment de tout Activitydans l'application. Si vous travaillez avec des bases de données, l'utilisation ContentResolveret Cursorvous donnerait un modèle Observer prêt à l'emploi qui est pratique pour mettre à jour l'interface utilisateur 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 post-opération serait exécuté via un rappel passé au Service(j'utilise unResultReceiver sous classe pour cela).

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

Christopher Francisco
la source
2

Bien que la réponse de Mark (CommonsWare) fonctionne bien 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 changements 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 à Ryan d'avoir compris celui-ci.

Scott Biggs
la source
1

Après 4 ans, Google a résolu le problème en appelant setRetainInstance (true) dans Activity onCreate. Il préservera votre instance d'activité pendant la rotation de l'appareil. J'ai également une solution simple pour les anciens Android.

Singagirl
la source
1
L'observateur de problèmes se produit car Android détruit une classe d'activité lors de la rotation, de l'extension du clavier et d'autres événements, mais une tâche asynchrone conserve toujours une référence pour l'instance détruite et essaie de l'utiliser pour les mises à jour de l'interface utilisateur. Vous pouvez demander à Android de ne pas détruire l'activité de manière manifeste ou pragmatique. Dans ce cas, une référence de tâche asynchrone reste valide et aucun problème n'a été observé. Étant donné que la rotation peut nécessiter un travail supplémentaire en tant que vues de rechargement, etc., Google ne recommande pas de conserver l'activité. Vous décidez donc.
Singagirl
Merci, je connaissais la situation mais pas à propos de setRetainInstance (). Ce que je ne comprends pas, c'est votre affirmation selon laquelle Google a utilisé cela pour résoudre les problèmes posés dans la question. Pouvez-vous relier la source d'information? Merci.
jj_
onRetainNonConfigurationInstance () Cette fonction est appelée uniquement à titre d'optimisation et vous ne devez pas vous fier à son appel. <De la même source: developer.android.com/reference/android/app/…
Dhananjay M
0

vous devez appeler toutes les actions d'activité à l'aide du gestionnaire d'activité. Donc, si vous êtes dans un fil, vous devez créer un Runnable et publié à l'aide du gestionnaire d'Activitie. Sinon, votre application se bloque parfois avec une exception fatale.

xpepermint
la source
0

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

Fondamentalement, les étapes sont les suivantes:

  1. J'utilise onSaveInstanceStatepour enregistrer la tâche si elle est toujours en cours de traitement.
  2. En - onCreateje obtenir la tâche si elle a été acceptée.
  3. Dans onPauseje jette le ProgressDialogsi elle est affichée.
  4. Dans onResumeJe montre ProgressDialogsi la tâche est toujours en cours de traitement.
Je t'ai eu
la source