84 votes

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

Je travaille sur une application pour Smartphone / Tablette, n'utilisant qu'un seul APK, et chargeant les ressources en fonction de la taille de l'écran, le meilleur choix de conception semble être l'utilisation de Fragments via l'ACL.

Cette application fonctionnait bien jusqu'à présent, étant uniquement basée sur l'activité. Il s'agit d'une classe factice de la façon dont je gère les AsyncTasks et les ProgressDialogs dans les activités afin qu'elles fonctionnent même lorsque l'écran est tourné ou qu'un changement de configuration se produit en cours de communication.

Je ne modifierai 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 les documents officiels disent que ce n'est pas recommandé et j'ai réussi à m'en passer jusqu'à présent, donc s'il vous plaît ne recommandez pas cette 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 très bien, j'ai environ 10 000 utilisateurs qui ne se plaignent pas, 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 utiliser onRetainNonConfigurationInstance() puisqu'il doit être appelé depuis l'activité et non le fragment, il en va de même avec getLastNonConfigurationInstance() . J'ai lu des questions similaires ici, sans réponse.

Je comprends que l'organisation de ces éléments en fragments puisse nécessiter un certain travail. Cela dit, j'aimerais 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, d'afficher une boîte de dialogue de progression, en tenant compte du fait que l'AsyncTask est une classe interne au Fragment et que c'est le Fragment lui-même qui invoque l'AsyncTask.execute() ?

75voto

hackbod Points 55292

Les fragments peuvent en fait rendre cela beaucoup plus facile. Il suffit d'utiliser la méthode Fragment.setRetainInstance(booléen) pour que votre instance de fragment soit conservée à travers les changements de configuration. Notez que ceci est le remplacement recommandé pour Activity.onRetainnonConfigurationInstance() dans les docs.

Si, pour une raison ou une autre, vous ne voulez vraiment pas utiliser un fragment retenu, vous pouvez adopter d'autres approches. Notez que chaque fragment a un identifiant unique retourné par Fragment.getId() . Vous pouvez également savoir si un fragment est en train d'être démoli pour un changement de configuration par le biais de Fragment.getActivity().isChangingConfigurations() . Ainsi, au moment où vous décidez d'arrêter votre AsyncTask (dans onStop() ou onDestroy() le plus probablement), vous pouvez par exemple vérifier si la configuration change et, si c'est le cas, la placer dans un SparseArray statique sous l'identifiant du fragment, puis dans votre onCreate() ou onStart() regarder si vous avez une AsyncTask dans le sparse array disponible.

66voto

Timmmm Points 9909

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 le 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 le dispositif pivote.
  5. Le code source complet peut être téléchargé et un APK précompilé afin que vous puissiez voir si le comportement est celui que vous souhaitez.

Modifier

Comme demandé par Brad Larson, j'ai reproduit la plupart de la solution ci-dessous. De plus, depuis que j'ai posté cette solution, on m'a indiqué les sites suivants AsyncTaskLoader . Je ne suis pas sûr qu'il s'applique totalement aux mêmes problèmes, mais vous devriez quand même le consulter.

Utilisation de AsyncTask avec des dialogues de progression et la rotation des appareils.

Une solution qui fonctionne !

J'ai finalement réussi à tout faire fonctionner. Mon code a les caractéristiques suivantes :

  1. A Fragment dont la disposition change avec l'orientation.
  2. Un site AsyncTask dans lequel vous pouvez travailler.
  3. A DialogFragment qui montre l'avancement de la tâche dans une barre de progression (et pas seulement une roue indéterminée).
  4. La rotation fonctionne sans interrompre la tâche ni rejeter la boîte de dialogue.
  5. Le bouton Retour permet de fermer la boîte de dialogue et d'annuler la tâche (vous pouvez cependant modifier ce comportement assez facilement).

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

L'idée de base est la suivante. Il existe un MainActivity qui contient un seul fragment - MainFragment . MainFragment a des mises en page différentes pour l'orientation horizontale et verticale, et setRetainInstance() est faux afin que la mise en page puisse être modifiée. Cela signifie que lorsque l'orientation de l'appareil est modifiée, les deux objets MainActivity y MainFragment sont complètement détruits et recréés.

Séparément, nous avons MyTask (prolongé de AsyncTask ) qui fait tout le travail. Nous ne pouvons pas le stocker dans MainFragment parce qu'elle sera détruite, et Google a déconseillé l'utilisation d'éléments comme setRetainNonInstanceConfiguration() . Ce n'est pas toujours disponible de toute façon et c'est au mieux une mauvaise idée. À la place, nous allons stocker MyTask dans un autre fragment, un DialogFragment appelé TaskFragment . Ce site fragment sera ont setRetainInstance() mis à true, de sorte que lorsque le dispositif tourne, ce fragment n'est pas détruit, et MyTask est conservé.

Enfin, nous devons indiquer au TaskFragment qui informer quand il est terminé, et nous le faisons en utilisant setTargetFragment(<the MainFragment>) lorsque nous la créons. Lorsque l'appareil est tourné et que le MainFragment est détruite et qu'une nouvelle instance est créée, nous utilisons la balise FragmentManager pour trouver le dialogue (sur la base de son étiquette) et faire 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 rejetée, et ensuite définir le message de rejet à null, sinon la boîte de dialogue est bizarrement rejetée lorsque le dispositif est tourné.

Le code

Je ne vais pas énumérer 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 callback dans cette activité pour qu'elle sache quand la tâche est terminée, mais vous n'en aurez peut-être pas besoin. Je voulais surtout montrer le mécanisme de rappel de l'activité fragmentaire parce qu'il est assez élégant et que vous ne l'avez peut-être jamais 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();
        }
    }

TaskFragment

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

MyTask

    // 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écharger le projet d'exemple

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

16voto

Alex Lockwood Points 31578

J'ai récemment a publié un article décrivant comment gérer les changements de configuration à l'aide de retenues Fragment s. Il résout le problème de la rétention d'un AsyncTask à travers un changement de rotation bien.

Le résumé est d'utiliser l'hôte de votre AsyncTask à l'intérieur d'un Fragment appel setRetainInstance(true) sur le Fragment et de signaler le AsyncTask Les progrès/résultats de l'entreprise sont renvoyés à l'entreprise. Activity (ou sa cible Fragment (si vous choisissez d'utiliser l'approche décrite par @Timmmm) par le biais de la retenue. Fragment .

13voto

NeTeInStEiN Points 7331

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

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

La seconde est de garder une référence de votre AsyncTask en cours d'exécution dans le fichier Application Classe - http://developer.Android.com/reference/Android/app/Application.html

Chaque fois que vous démarrez une AsyncTask, vous la définissez sur l'application et quand elle se termine, vous la définissez 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) et ensuite définir la référence à l'intérieur de ce que vous voulez (activité, fragment, etc. pour pouvoir faire des callbacks).

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

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

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

Le dialogue de progression peut suivre exactement le même principe.

4voto

Matt Wolfe Points 4123

J'ai trouvé une méthode qui consiste à utiliser les AsyncTaskLoaders pour cela. C'est assez facile à utiliser et nécessite moins d'IMO

En gros, 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 bien gérer 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 rattachez à 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 délicat à gérer. Bien que j'imagine qu'il existe de nombreuses façons de gérer ce problème, j'ai appelé loader.abandon() dans onLoadFinished. Ensuite, j'ai ajouté une vérification dans onCreate pour ne rattacher le chargeur que s'il n'était pas déjà abandonné. Si vous avez besoin des données résultantes à nouveau, vous ne voudrez pas faire cela. Dans la plupart des cas, vous voulez les données.

J'ai plus de détails sur l'utilisation de ce système pour les appels http. aquí

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X