Comment gérer une AsyncTask pendant la rotation de l'écran?

88

J'ai beaucoup lu sur la manière de sauvegarder l'état de mon instance ou de gérer la destruction de mon activité lors de la rotation de l'écran.

Il semble y avoir beaucoup de possibilités, mais je n'ai pas trouvé celle qui fonctionne le mieux pour récupérer les résultats d'une AsyncTask.

J'ai quelques AsyncTasks qui sont simplement redémarrées et appellent la isFinishing()méthode de l'activité et si l'activité se termine, elles ne mettront rien à jour.

Le problème est que j'ai une tâche qui fait une demande à un service Web qui peut échouer ou réussir et le redémarrage de la tâche entraînerait une perte financière pour l'utilisateur.

Comment résoudriez-vous cela? Quels sont les avantages ou les inconvénients des solutions possibles?

Janusz
la source
1
Voir ma réponse ici . Vous pouvez également trouver ces informations sur ce qui setRetainInstance(true)fait réellement utile.
Timmmm
ce que je ferais, c'est simplement implémenter un service local qui effectue le traitement (dans un thread) que votre asyncTask effectue. Pour afficher les résultats, diffusez les données sur votre activité. Désormais, l'activité est uniquement responsable de l'affichage des données et le traitement n'est jamais interrompu par une rotation d'écran.
Someone Somewhere
Qu'en est-il d'utiliser AsyncTaskLoader au lieu d'AsyncTask?
Sourangshu Biswas

Réponses:

6

Ma première suggestion serait de vous assurer que vous avez réellement besoin que votre activité soit réinitialisée lors d'une rotation d'écran (le comportement par défaut). Chaque fois que j'ai eu des problèmes de rotation, j'ai ajouté cet attribut à ma <activity>balise dans le fichier AndroidManifest.xml, et tout s'est bien passé.

android:configChanges="keyboardHidden|orientation"

Cela a l'air bizarre, mais ce qu'il fait est transféré à votre onConfigurationChanged()méthode, si vous n'en fournissez pas une, cela ne fait rien d'autre que de re-mesurer la mise en page, ce qui semble être un moyen parfaitement adéquat de gérer la rotation la plupart du temps. .

Jim Blackler
la source
5
Mais cela empêchera l'activité de modifier la mise en page. Et oblige donc l'utilisateur à utiliser son appareil dans une orientation spécifique dictée par votre application et non par ses besoins.
Janusz
77
L'utilisation de cette technique vous empêche d'utiliser facilement des ressources spécifiques à la configuration. Par exemple, si vous voulez que votre mise en page, vos éléments dessinables, vos chaînes ou tout autre élément différent en mode portrait et paysage, vous souhaiterez le comportement par défaut. Remplacer le changement de configuration ne doit être effectué que dans des cas très spécifiques (un jeu, un navigateur Web, etc.) et non par paresse ou par commodité parce que vous vous contraignez.
Romain Guy
38
Et bien c'est juste ça, Romain. "si vous voulez que votre mise en page, vos dessinables, vos chaînes ou autre soient différents en portrait et en paysages, vous voudrez le comportement par défaut", je pense que c'est un cas d'utilisation beaucoup plus rare que ce que vous envisagez. Ce que vous appelez des «cas très spécifiques», c'est la plupart des développeurs, je crois. L'utilisation de mises en page relatives qui fonctionnent dans toutes les dimensions est la meilleure pratique et ce n'est pas si difficile. Parler de paresse est très erroné, ces techniques visent à améliorer l'expérience utilisateur et non à réduire le temps de développement.
Jim Blackler
2
J'ai trouvé que cela fonctionne parfaitement pour LinearLayout, mais lors de l'utilisation de RelativeLayout, il ne redessine pas correctement la mise en page lors du passage en mode paysage (du moins pas sur le N1). Voir ces questions: stackoverflow.com/questions/2987049/…
JohnRock
9
Je suis d'accord avec Romain (il sait de quoi il parle, il développe l'OS). Que se passe-t-il lorsque vous souhaitez porter votre application sur une tablette et que votre interface utilisateur a l'air horrible lorsqu'elle est étirée? Si vous adoptez l'approche de cette réponse, vous devrez recoder toute votre solution car vous avez opté pour ce hack paresseux.
Austyn Mahoney
46

Vous pouvez découvrir comment je gère AsyncTaskles changements d'orientation et les changements d'orientation sur code.google.com/p/shelves . Il existe différentes façons de le faire, celle que j'ai choisie dans cette application est d'annuler toute tâche en cours d'exécution, d'enregistrer son état et d'en démarrer une nouvelle avec l'état enregistré lorsque la nouvelle Activityest créée. C'est facile à faire, cela fonctionne bien et en prime, il s'occupe d'arrêter vos tâches lorsque l'utilisateur quitte l'application.

Vous pouvez également utiliser onRetainNonConfigurationInstance()pour passer votre AsyncTaskau nouveau Activity( faites attention à ne pas fuir le précédent de Activitycette façon.)

Romain Guy
la source
1
Je l'ai essayé et la rotation pendant les interruptions de recherche de livre et me donne moins de résultats que lorsque je ne tourne pas, dommage
max4ever
1
Je n'ai pas pu trouver une seule utilisation d'AsyncTask dans ce code. Il existe une classe UserTask qui semble similaire. Ce projet est-il antérieur à AsyncTask?
devconsole
7
AsyncTask provient de UserTask. J'ai initialement écrit UserTask pour mes propres applications et je l'ai ensuite transformé en AsyncTask. Désolé, j'ai oublié qu'il a été renommé.
Romain Guy
@RomainGuy Salut, j'espère que vous allez bien. Selon votre code, 2 demandes sont envoyées au serveur bien que la première tâche soit annulée mais pas avec succès. Je ne sais pas pourquoi. Pouvez-vous me dire s'il existe un moyen de résoudre ce problème?
iamcrypticcoder
10

C'est la question la plus intéressante que j'ai vue concernant Android !!! En fait, j'ai déjà cherché la solution ces derniers mois. Toujours pas résolu.

Soyez prudent, remplacez simplement le

android:configChanges="keyboardHidden|orientation"

les choses ne suffisent pas.

Considérez le cas où l'utilisateur reçoit un appel téléphonique pendant que votre AsyncTask est en cours d'exécution. Votre demande est déjà en cours de traitement par le serveur, donc l'AsyncTask attend une réponse. En ce moment, votre application passe en arrière-plan, car l'application Téléphone vient juste de passer au premier plan. Le système d'exploitation peut tuer votre activité car elle est en arrière-plan.

Vit Khudenko
la source
6

Pourquoi ne gardez-vous pas toujours une référence à la AsyncTask actuelle sur le Singleton fournie par Android?

A chaque démarrage d'une tâche, sur PreExecute ou sur le générateur, vous définissez:

((Application) getApplication()).setCurrentTask(asyncTask);

Chaque fois qu'il se termine, vous le définissez sur null.

De cette façon, vous avez toujours une référence qui vous permet de faire quelque chose comme, onCreate ou onResume en fonction de votre logique spécifique:

this.asyncTaskReference = ((Application) getApplication()).getCurrentTask();

S'il est nul, vous savez qu'il n'y en a actuellement aucun en cours d'exécution!

:-)

Neteinstein
la source
Est-ce que ça va marcher? Quelqu'un a-t-il testé cela? La tâche sera-t-elle toujours supprimée par le système si une interruption d'appel téléphonique se produit ou si nous naviguons vers une nouvelle activité puis revenons en arrière?
Robert
6
ApplicationL'instance a son propre cycle de vie - elle peut également être tuée par le système d'exploitation, donc cette solution peut provoquer un bogue difficile à reproduire.
Vit Khudenko le
7
J'ai pensé: si l'application est tuée, toute l'application est tuée (et donc toutes les AsyncTasks aussi)?
manmal
Je pense que cette application peut être tuée sans que toutes les tâches asynchrones soient (très rares). Mais @Arhimed avec quelques vérifications faciles à faire au début et à la fin de chaque asynctask, vous pouvez éviter les bogues.
neteinstein
3

Dans Pro android 4. l'auteur a suggéré une bonne manière, que vous devriez utiliser weak reference.

Note de référence faible

hqt
la source
3

À mon point de vue, il est préférable de stocker un asynctask en le onRetainNonConfigurationInstancedécouplant de l'objet Activity actuel et en le liant à un nouvel objet Activity après le changement d'orientation. Ici, j'ai trouvé un très bel exemple comment travailler avec AsyncTask et ProgressDialog.

Yury
la source
2

Android: traitement en arrière-plan / Opeartion asynchrone avec changement de configuration

Pour maintenir les états d'opération asynchrone pendant le processus d'arrière-plan: vous pouvez prendre une aide de fragments.

Suivez les étapes suivantes:

Étape 1: Créez un fragment sans en-tête, disons une tâche d'arrière-plan et ajoutez une classe de tâche asynchrone privée avec dedans.

Étape 2 (Étape facultative): si vous souhaitez placer un curseur de chargement au-dessus de votre activité, utilisez le code ci-dessous:

Étape 3: Dans votre activité principale, implémentez l'interface BackgroundTaskCallbacks définie à l'étape 1

class BackgroundTask extends Fragment {
public BackgroundTask() {

}

// Add a static interface 

static interface BackgroundTaskCallbacks {
    void onPreExecute();

    void onCancelled();

    void onPostExecute();

    void doInBackground();
}

private BackgroundTaskCallbacks callbacks;
private PerformAsyncOpeation asyncOperation;
private boolean isRunning;
private final String TAG = BackgroundTask.class.getSimpleName();

/**
 * Start the async operation.
 */
public void start() {
    Log.d(TAG, "********* BACKGROUND TASK START OPERATION ENTER *********");
    if (!isRunning) {
        asyncOperation = new PerformAsyncOpeation();
        asyncOperation.execute();
        isRunning = true;
    }
    Log.d(TAG, "********* BACKGROUND TASK START OPERATION EXIT *********");
}

/**
 * Cancel the background task.
 */
public void cancel() {
    Log.d(TAG, "********* BACKGROUND TASK CANCEL OPERATION ENTER *********");
    if (isRunning) {
        asyncOperation.cancel(false);
        asyncOperation = null;
        isRunning = false;
    }
    Log.d(TAG, "********* BACKGROUND TASK CANCEL OPERATION EXIT *********");
}

/**
 * Returns the current state of the background task.
 */
public boolean isRunning() {
    return isRunning;
}

/**
 * Android passes us a reference to the newly created Activity by calling
 * this method after each configuration change.
 */
public void onAttach(Activity activity) {
    Log.d(TAG, "********* BACKGROUND TASK ON ATTACH ENTER *********");
    super.onAttach(activity);
    if (!(activity instanceof BackgroundTaskCallbacks)) {
        throw new IllegalStateException(
                "Activity must implement the LoginCallbacks interface.");
    }

    // Hold a reference to the parent Activity so we can report back the
    // task's
    // current progress and results.
    callbacks = (BackgroundTaskCallbacks) activity;
    Log.d(TAG, "********* BACKGROUND TASK ON ATTACH EXIT *********");
}

public void onCreate(Bundle savedInstanceState) {
    Log.d(TAG, "********* BACKGROUND TASK ON CREATE ENTER *********");
    super.onCreate(savedInstanceState);
    // Retain this fragment across configuration changes.
    setRetainInstance(true);
    Log.d(TAG, "********* BACKGROUND TASK ON CREATE EXIT *********");
}

public void onDetach() {
    super.onDetach();
    callbacks = null;
}

private class PerformAsyncOpeation extends AsyncTask<Void, Void, Void> {
    protected void onPreExecute() {
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON PRE EXECUTE ENTER *********");
        if (callbacks != null) {
            callbacks.onPreExecute();
        }
        isRunning = true;
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON PRE EXECUTE EXIT *********");
    }

    protected Void doInBackground(Void... params) {
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > DO IN BACKGROUND ENTER *********");
        if (callbacks != null) {
            callbacks.doInBackground();
        }
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > DO IN BACKGROUND EXIT *********");
        return null;
    }

    protected void onCancelled() {
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON CANCEL ENTER *********");
        if (callbacks != null) {
            callbacks.onCancelled();
        }
        isRunning = false;
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON CANCEL EXIT *********");
    }

    protected void onPostExecute(Void ignore) {
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON POST EXECUTE ENTER *********");
        if (callbacks != null) {
            callbacks.onPostExecute();
        }
        isRunning = false;
        Log.d(TAG,
                "********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON POST EXECUTE EXIT *********");
    }
}

public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    setRetainInstance(true);
}

public void onStart() {
    super.onStart();
}

public void onResume() {
    super.onResume();
}

public void onPause() {
    super.onPause();
}

public void onStop() {
    super.onStop();
}

public class ProgressIndicator extends Dialog {

public ProgressIndicator(Context context, int theme) {
    super(context, theme);
}

private ProgressBar progressBar;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    setContentView(R.layout.progress_indicator);
    this.setCancelable(false);
    progressBar = (ProgressBar) findViewById(R.id.progressBar);
    progressBar.getIndeterminateDrawable().setColorFilter(R.color.DarkBlue, android.graphics.PorterDuff.Mode.SCREEN);
}

@Override
public void show() {
    super.show();
}

@Override
public void dismiss() {
    super.dismiss();
}

@Override
public void cancel() {
    super.cancel();
}

public class MyActivity extends FragmentActivity implements BackgroundTaskCallbacks,{

private static final String KEY_CURRENT_PROGRESS = "current_progress";

ProgressIndicator progressIndicator = null;

private final static String TAG = MyActivity.class.getSimpleName();

private BackgroundTask task = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(//"set your layout here");
    initialize your views and widget here .............



    FragmentManager fm = getSupportFragmentManager();
    task = (BackgroundTask) fm.findFragmentByTag("login");

    // If the Fragment is non-null, then it is currently being
    // retained across a configuration change.
    if (task == null) {
        task = new BackgroundTask();
        fm.beginTransaction().add(task, "login").commit();
    }

    // Restore saved state
    if (savedInstanceState != null) {
        Log.i(TAG, "KEY_CURRENT_PROGRESS_VALUE ON CREATE :: "
                + task.isRunning());
        if (task.isRunning()) {
            progressIndicator = new ProgressIndicator(this,
                    R.style.TransparentDialog);
            if (progressIndicator != null) {
                progressIndicator.show();
            }
        }
    }
}

@Override
protected void onPause() {
    // TODO Auto-generated method stub
    super.onPause();

}

@Override
protected void onSaveInstanceState(Bundle outState) {
    // save the current state of your operation here by saying this 

    super.onSaveInstanceState(outState);
    Log.i(TAG, "KEY_CURRENT_PROGRESS_VALUE ON SAVE INSTANCE :: "
            + task.isRunning());
    outState.putBoolean(KEY_CURRENT_PROGRESS, task.isRunning());
    if (progressIndicator != null) {
        progressIndicator.dismiss();
        progressIndicator.cancel();
    }
    progressIndicator = null;
}


private void performOperation() {

            if (!task.isRunning() && progressIndicator == null) {
                progressIndicator = new ProgressIndicator(this,
                        R.style.TransparentDialog);
                progressIndicator.show();
            }
            if (task.isRunning()) {
                task.cancel();
            } else {
                task.start();
            }
        }


@Override
protected void onDestroy() {
    super.onDestroy();
    if (progressIndicator != null) {
        progressIndicator.dismiss();
        progressIndicator.cancel();
    }
    progressIndicator = null;
}

@Override
public void onPreExecute() {
    Log.i(TAG, "CALLING ON PRE EXECUTE");
}

@Override
public void onCancelled() {
    Log.i(TAG, "CALLING ON CANCELLED");
    if (progressIndicator != null) {
        progressIndicator.dismiss();
        progressIndicator.cancel();

}

public void onPostExecute() {
    Log.i(TAG, "CALLING ON POST EXECUTE");
    if (progressIndicator != null) {
        progressIndicator.dismiss();
        progressIndicator.cancel();
        progressIndicator = null;
    }
}

@Override
public void doInBackground() {
    // put your code here for background operation
}

}

Piyush Gupta
la source
1

Une chose à considérer est de savoir si le résultat de l'AsyncTask doit être disponible uniquement pour l'activité qui a démarré la tâche. Si oui, la réponse de Romain Guy est la meilleure. S'il doit être disponible pour d'autres activités de votre application, onPostExecutevous pouvez utiliser LocalBroadcastManager.

LocalBroadcastManager.getInstance(getContext()).sendBroadcast(new Intent("finished"));

Vous devrez également vous assurer que l'activité gère correctement la situation lorsque la diffusion est envoyée alors que l'activité est en pause.

Juozas Kontvainis
la source
1

Jetez un œil à ce post . Ce message implique qu'AsyncTask effectue une opération de longue durée et une fuite de mémoire lorsque la rotation de l'écran se produit à la fois dans un exemple d'application. L'exemple d'application est disponible sur la forge source

Vahid
la source
0

Ma solution.

Dans mon cas, j'ai une chaîne d'AsyncTasks avec le même contexte. L'activité n'avait accès qu'au premier. Pour annuler toute tâche en cours, j'ai effectué les opérations suivantes:

public final class TaskLoader {

private static AsyncTask task;

     private TaskLoader() {
         throw new UnsupportedOperationException();
     }

     public static void setTask(AsyncTask task) {
         TaskLoader.task = task;
     }

    public static void cancel() {
         TaskLoader.task.cancel(true);
     }
}

Tâche doInBackground():

protected Void doInBackground(Params... params) {
    TaskLoader.setTask(this);
    ....
}

Activité onStop()ou onPause():

protected void onStop() {
    super.onStop();
    TaskLoader.cancel();
}
Pitt90
la source
0
@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    final AddTask task = mAddTask;
    if (task != null && task.getStatus() != UserTask.Status.FINISHED) {
        final String bookId = task.getBookId();
        task.cancel(true);

        if (bookId != null) {
            outState.putBoolean(STATE_ADD_IN_PROGRESS, true);
            outState.putString(STATE_ADD_BOOK, bookId);
        }

        mAddTask = null;
    }
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
    if (savedInstanceState.getBoolean(STATE_ADD_IN_PROGRESS)) {
        final String id = savedInstanceState.getString(STATE_ADD_BOOK);
        if (!BooksManager.bookExists(getContentResolver(), id)) {
            mAddTask = (AddTask) new AddTask().execute(id);
        }
    }
}
Atif Mahmood
la source
0

vous pouvez également ajouter android: configChanges = "keyboardHidden | orientation | screenSize"

à votre exemple manifeste j'espère que cela vous aidera

 <application
    android:name=".AppController"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:configChanges="keyboardHidden|orientation|screenSize"
    android:theme="@style/AppTheme">
eng mohamed emam
la source