Fragments Android. Conserver une AsyncTask pendant la rotation de l'écran ou le changement de configuration

86

Je travaille sur une application pour smartphone / tablette, en utilisant un seul APK et en chargeant les ressources selon les besoins en fonction de la taille de l'écran, le meilleur choix de conception semblait être d'utiliser des fragments via l'ACL.

Cette application fonctionnait bien jusqu'à présent, étant uniquement basée sur l'activité. Ceci est une classe simulée de la façon dont je gère les AsyncTasks et ProgressDialogs dans les activités afin de les faire fonctionner même lorsque l'écran est pivoté ou qu'un changement de configuration se produit au milieu de la communication.

Je ne changerai pas le manifeste pour éviter la recréation de l'activité, il y a de nombreuses raisons pour lesquelles je ne veux pas le faire, mais principalement parce que la documentation officielle dit que ce n'est pas recommandé et que je me suis débrouillé sans cela jusqu'à présent, alors veuillez ne pas le recommander route.

public class Login extends Activity {

    static ProgressDialog pd;
    AsyncTask<String, Void, Boolean> asyncLoginThread;

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.login);
        //SETUP UI OBJECTS
        restoreAsyncTask();
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        if (pd != null) pd.dismiss();
        if (asyncLoginThread != null) return (asyncLoginThread);
        return super.onRetainNonConfigurationInstance();
    }

    private void restoreAsyncTask();() {
        pd = new ProgressDialog(Login.this);
        if (getLastNonConfigurationInstance() != null) {
            asyncLoginThread = (AsyncTask<String, Void, Boolean>) getLastNonConfigurationInstance();
            if (asyncLoginThread != null) {
                if (!(asyncLoginThread.getStatus()
                        .equals(AsyncTask.Status.FINISHED))) {
                    showProgressDialog();
                }
            }
        }
    }

    public class LoginThread extends AsyncTask<String, Void, Boolean> {
        @Override
        protected Boolean doInBackground(String... args) {
            try {
                //Connect to WS, recieve a JSON/XML Response
                //Place it somewhere I can use it.
            } catch (Exception e) {
                return true;
            }
            return true;
        }

        protected void onPostExecute(Boolean result) {
            if (result) {
                pd.dismiss();
                //Handle the response. Either deny entry or launch new Login Succesful Activity
            }
        }
    }
}

Ce code fonctionne bien, j'ai environ 10 000 utilisateurs sans se plaindre, il semblait donc logique de simplement copier cette logique dans la nouvelle conception basée sur les fragments, mais, bien sûr, cela ne fonctionne pas.

Voici le LoginFragment:

public class LoginFragment extends Fragment {

    FragmentActivity parentActivity;
    static ProgressDialog pd;
    AsyncTask<String, Void, Boolean> asyncLoginThread;

    public interface OnLoginSuccessfulListener {
        public void onLoginSuccessful(GlobalContainer globalContainer);
    }

    public void onSaveInstanceState(Bundle outState){
        super.onSaveInstanceState(outState);
        //Save some stuff for the UI State
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setRetainInstance(true);
        //If I setRetainInstance(true), savedInstanceState is always null. Besides that, when loading UI State, a NPE is thrown when looking for UI Objects.
        parentActivity = getActivity();
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            loginSuccessfulListener = (OnLoginSuccessfulListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString() + " must implement OnLoginSuccessfulListener");
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        RelativeLayout loginLayout = (RelativeLayout) inflater.inflate(R.layout.login, container, false);
        return loginLayout;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        //SETUP UI OBJECTS
        if(savedInstanceState != null){
            //Reload UI state. Im doing this properly, keeping the content of the UI objects, not the object it self to avoid memory leaks.
        }
    }

    public class LoginThread extends AsyncTask<String, Void, Boolean> {
            @Override
            protected Boolean doInBackground(String... args) {
                try {
                    //Connect to WS, recieve a JSON/XML Response
                    //Place it somewhere I can use it.
                } catch (Exception e) {
                    return true;
                }
                return true;
            }

            protected void onPostExecute(Boolean result) {
                if (result) {
                    pd.dismiss();
                    //Handle the response. Either deny entry or launch new Login Succesful Activity
                }
            }
        }
    }
}

Je ne peux pas l'utiliser onRetainNonConfigurationInstance()car il doit être appelé à partir de l'activité et non du fragment, il en va de même getLastNonConfigurationInstance(). J'ai lu des questions similaires ici sans réponse.

Je comprends que cela pourrait nécessiter un peu de travail pour organiser correctement ces éléments en fragments, cela étant dit, je voudrais conserver la même logique de conception de base.

Quelle serait la bonne façon de conserver l'AsyncTask pendant un changement de configuration, et si elle est toujours en cours d'exécution, afficher un progressDialog, en tenant compte du fait que l'AsyncTask est une classe interne du Fragment et que c'est le Fragment lui-même qui invoque l'AsyncTask.execute ()?

aveugle
la source
1
Peut-être que le fil de discussion sur la gestion des changements de configuration avec AsyncTask peut vous aider
rds
associez AsyncTask à un cycle de vie d'application .. ainsi peut reprendre lorsque l'activité se recrée
Fred Grott
Consultez mon article sur ce sujet: Gestion des changements de configuration avec Fragments
Alex Lockwood

Réponses:

75

Les fragments peuvent en fait rendre cela beaucoup plus facile. Utilisez simplement la méthode Fragment.setRetainInstance (boolean) pour conserver votre instance de fragment lors des modifications de configuration. Notez qu'il s'agit du remplacement recommandé pour Activity.onRetainnonConfigurationInstance () dans la documentation.

Si, pour une raison quelconque, vous ne voulez vraiment pas utiliser un fragment conservé, il existe d'autres approches que vous pouvez adopter. Notez que chaque fragment a un identifiant unique renvoyé par Fragment.getId () . Vous pouvez également savoir si un fragment est en cours de suppression pour un changement de configuration via Fragment.getActivity (). IsChangingConfigurations () . Ainsi, au moment où vous décideriez d'arrêter votre AsyncTask (dans onStop () ou onDestroy () très probablement), vous pourriez par exemple vérifier si la configuration est en train de changer et si c'est le cas la coller dans un SparseArray statique sous l'identifiant du fragment, puis dans votre onCreate () ou onStart () regardez si vous avez une AsyncTask dans le tableau épars disponible.

hackbod
la source
Notez que setRetainInstance ne l'est que si vous n'utilisez pas de back stack.
Neil
4
N'est-il pas possible que l'AsyncTask renvoie son résultat avant que l'onCreateView du fragment conservé ne s'exécute?
jakk
6
@jakk Les méthodes de cycle de vie pour les activités, les fragments, etc. sont appelées séquentiellement par la file d'attente de messages du fil GUI principal, donc même si la tâche se terminait simultanément en arrière-plan avant que ces méthodes de cycle de vie ne soient terminées (ou même invoquées), la onPostExecuteméthode aurait encore besoin de attendez avant d'être finalement traité par la file d'attente de messages du thread principal.
Alex Lockwood
Cette approche (RetainInstance = true) ne fonctionnera pas si vous souhaitez charger différents fichiers de mise en page pour chaque orientation.
Justin le
Le démarrage de l'asynctask dans la méthode onCreate de MainActivity ne semble fonctionner que si l'asynctask - à l'intérieur du fragment "worker" - est démarré par une action explicite de l'utilisateur. Parce que le fil principal et l'interface utilisateur sont disponibles. Cependant, démarrer l'asynctask juste après le lancement de l'application - sans action de l'utilisateur comme un clic sur un bouton - donne une exception. Dans ce cas, l'asynctask peut être appelée dans la méthode onStart sur MainActivity, pas dans la méthode onCreate.
ʕ ᵔᴥᵔ ʔ
66

Je pense que vous apprécierez mon exemple extrêmement complet et fonctionnel détaillé ci-dessous.

  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 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.
  5. Il existe un téléchargement complet du code source et un APK précompilé afin que vous puissiez voir si le comportement est ce que vous voulez.

Éditer

Comme demandé par Brad Larson, j'ai reproduit la plupart des solutions liées ci-dessous. Aussi, depuis que je l'ai posté, on m'a signalé AsyncTaskLoader. Je ne suis pas sûr que ce soit totalement applicable aux mêmes problèmes, mais vous devriez quand même le vérifier.

Utilisation AsyncTaskavec les boîtes de dialogue de progression et la rotation de l'appareil.

Une solution de travail!

J'ai enfin tout pour fonctionner. Mon code a les caractéristiques suivantes:

  1. A Fragmentdont la mise en page change avec l'orientation.
  2. Un AsyncTaskdans lequel vous pouvez faire du travail.
  3. A DialogFragmentqui montre la progression de la tâche dans une barre de progression (pas seulement un spinner indéterminé).
  4. La rotation fonctionne sans interrompre la tâche ni fermer la boîte de dialogue.
  5. Le bouton de retour ferme la boîte de dialogue et annule la tâche (vous pouvez cependant modifier ce comportement assez facilement).

Je ne pense pas que cette combinaison de travail peut être trouvée nulle part ailleurs.

L'idée basique est la suivante. Il existe une MainActivityclasse qui contient un seul fragment - MainFragment. MainFragmenta des dispositions différentes pour l'orientation horizontale et verticale, et setRetainInstance()est faux pour que la disposition puisse changer. Cela signifie que lorsque l'orientation de l' appareil est modifié, à la fois MainActivityet MainFragmentsont complètement détruits et recréés.

Séparément, nous avons MyTask(étendu de AsyncTask) qui fait tout le travail. Nous ne pouvons pas le stocker, MainFragmentcar cela sera détruit et Google a abandonné l'utilisation de quelque chose comme setRetainNonInstanceConfiguration(). Ce n'est pas toujours disponible de toute façon et c'est au mieux un hack laid. Au lieu de cela, nous allons stocker MyTaskdans un autre fragment, un DialogFragmentappelé TaskFragment. Ce fragment sera a la setRetainInstance()valeur true, de sorte que le dispositif tourne ce fragment ne soit pas détruit, et MyTaskest conservée.

Enfin, nous devons dire à TaskFragmentqui informer quand il est terminé, et nous le faisons en utilisant setTargetFragment(<the MainFragment>)lorsque nous le créons. Lorsque le périphérique est tourné et que le MainFragmentest détruit et qu'une nouvelle instance est créée, nous utilisons le FragmentManagerpour trouver la boîte de dialogue (en fonction de son étiquette) et le faisons setTargetFragment(<the new MainFragment>). C'est à peu près tout.

Il y avait deux autres choses que je devais faire: d'abord annuler la tâche lorsque la boîte de dialogue est fermée, puis définir le message de rejet sur null, sinon la boîte de dialogue est étrangement fermée lorsque l'appareil est tourné.

Le code

Je ne listerai pas les mises en page, elles sont assez évidentes et vous pouvez les trouver dans le téléchargement du projet ci-dessous.

Activité principale

C'est assez simple. J'ai ajouté un rappel dans cette activité pour qu'il sache quand la tâche est terminée, mais vous n'en aurez peut-être pas besoin. Principalement, je voulais juste montrer le mécanisme de rappel de l'activité de fragment parce qu'il est assez soigné et que vous ne l'avez peut-être pas vu auparavant.

public class MainActivity extends Activity implements MainFragment.Callbacks
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    @Override
    public void onTaskFinished()
    {
        // Hooray. A toast to our success.
        Toast.makeText(this, "Task finished!", Toast.LENGTH_LONG).show();
        // NB: I'm going to blow your mind again: the "int duration" parameter of makeText *isn't*
        // the duration in milliseconds. ANDROID Y U NO ENUM? 
    }
}

MainFragment

C'est long mais ça vaut le coup!

public class MainFragment extends Fragment implements OnClickListener
{
    // This code up to onDetach() is all to get easy callbacks to the Activity. 
    private Callbacks mCallbacks = sDummyCallbacks;

    public interface Callbacks
    {
        public void onTaskFinished();
    }
    private static Callbacks sDummyCallbacks = new Callbacks()
    {
        public void onTaskFinished() { }
    };

    @Override
    public void onAttach(Activity activity)
    {
        super.onAttach(activity);
        if (!(activity instanceof Callbacks))
        {
            throw new IllegalStateException("Activity must implement fragment's callbacks.");
        }
        mCallbacks = (Callbacks) activity;
    }

    @Override
    public void onDetach()
    {
        super.onDetach();
        mCallbacks = sDummyCallbacks;
    }

    // Save a reference to the fragment manager. This is initialised in onCreate().
    private FragmentManager mFM;

    // Code to identify the fragment that is calling onActivityResult(). We don't really need
    // this since we only have one fragment to deal with.
    static final int TASK_FRAGMENT = 0;

    // Tag so we can find the task fragment again, in another instance of this fragment after rotation.
    static final String TASK_FRAGMENT_TAG = "task";

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

        // At this point the fragment may have been recreated due to a rotation,
        // and there may be a TaskFragment lying around. So see if we can find it.
        mFM = getFragmentManager();
        // Check to see if we have retained the worker fragment.
        TaskFragment taskFragment = (TaskFragment)mFM.findFragmentByTag(TASK_FRAGMENT_TAG);

        if (taskFragment != null)
        {
            // Update the target fragment so it goes to this fragment instead of the old one.
            // This will also allow the GC to reclaim the old MainFragment, which the TaskFragment
            // keeps a reference to. Note that I looked in the code and setTargetFragment() doesn't
            // use weak references. To be sure you aren't leaking, you may wish to make your own
            // setTargetFragment() which does.
            taskFragment.setTargetFragment(this, TASK_FRAGMENT);
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState)
    {
        return inflater.inflate(R.layout.fragment_main, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState)
    {
        super.onViewCreated(view, savedInstanceState);

        // Callback for the "start task" button. I originally used the XML onClick()
        // but it goes to the Activity instead.
        view.findViewById(R.id.taskButton).setOnClickListener(this);
    }

    @Override
    public void onClick(View v)
    {
        // We only have one click listener so we know it is the "Start Task" button.

        // We will create a new TaskFragment.
        TaskFragment taskFragment = new TaskFragment();
        // And create a task for it to monitor. In this implementation the taskFragment
        // executes the task, but you could change it so that it is started here.
        taskFragment.setTask(new MyTask());
        // And tell it to call onActivityResult() on this fragment.
        taskFragment.setTargetFragment(this, TASK_FRAGMENT);

        // Show the fragment.
        // I'm not sure which of the following two lines is best to use but this one works well.
        taskFragment.show(mFM, TASK_FRAGMENT_TAG);
//      mFM.beginTransaction().add(taskFragment, TASK_FRAGMENT_TAG).commit();
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data)
    {
        if (requestCode == TASK_FRAGMENT && resultCode == Activity.RESULT_OK)
        {
            // Inform the activity. 
            mCallbacks.onTaskFinished();
        }
    }

TâcheFragment

    // This and the other inner class can be in separate files if you like.
    // There's no reason they need to be inner classes other than keeping everything together.
    public static class TaskFragment extends DialogFragment
    {
        // The task we are running.
        MyTask mTask;
        ProgressBar mProgressBar;

        public void setTask(MyTask task)
        {
            mTask = task;

            // Tell the AsyncTask to call updateProgress() and taskFinished() on this fragment.
            mTask.setFragment(this);
        }

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

            // Retain this instance so it isn't destroyed when MainActivity and
            // MainFragment change configuration.
            setRetainInstance(true);

            // Start the task! You could move this outside this activity if you want.
            if (mTask != null)
                mTask.execute();
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState)
        {
            View view = inflater.inflate(R.layout.fragment_task, container);
            mProgressBar = (ProgressBar)view.findViewById(R.id.progressBar);

            getDialog().setTitle("Progress Dialog");

            // If you're doing a long task, you probably don't want people to cancel
            // it just by tapping the screen!
            getDialog().setCanceledOnTouchOutside(false);

            return view;
        }

        // This is to work around what is apparently a bug. If you don't have it
        // here the dialog will be dismissed on rotation, so tell it not to dismiss.
        @Override
        public void onDestroyView()
        {
            if (getDialog() != null && getRetainInstance())
                getDialog().setDismissMessage(null);
            super.onDestroyView();
        }

        // Also when we are dismissed we need to cancel the task.
        @Override
        public void onDismiss(DialogInterface dialog)
        {
            super.onDismiss(dialog);
            // If true, the thread is interrupted immediately, which may do bad things.
            // If false, it guarantees a result is never returned (onPostExecute() isn't called)
            // but you have to repeatedly call isCancelled() in your doInBackground()
            // function to check if it should exit. For some tasks that might not be feasible.
            if (mTask != null) {
                mTask.cancel(false);
            }

            // You don't really need this if you don't want.
            if (getTargetFragment() != null)
                getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_CANCELED, null);
        }

        @Override
        public void onResume()
        {
            super.onResume();
            // This is a little hacky, but we will see if the task has finished while we weren't
            // in this activity, and then we can dismiss ourselves.
            if (mTask == null)
                dismiss();
        }

        // This is called by the AsyncTask.
        public void updateProgress(int percent)
        {
            mProgressBar.setProgress(percent);
        }

        // This is also called by the AsyncTask.
        public void taskFinished()
        {
            // Make sure we check if it is resumed because we will crash if trying to dismiss the dialog
            // after the user has switched to another app.
            if (isResumed())
                dismiss();

            // If we aren't resumed, setting the task to null will allow us to dimiss ourselves in
            // onResume().
            mTask = null;

            // Tell the fragment that we are done.
            if (getTargetFragment() != null)
                getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_OK, null);
        }
    }

Ma tâche

    // This is a fairly standard AsyncTask that does some dummy work.
    public static class MyTask extends AsyncTask<Void, Void, Void>
    {
        TaskFragment mFragment;
        int mProgress = 0;

        void setFragment(TaskFragment fragment)
        {
            mFragment = fragment;
        }

        @Override
        protected Void doInBackground(Void... params)
        {
            // Do some longish task. This should be a task that we don't really
            // care about continuing
            // if the user exits the app.
            // Examples of these things:
            // * Logging in to an app.
            // * Downloading something for the user to view.
            // * Calculating something for the user to view.
            // Examples of where you should probably use a service instead:
            // * Downloading files for the user to save (like the browser does).
            // * Sending messages to people.
            // * Uploading data to a server.
            for (int i = 0; i < 10; i++)
            {
                // Check if this has been cancelled, e.g. when the dialog is dismissed.
                if (isCancelled())
                    return null;

                SystemClock.sleep(500);
                mProgress = i * 10;
                publishProgress();
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Void... unused)
        {
            if (mFragment == null)
                return;
            mFragment.updateProgress(mProgress);
        }

        @Override
        protected void onPostExecute(Void unused)
        {
            if (mFragment == null)
                return;
            mFragment.taskFinished();
        }
    }
}

Téléchargez l'exemple de projet

Voici le code source et l'APK . Désolé, l'ADT a insisté pour ajouter la bibliothèque de support avant de me laisser faire un projet. Je suis sûr que vous pouvez le supprimer.

Timmmm
la source
4
J'éviterais de conserver la barre de progression DialogFragment, car elle contient des éléments d'interface utilisateur qui contiennent des références à l'ancien contexte. Au lieu de cela, je stockerais AsyncTaskdans un autre fragment vide et le définirais DialogFragmentcomme cible.
SD
Ces références ne seront-elles pas effacées lorsque l'appareil est tourné et onCreateView()est à nouveau appelé? L'ancien mProgressBarsera écrasé par un nouveau au moins.
Timmmm
Pas explicitement, mais j'en suis presque sûr. Vous pouvez ajouter mProgressBar = null;à onDestroyView()si vous voulez être très sûr. La méthode de Singularity peut être une bonne idée, mais elle augmentera encore plus la complexité du code!
Timmmm
1
La référence que vous tenez sur l'asynctask est le fragment progressdialog, non? donc 2 questions: 1- et si je veux modifier le vrai fragment qui appelle le progressdialog; 2- Et si je veux passer des paramètres à l'asynctask? Cordialement,
Maxrunner
1
@Maxrunner, Pour passer des paramètres, le plus simple est probablement de passer mTask.execute()à MainFragment.onClick(). Vous pouvez également autoriser la transmission de paramètres setTask()ou même les stocker eux- MyTaskmêmes. Je ne sais pas exactement ce que signifie votre première question, mais vous voulez peut-être l'utiliser TaskFragment.getTargetFragment()? Je suis presque sûr que cela fonctionnera en utilisant un fichier ViewPager. Mais ViewPagersne sont pas très bien compris ou documentés, alors bonne chance! N'oubliez pas que votre fragment n'est pas créé tant qu'il n'est pas visible la première fois.
Timmmm
16

J'ai récemment publié un article décrivant comment gérer les changements de configuration à l'aide des Fragments. Cela résout bien le problème de la conservation d' AsyncTaskun changement de rotation.

Le TL; DR consiste à utiliser host your AsyncTaskinside a Fragment, à appeler setRetainInstance(true)le Fragment, et à rendre compte de la AsyncTaskprogression / des résultats de son Activity(ou de sa cible Fragment, si vous choisissez d'utiliser l'approche décrite par @Timmmm) via le fichier retenu Fragment.

Alex Lockwood
la source
5
Comment aborderiez-vous les fragments imbriqués? Comme une AsyncTask démarrée à partir d'un RetainedFragment à l'intérieur d'un autre Fragment (Tab).
Rekin
Si le fragment est déjà conservé, pourquoi ne pas simplement exécuter la tâche asynchrone à partir de ce fragment conservé? S'il est déjà conservé, la tâche asynchrone pourra lui faire rapport même si un changement de configuration se produit.
Alex Lockwood
@AlexLockwood Merci pour le blog. Au lieu d'avoir à gérer onAttachet onDetach, est-ce que ce sera mieux si à l'intérieur TaskFragment, nous appelons simplement getActivitychaque fois que nous avons besoin de déclencher un rappel. (En vérifiant instaceof TaskCallbacks)
Cheok Yan Cheng
1
Vous pouvez le faire de toute façon. Je l'ai juste fait onAttach()et onDetach()je pourrais éviter de lancer constamment l'activité à TaskCallbackschaque fois que je voulais l'utiliser.
Alex Lockwood
1
@AlexLockwood Si mon application suit une seule activité - conception de plusieurs fragments, dois-je avoir un fragment de tâche distinct pour chacun de mes fragments d'interface utilisateur? Donc, fondamentalement, le cycle de vie de chaque fragment de tâche sera géré par son fragment cible, et il n'y aurait aucune communication avec l'activité.
Manas Bajaj
13

Ma première suggestion est d' éviter les AsyncTasks internes , vous pouvez lire une question que j'ai posée à ce sujet et les réponses: Android: Recommandations AsyncTask: classe privée ou classe publique?

Après cela, j'ai commencé à utiliser non-interne et ... maintenant je vois BEAUCOUP d'avantages.

La seconde est de conserver une référence de votre AsyncTask en cours d'exécution dans la Applicationclasse - http://developer.android.com/reference/android/app/Application.html

Chaque fois que vous démarrez une AsyncTask, définissez-la sur l'application et lorsqu'elle se termine, définissez-la sur null.

Lorsqu'un fragment / activité démarre, vous pouvez vérifier si une AsyncTask est en cours d'exécution (en vérifiant si elle est nulle ou non sur l'application), puis définir la référence à l'intérieur de ce que vous voulez (activité, fragment, etc. afin que vous puissiez faire des rappels).

Cela résoudra votre problème: si vous n'avez qu'une seule AsyncTask en cours d'exécution à un moment déterminé, vous pouvez ajouter une référence simple:

AsyncTask<?,?,?> asyncTask = null;

Sinon, ayez dans l'Aplication un HashMap avec des références à eux.

La boîte de dialogue de progression peut suivre exactement le même principe.

Neteinstein
la source
2
J'ai accepté tant que vous liez le cycle de vie d'AsyncTask à son parent (en définissant AsyncTask comme une classe interne d'Activité / Fragment), il est assez difficile de faire en sorte que votre AsyncTask échappe à la récréation du cycle de vie de son parent, cependant, je n'aime pas votre solution, ça a l'air tellement piraté.
yorkw
La question est: avez-vous une meilleure solution?
neteinstein
1
Je vais devoir être d'accord avec @yorkw ici, cette solution m'a été présentée il y a quelque temps lorsque je traitais ce problème sans utiliser de fragments (application basée sur l'activité). Cette question: stackoverflow.com/questions/2620917/… a la même réponse, et je suis d'accord avec l'un des commentaires qui dit: "L'instance d'application a son propre cycle de vie - elle peut également être tuée par le système d'exploitation, donc cette solution peut provoquer un bug difficile à reproduire "
blindstuff
1
Pourtant, je ne vois pas d'autre moyen qui soit moins «hacky» comme l'a dit @yorkw. Je l'utilise dans plusieurs applications, et avec une certaine attention aux problèmes possibles, tout fonctionne très bien.
neteinstein
Peut-être que la solution @hackbod vous convient davantage.
neteinstein
4

J'ai proposé une méthode d'utilisation d'AsyncTaskLoaders pour cela. Il est assez facile à utiliser et nécessite moins de frais généraux IMO.

Fondamentalement, vous créez un AsyncTaskLoader comme ceci:

public class MyAsyncTaskLoader extends AsyncTaskLoader {
    Result mResult;
    public HttpAsyncTaskLoader(Context context) {
        super(context);
    }

    protected void onStartLoading() {
        super.onStartLoading();
        if (mResult != null) {
            deliverResult(mResult);
        }
        if (takeContentChanged() ||  mResult == null) {
            forceLoad();
        }
    }

    @Override
    public Result loadInBackground() {
        SystemClock.sleep(500);
        mResult = new Result();
        return mResult;
    }
}

Ensuite, dans votre activité qui utilise le AsyncTaskLoader ci-dessus lorsqu'un bouton est cliqué:

public class MyActivityWithBackgroundWork extends FragmentActivity implements LoaderManager.LoaderCallbacks<Result> {

    private String username,password;       
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.mylayout);
        //this is only used to reconnect to the loader if it already started
        //before the orientation changed
        Loader loader = getSupportLoaderManager().getLoader(0);
        if (loader != null) {
            getSupportLoaderManager().initLoader(0, null, this);
        }
    }

    public void doBackgroundWorkOnClick(View button) {
        //might want to disable the button while you are doing work
        //to prevent user from pressing it again.

        //Call resetLoader because calling initLoader will return
        //the previous result if there was one and we may want to do new work
        //each time
        getSupportLoaderManager().resetLoader(0, null, this);
    }   


    @Override
    public Loader<Result> onCreateLoader(int i, Bundle bundle) {
        //might want to start a progress bar
        return new MyAsyncTaskLoader(this);
    }


    @Override
    public void onLoadFinished(Loader<LoginResponse> loginLoader,
                               LoginResponse loginResponse)
    {
        //handle result
    }

    @Override
    public void onLoaderReset(Loader<LoginResponse> responseAndJsonHolderLoader)
    {
        //remove references to previous loader resources

    }
}

Cela semble gérer correctement les changements d'orientation et votre tâche d'arrière-plan se poursuivra pendant la rotation.

Quelques points à noter:

  1. Si dans onCreate vous vous reconnectez à l'asynctaskloader, vous serez rappelé dans onLoadFinished () avec le résultat précédent (même si on vous avait déjà dit que la requête était terminée). C'est en fait un bon comportement la plupart du temps, mais parfois cela peut être difficile à gérer. Bien que j'imagine qu'il existe de nombreuses façons de gérer cela, j'ai appelé loader.abandon () dans onLoadFinished. Ensuite, j'ai ajouté check in onCreate pour ne le rattacher au chargeur que s'il n'était pas déjà abandonné. Si vous avez à nouveau besoin des données obtenues, vous ne voudrez plus le faire. Dans la plupart des cas, vous voulez les données.

J'ai plus de détails sur l'utilisation de ceci pour les appels http ici

Matt Wolfe
la source
Sûr que getSupportLoaderManager().getLoader(0);cela ne retournera pas null (parce que le chargeur avec cet id, 0, n'existe pas encore)?
EmmanuelMess
1
Ouais, ce sera nul à moins qu'un changement de configuration ne provoque le redémarrage de l'activité pendant que le chargeur était en cours.
Matt Wolfe
3

J'ai créé une toute petite bibliothèque de tâches d'arrière-plan open source qui est fortement basée sur Marshmallow AsyncTaskmais avec des fonctionnalités supplémentaires telles que:

  1. Conservation automatique des tâches lors des changements de configuration;
  2. Rappel de l'interface utilisateur (écouteurs);
  3. Ne redémarre pas ou n'annule pas la tâche lorsque l'appareil tourne (comme le ferait Loaders);

La bibliothèque utilise en interne un Fragmentsans aucune interface utilisateur, qui est conservé lors des modifications de configuration ( setRetainInstance(true)).

Vous pouvez le trouver sur GitHub: https://github.com/NeoTech-Software/Android-Retainable-Tasks

Exemple le plus basique (version 0.2.0):

Cet exemple conserve entièrement la tâche, en utilisant une quantité très limitée de code.

Tâche:

private class ExampleTask extends Task<Integer, String> {

    public ExampleTask(String tag){
        super(tag);
    }

    protected String doInBackground() {
        for(int i = 0; i < 100; i++) {
            if(isCancelled()){
                break;
            }
            SystemClock.sleep(50);
            publishProgress(i);
        }
        return "Result";
    }
}

Activité:

public class Main extends TaskActivityCompat implements Task.Callback {

    @Override
    public void onClick(View view){
        ExampleTask task = new ExampleTask("activity-unique-tag");
        getTaskManager().execute(task, this);
    }

    @Override
    public Task.Callback onPreAttach(Task<?, ?> task) {
        //Restore the user-interface based on the tasks state
        return this; //This Activity implements Task.Callback
    }

    @Override
    public void onPreExecute(Task<?, ?> task) {
        //Task started
    }

    @Override
    public void onPostExecute(Task<?, ?> task) {
        //Task finished
        Toast.makeText(this, "Task finished", Toast.LENGTH_SHORT).show();
    }
}
Rolf ツ
la source
1

Mon approche consiste à utiliser le modèle de conception de délégation, en général, nous pouvons isoler la logique métier réelle (lire les données d'Internet ou d'une base de données ou autre) d'AsyncTask (le délégant) à BusinessDAO (le délégué), dans votre méthode AysncTask.doInBackground () , déléguez la tâche réelle à BusinessDAO, puis implémentez un mécanisme de processus singleton dans BusinessDAO, de sorte que plusieurs appels à BusinessDAO.doSomething () déclenchent simplement une tâche réelle exécutée à chaque fois et en attente du résultat de la tâche. L'idée est de conserver le délégué (c'est-à-dire BusinessDAO) pendant le changement de configuration, au lieu du délégant (c'est-à-dire AsyncTask).

  1. Créez / implémentez notre propre application, le but est de créer / initialiser BusinessDAO ici, de sorte que le cycle de vie de notre BusinessDAO soit axé sur l'application et non sur l'activité, notez que vous devez modifier AndroidManifest.xml pour utiliser MyApplication:

    public class MyApplication extends android.app.Application {
      private BusinessDAO businessDAO;
    
      @Override
      public void onCreate() {
        super.onCreate();
        businessDAO = new BusinessDAO();
      }
    
      pubilc BusinessDAO getBusinessDAO() {
        return businessDAO;
      }
    
    }
    
  2. Nos Activity / Fragement existants sont pour la plupart inchangés, implémentent toujours AsyncTask en tant que classe interne et impliquent AsyncTask.execute () de Activity / Fragement, la différence maintenant est qu'AsyncTask va déléguer la tâche réelle à BusinessDAO, donc lors du changement de configuration, une seconde AsyncTask sera initialisé et exécuté, et appelez BusinessDAO.doSomething () une deuxième fois, cependant, un deuxième appel à BusinessDAO.doSomething () ne déclenchera pas une nouvelle tâche en cours d'exécution, mais attendra la fin de la tâche en cours d'exécution:

    public class LoginFragment extends Fragment {
      ... ...
    
      public class LoginAsyncTask extends AsyncTask<String, Void, Boolean> {
        // get a reference of BusinessDAO from application scope.
        BusinessDAO businessDAO = ((MyApplication) getApplication()).getBusinessDAO();
    
        @Override
        protected Boolean doInBackground(String... args) {
            businessDAO.doSomething();
            return true;
        }
    
        protected void onPostExecute(Boolean result) {
          //Handle task result and update UI stuff.
        }
      }
    
      ... ...
    }
    
  3. Dans BusinessDAO, implémentez le mécanisme de processus singleton, par exemple:

    public class BusinessDAO {
      ExecutorCompletionService<MyTask> completionExecutor = new ExecutorCompletionService<MyTask(Executors.newFixedThreadPool(1));
      Future<MyTask> myFutureTask = null;
    
      public void doSomething() {
        if (myFutureTask == null) {
          // nothing running at the moment, submit a new callable task to run.
          MyTask myTask = new MyTask();
          myFutureTask = completionExecutor.submit(myTask);
        }
        // Task already submitted and running, waiting for the running task to finish.
        myFutureTask.get();
      }
    
      // If you've never used this before, Callable is similar with Runnable, with ability to return result and throw exception.
      private class MyTask extends Callable<MyTask> {
        public MyAsyncTask call() {
          // do your job here.
          return this;
        }
      }
    
    }
    

Je ne suis pas sûr à 100% si cela fonctionnera.De plus, l'extrait de code d'exemple doit être considéré comme un pseudocode. J'essaie juste de vous donner quelques indices au niveau de la conception. Tout commentaire ou suggestion est le bienvenu et apprécié.

Yorkw
la source
Semble une très bonne solution. Depuis que vous y avez répondu il y a environ 2 ans et demi, l'avez-vous testé depuis?! Vous dites que je ne suis pas sûr que ça marche, mais moi non plus !! Je cherche une solution bien testée pour ce problème. Avez-vous des suggestions?
Alireza A. Ahmadi
1

Vous pouvez faire de la AsyncTask un champ statique. Si vous avez besoin d'un contexte, vous devez expédier le contexte de votre application. Cela évitera les fuites de mémoire, sinon vous conserveriez une référence à l'ensemble de votre activité.

Boude
la source
1

Si quelqu'un trouve son chemin vers ce fil, j'ai trouvé qu'une approche propre consistait à exécuter la tâche Async à partir d'un app.Service(commencé par START_STICKY), puis à recréer une itération sur les services en cours d'exécution pour savoir si le service (et donc la tâche asynchrone) est toujours fonctionnement;

    public boolean isServiceRunning(String serviceClassName) {
    final ActivityManager activityManager = (ActivityManager) Application.getContext().getSystemService(Context.ACTIVITY_SERVICE);
    final List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);

    for (RunningServiceInfo runningServiceInfo : services) {
        if (runningServiceInfo.service.getClassName().equals(serviceClassName)){
            return true;
        }
    }
    return false;
 }

Si c'est le cas, rajoutez le DialogFragment(ou autre) et si ce n'est pas le cas, assurez-vous que la boîte de dialogue a été fermée.

Ceci est particulièrement pertinent si vous utilisez les v4.support.*bibliothèques car (au moment de la rédaction) elles connaissent des problèmes avec la setRetainInstanceméthode et la pagination de vue. De plus, en ne conservant pas l'instance, vous pouvez recréer votre activité en utilisant un ensemble différent de ressources (c'est-à-dire une disposition de vue différente pour la nouvelle orientation)

BrantApps
la source
N'est-il pas exagéré d'exécuter un service uniquement pour conserver une AsyncTask? Un service fonctionne dans son propre processus, et cela ne se fait pas sans frais supplémentaires.
WeNeigh
Intéressant Vinay. Je n'ai pas remarqué que l'application était plus lourde en ressources (elle est de toute façon assez légère pour le moment). Qu'avez-vous trouvé? Je considère un service comme un environnement prévisible pour permettre au système de fonctionner avec des tâches lourdes ou des E / S quel que soit l'état de l'interface utilisateur. Communiquer avec le service pour voir quand quelque chose est terminé semblait «juste». Les services que je commence à exécuter un peu de travail s'arrêtent à la fin de la tâche et survivent donc généralement environ 10 à 30 secondes.
BrantApps
La réponse de Commonsware ici semble suggérer que les services sont une mauvaise idée. J'envisage maintenant les AsyncTaskLoaders mais ils semblent venir avec leurs propres problèmes (rigidité, uniquement pour le chargement de données, etc.)
WeNeigh
1
Je vois. Il est à noter que ce service auquel vous vous êtes lié était explicitement configuré pour s'exécuter dans son propre processus. Le maître ne semblait pas aimer ce modèle utilisé souvent. Je n'ai pas explicitement fourni ces propriétés "exécuter dans un nouveau processus à chaque fois", donc j'espère que je suis isolé de cette partie de la critique. Je m'efforcerai de quantifier les effets. Les services en tant que concept ne sont bien sûr pas de `` mauvaises idées '' et sont fondamentaux pour que chaque application fasse quelque chose d'intéressant à distance, sans jeu de mots. Leur JDoc fournit plus de conseils sur leur utilisation si vous n'êtes toujours pas sûr.
BrantApps
0

J'écris du code samepl pour résoudre ce problème

La première étape consiste à créer une classe d'application:

public class TheApp extends Application {

private static TheApp sTheApp;
private HashMap<String, AsyncTask<?,?,?>> tasks = new HashMap<String, AsyncTask<?,?,?>>();

@Override
public void onCreate() {
    super.onCreate();
    sTheApp = this;
}

public static TheApp get() {
    return sTheApp;
}

public void registerTask(String tag, AsyncTask<?,?,?> task) {
    tasks.put(tag, task);
}

public void unregisterTask(String tag) {
    tasks.remove(tag);
}

public AsyncTask<?,?,?> getTask(String tag) {
    return tasks.get(tag);
}
}

Dans AndroidManifest.xml

<application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme"
        android:name="com.example.tasktest.TheApp">

Code en activité:

public class MainActivity extends Activity {

private Task1 mTask1;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mTask1 = (Task1)TheApp.get().getTask("task1");

}

/*
 * start task is not running jet
 */
public void handletask1(View v) {
    if (mTask1 == null) {
        mTask1 = new Task1();
        TheApp.get().registerTask("task1", mTask1);
        mTask1.execute();
    } else
        Toast.makeText(this, "Task is running...", Toast.LENGTH_SHORT).show();

}

/*
 * cancel task if is not finished
 */
public void handelCancel(View v) {
    if (mTask1 != null)
        mTask1.cancel(false);
}

public class Task1 extends AsyncTask<Void, Void, Void>{

    @Override
    protected Void doInBackground(Void... params) {
        try {
            for(int i=0; i<120; i++) {
                Thread.sleep(1000);
                Log.i("tests", "loop=" + i);
                if (this.isCancelled()) {
                    Log.e("tests", "tssk cancelled");
                    break;
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void onCancelled(Void result) {
        TheApp.get().unregisterTask("task1");
        mTask1 = null;
    }

    @Override
    protected void onPostExecute(Void result) {
        TheApp.get().unregisterTask("task1");
        mTask1 = null;
    }
}

}

Lorsque l'orientation de l'activité change, la variable mTask est initiée à partir du contexte de l'application. Lorsque la tâche est terminée, la variable est définie sur null et supprimée de la mémoire.

Pour moi, c'est assez.

Kenumir
la source
0

Jetez un œil à l'exemple ci-dessous, comment utiliser le fragment conservé pour conserver la tâche en arrière-plan:

public class NetworkRequestFragment extends Fragment {

    // Declare some sort of interface that your AsyncTask will use to communicate with the Activity
    public interface NetworkRequestListener {
        void onRequestStarted();
        void onRequestProgressUpdate(int progress);
        void onRequestFinished(SomeObject result);
    }

    private NetworkTask mTask;
    private NetworkRequestListener mListener;

    private SomeObject mResult;

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);

        // Try to use the Activity as a listener
        if (activity instanceof NetworkRequestListener) {
            mListener = (NetworkRequestListener) activity;
        } else {
            // You can decide if you want to mandate that the Activity implements your callback interface
            // in which case you should throw an exception if it doesn't:
            throw new IllegalStateException("Parent activity must implement NetworkRequestListener");
            // or you could just swallow it and allow a state where nobody is listening
        }
    }

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

        // Retain this Fragment so that it will not be destroyed when an orientation
        // change happens and we can keep our AsyncTask running
        setRetainInstance(true);
    }

    /**
     * The Activity can call this when it wants to start the task
     */
    public void startTask(String url) {
        mTask = new NetworkTask(url);
        mTask.execute();
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // If the AsyncTask finished when we didn't have a listener we can
        // deliver the result here
        if ((mResult != null) && (mListener != null)) {
            mListener.onRequestFinished(mResult);
            mResult = null;
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        // We still have to cancel the task in onDestroy because if the user exits the app or
        // finishes the Activity, we don't want the task to keep running
        // Since we are retaining the Fragment, onDestroy won't be called for an orientation change
        // so this won't affect our ability to keep the task running when the user rotates the device
        if ((mTask != null) && (mTask.getStatus == AsyncTask.Status.RUNNING)) {
            mTask.cancel(true);
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();

        // This is VERY important to avoid a memory leak (because mListener is really a reference to an Activity)
        // When the orientation change occurs, onDetach will be called and since the Activity is being destroyed
        // we don't want to keep any references to it
        // When the Activity is being re-created, onAttach will be called and we will get our listener back
        mListener = null;
    }

    private class NetworkTask extends AsyncTask<String, Integer, SomeObject> {

        @Override
        protected void onPreExecute() {
            if (mListener != null) {
                mListener.onRequestStarted();
            }
        }

        @Override
        protected SomeObject doInBackground(String... urls) {
           // Make the network request
           ...
           // Whenever we want to update our progress:
           publishProgress(progress);
           ...
           return result;
        }

        @Override
        protected void onProgressUpdate(Integer... progress) {
            if (mListener != null) {
                mListener.onRequestProgressUpdate(progress[0]);
            }
        }

        @Override
        protected void onPostExecute(SomeObject result) {
            if (mListener != null) {
                mListener.onRequestFinished(result);
            } else {
                // If the task finishes while the orientation change is happening and while
                // the Fragment is not attached to an Activity, our mListener might be null
                // If you need to make sure that the result eventually gets to the Activity
                // you could save the result here, then in onActivityCreated you can pass it back
                // to the Activity
                mResult = result;
            }
        }

    }
}
DeepakPanwar
la source
-1

Jetez un oeil ici .

Il existe une solution basée sur la solution de Timmmm .

Mais je l'ai amélioré:

  • Maintenant, la solution est extensible - il vous suffit d'étendre FragmentAbleToStartTask

  • Vous pouvez continuer à exécuter plusieurs tâches en même temps.

    Et à mon avis, c'est aussi simple que startActivityForResult et recevoir le résultat

  • Vous pouvez également arrêter une tâche en cours d'exécution et vérifier si une tâche particulière est en cours d'exécution

Désolé pour mon anglais

l3onid-clean-coder
la source
premier lien est rompu
Tejzeratul