Fragmentos de Android. Retención de una AsyncTask durante la rotación de la pantalla o el cambio de configuración


86

Estoy trabajando en una aplicación para teléfono inteligente / tableta, usando solo un APK y cargando recursos según sea necesario según el tamaño de la pantalla, la mejor opción de diseño parecía ser usar Fragmentos a través de ACL.

Esta aplicación ha funcionado bien hasta ahora y solo se basa en actividades. Esta es una clase simulada de cómo manejo AsyncTasks y ProgressDialogs en las actividades para que funcionen incluso cuando se gira la pantalla o se produce un cambio de configuración en medio de la comunicación.

No cambiaré el manifiesto para evitar la recreación de la Actividad, hay muchas razones por las que no quiero hacerlo, pero principalmente porque los documentos oficiales dicen que no se recomienda y me las he arreglado sin él hasta ahora, así que por favor no lo recomiendo. ruta.

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

Este código funciona bien, tengo alrededor de 10.000 usuarios sin quejas, por lo que parecía lógico copiar esta lógica en el nuevo Diseño Basado en Fragmentos, pero, por supuesto, no funciona.

Aquí está el 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
                }
            }
        }
    }
}

No puedo usarlo onRetainNonConfigurationInstance()ya que debe llamarse desde la Actividad y no desde el Fragmento, lo mismo ocurre con getLastNonConfigurationInstance(). He leído algunas preguntas similares aquí sin respuesta.

Entiendo que podría requerir algo de trabajo para organizar estas cosas correctamente en fragmentos, dicho esto, me gustaría mantener la misma lógica de diseño básica.

¿Cuál sería la forma correcta de retener AsyncTask durante un cambio de configuración y, si aún se está ejecutando, mostrar un progressDialog, teniendo en cuenta que AsyncTask es una clase interna del Fragmento y es el Fragmento en sí quien invoca el AsyncTask.execute? ()?


1
Tal vez el hilo sobre cómo manejar el cambio de configuración con AsyncTask pueda ayudar
rds

asociar AsyncTask con un ciclo de vida de la aplicación ... así se puede reanudar cuando se vuelve a crear la actividad
Fred Grott

Consulte mi publicación sobre este tema: Manejo de cambios de configuración con Fragments
Alex Lockwood

Respuestas:


75

Los fragmentos pueden hacer esto mucho más fácil. Simplemente use el método Fragment.setRetainInstance (boolean) para que su instancia de fragmento se mantenga a través de los cambios de configuración. Tenga en cuenta que este es el reemplazo recomendado para Activity.onRetainnonConfigurationInstance () en los documentos.

Si por alguna razón realmente no desea utilizar un fragmento retenido, existen otros enfoques que puede tomar. Tenga en cuenta que cada fragmento tiene un identificador único devuelto por Fragment.getId () . También puede averiguar si se está eliminando un fragmento para un cambio de configuración a través de Fragment.getActivity (). IsChangingConfigurations () . Entonces, en el punto en el que decidiría detener su AsyncTask (en onStop () o onDestroy () lo más probable), podría, por ejemplo, verificar si la configuración está cambiando y, de ser así, pegarla en un SparseArray estático debajo del identificador del fragmento, y luego en su onCreate () o onStart () mire para ver si tiene una AsyncTask en la matriz dispersa disponible.


Tenga en cuenta que setRetainInstance es solo si no está utilizando la pila de actividades.
Neil

4
¿No es posible que AsyncTask envíe su resultado antes de que se ejecute onCreateView del Fragmento retenido?
jakk

6
@jakk Los métodos de ciclo de vida para actividades, fragmentos, etc. son invocados secuencialmente por la cola de mensajes del hilo de la GUI principal, por lo que incluso si la tarea finalizaba simultáneamente en segundo plano antes de que estos métodos de ciclo de vida se completaran (o incluso se invocaran), el onPostExecutemétodo aún necesitaría espere antes de ser finalmente procesado por la cola de mensajes del hilo principal.
Alex Lockwood

Este enfoque (RetainInstance = true) no funcionará si desea cargar diferentes archivos de diseño para cada orientación.
Justin

Iniciar el asynctask dentro del método onCreate de MainActivity solo parece funcionar si el asynctask, dentro del fragmento "trabajador", se inicia mediante la acción explícita del usuario. Porque el hilo principal y la interfaz de usuario están disponibles. Sin embargo, iniciar el asynctask justo después de iniciar la aplicación, sin una acción del usuario como hacer clic en un botón, ofrece una excepción. En ese caso, el asynctask se puede llamar en el método onStart en MainActivity, no en el método onCreate.
ʕ ᵔᴥᵔ ʔ

66

Creo que disfrutará de mi ejemplo extremadamente completo y funcional que se detalla a continuación.

  1. La rotación funciona y el diálogo sobrevive.
  2. Puede cancelar la tarea y el diálogo presionando el botón Atrás (si desea este comportamiento).
  3. Utiliza fragmentos.
  4. El diseño del fragmento debajo de la actividad cambia correctamente cuando el dispositivo gira.
  5. Hay una descarga completa del código fuente y un APK precompilado para que pueda ver si el comportamiento es el que desea.

Editar

Según lo solicitado por Brad Larson, he reproducido la mayor parte de la solución vinculada a continuación. También desde que lo publiqué me han señalado AsyncTaskLoader. No estoy seguro de que sea totalmente aplicable a los mismos problemas, pero debería comprobarlo de todos modos.

Utilizando AsyncTaskcon cuadros de diálogo de progreso y rotación de dispositivos.

¡Una solución de trabajo!

Finalmente he conseguido que todo funcione. Mi código tiene las siguientes características:

  1. A Fragmentcuyo diseño cambia con la orientación.
  2. Un lugar AsyncTasken el que puedes trabajar.
  3. A DialogFragmentque muestra el progreso de la tarea en una barra de progreso (no solo una ruleta indeterminada).
  4. La rotación funciona sin interrumpir la tarea ni descartar el diálogo.
  5. El botón Atrás descarta el diálogo y cancela la tarea (aunque puede modificar este comportamiento con bastante facilidad).

No creo que esa combinación de funcionalidad se pueda encontrar en ningún otro lugar.

La idea básica es la siguiente. Hay una MainActivityclase que contiene un solo fragmento - MainFragment. MainFragmenttiene diferentes diseños para la orientación horizontal y vertical, y setRetainInstance()es falso para que el diseño pueda cambiar. Esto significa que cuando se cambia la orientación del dispositivo, ambos MainActivityy MainFragmentse destruyen y recrean por completo.

Por separado tenemos MyTask(extendido desde AsyncTask) que hace todo el trabajo. No podemos almacenarlo MainFragmentporque se destruirá, y Google ha dejado de usar algo como setRetainNonInstanceConfiguration(). De todos modos, eso no siempre está disponible y, en el mejor de los casos, es un truco feo. En su lugar, almacenaremos MyTasken otro fragmento, un DialogFragmentllamado TaskFragment. Este fragmento se habrá setRetainInstance()establecido en verdadero, por lo que a medida que el dispositivo gira, este fragmento no se destruye y MyTaskse retiene.

Finalmente, necesitamos decirle a TaskFragmentquién informar cuando esté terminado, y lo hacemos usando setTargetFragment(<the MainFragment>)cuando lo creamos. Cuando se gira el dispositivo y MainFragmentse destruye y se crea una nueva instancia, usamos FragmentManagerpara buscar el cuadro de diálogo (según su etiqueta) y lo hacemos setTargetFragment(<the new MainFragment>). Eso es practicamente todo.

Había otras dos cosas que tenía que hacer: primero cancelar la tarea cuando se descarta el cuadro de diálogo y, en segundo lugar, establecer el mensaje de descarte en nulo; de lo contrario, el cuadro de diálogo se descarta de forma extraña cuando se gira el dispositivo.

El código

No enumeraré los diseños, son bastante obvios y puede encontrarlos en la descarga del proyecto a continuación.

Actividad principal

Esto es bastante sencillo. Agregué una devolución de llamada a esta actividad para que sepa cuándo finaliza la tarea, pero es posible que no la necesite. Principalmente solo quería mostrar el mecanismo de devolución de llamada de actividad de fragmentos porque es bastante ordenado y es posible que no lo hayas visto antes.

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

¡Es largo pero merece la pena!

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

Mi tarea

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

Descarga el proyecto de ejemplo

Aquí está el código fuente y el APK . Lo siento, el ADT insistió en agregar la biblioteca de soporte antes de permitirme hacer un proyecto. Estoy seguro de que puedes eliminarlo.


4
Evitaría retener la barra de progreso DialogFragment, porque tiene elementos de la interfaz de usuario que contienen referencias al contexto anterior. En cambio, lo almacenaría AsyncTasken otro fragmento vacío y lo establecería DialogFragmentcomo su objetivo.
SD

¿No se borrarán esas referencias cuando se gire el dispositivo y onCreateView()se vuelva a llamar? El antiguo mProgressBarse sobrescribirá con uno nuevo al menos.
Timmmm

No explícitamente, pero estoy bastante seguro de ello. Se pueden añadir mProgressBar = null;en onDestroyView()si usted quiere estar seguro adicional. El método de Singularity puede ser una buena idea, ¡pero aumentará aún más la complejidad del código!
Timmmm

1
La referencia que tiene en el asynctask es el fragmento progressdialog, ¿verdad? así que 2 preguntas: 1- ¿Qué pasa si quiero alterar el fragmento real que llama al diálogo de progreso? 2- ¿que pasa si quiero pasar parámetros al asynctask? Saludos,
Maxrunner

1
@Maxrunner, para pasar parámetros, probablemente lo más fácil sea pasar mTask.execute()a MainFragment.onClick(). Alternativamente, puede permitir que se pasen parámetros setTask()o incluso almacenarlos en MyTasksí mismo. No estoy seguro de lo que significa tu primera pregunta, pero tal vez quieras usar TaskFragment.getTargetFragment(). Estoy bastante seguro de que funcionará con un ViewPager. Pero ViewPagersno se entienden o documentan muy bien, ¡así que buena suerte! Recuerde que su fragmento no se crea hasta que sea visible la primera vez.
Timmmm

16

Recientemente publiqué un artículo que describe cómo manejar los cambios de configuración usando Fragments retenidos . Resuelve muy bien el problema de retener un AsyncTaskcambio de rotación transversal.

El TL; DR es utilizar el host de su AsyncTaskinterior Fragment, llamar setRetainInstance(true)al Fragment, e informar el AsyncTaskprogreso / resultados de los a su Activity(o su objetivo Fragment, si elige utilizar el enfoque descrito por @Timmmm) a través del retenido Fragment.


5
¿Cómo harías con los Fragmentos anidados? Como una AsyncTask comenzada desde un RetainedFragment dentro de otro Fragment (Tab).
Rekin

Si el fragmento ya está retenido, ¿por qué no ejecutar la tarea asíncrona desde dentro de ese fragmento retenido? Si ya está retenido, la tarea asíncrona podrá informarle incluso si se produce un cambio de configuración.
Alex Lockwood

@AlexLockwood Gracias por el blog. En lugar de tener que manejar onAttachy onDetach, será mejor si está adentro TaskFragment, simplemente llamamos getActivitycada vez que necesitamos disparar una devolución de llamada. (Verificando instaceof TaskCallbacks)
Cheok Yan Cheng

1
Puedes hacerlo de cualquier manera. Simplemente lo hice onAttach()y onDetach()así pude evitar enviar constantemente la actividad a TaskCallbackscada vez que quería usarla.
Alex Lockwood

1
@AlexLockwood Si mi aplicación sigue una sola actividad: diseño de múltiples fragmentos, ¿debería tener un fragmento de tarea separado para cada uno de mis fragmentos de IU? Básicamente, el ciclo de vida de cada fragmento de tarea será administrado por su fragmento de destino y no habrá comunicación con la actividad.
Manas Bajaj

13

Mi primera sugerencia es evitar AsyncTasks internas , puede leer una pregunta que hice sobre esto y las respuestas: Android: recomendaciones de AsyncTask: ¿clase privada o clase pública?

Después de eso, comencé a usar non-inner y ... ahora veo MUCHOS beneficios.

El segundo es, mantenga una referencia de su AsyncTask en ejecución en la Applicationclase: http://developer.android.com/reference/android/app/Application.html

Cada vez que inicie una AsyncTask, configúrela en la Aplicación y cuando termine, configúrela como nula.

Cuando se inicia un fragmento / actividad, puede verificar si se está ejecutando alguna AsyncTask (verificando si es nula o no en la Aplicación) y luego establecer la referencia interna a lo que desee (actividad, fragmento, etc. para que pueda hacer devoluciones de llamada).

Esto resolverá su problema: si solo tiene 1 AsyncTask ejecutándose en un momento determinado, puede agregar una referencia simple:

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

De lo contrario, tenga en la Aplicación un HashMap con referencias a ellos.

El diálogo de progreso puede seguir exactamente el mismo principio.


2
Estuve de acuerdo, siempre que vincule el ciclo de vida de AsyncTask a su padre (al definir AsyncTask como una clase interna de Actividad / Fragmento), es bastante difícil hacer que su AsyncTask escape de la recreación del ciclo de vida de su padre, sin embargo, no me gusta su solución, se ve tan hacky.
yorkw

La pregunta es ... ¿tienes una mejor solución?
neteinstein

1
Voy a tener que estar de acuerdo con @yorkw aquí, esta solución se me presentó hace un tiempo cuando estaba lidiando con este problema sin usar fragmentos (aplicación basada en actividades). Esta pregunta: stackoverflow.com/questions/2620917/… tiene la misma respuesta, y estoy de acuerdo con uno de los comentarios que dice "La instancia de la aplicación tiene su propio ciclo de vida; el sistema operativo también puede eliminarla, por lo que esta solución puede causar un error difícil de reproducir "
Blindstuff

1
Aún así, no veo ninguna otra forma que sea menos "hacky" como dijo @yorkw. Lo he estado usando en varias aplicaciones, y con algo de atención a los posibles problemas, todo funciona muy bien.
neteinstein

Tal vez la solución @hackbod sea más adecuada para ti.
neteinstein

4

Se me ocurrió un método para usar AsyncTaskLoaders para esto. Es bastante fácil de usar y requiere menos gastos generales en mi opinión.

Básicamente, creas un AsyncTaskLoader como este:

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

Luego, en su actividad que usa el AsyncTaskLoader anterior cuando se hace clic en un botón:

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

    }
}

Esto parece manejar bien los cambios de orientación y su tarea en segundo plano continuará durante la rotación.

Algunas cosas a tener en cuenta:

  1. Si en onCreate se vuelve a conectar al asynctaskloader, se le volverá a llamar en onLoadFinished () con el resultado anterior (incluso si ya le habían dicho que la solicitud estaba completa). En realidad, este es un buen comportamiento la mayor parte del tiempo, pero a veces puede ser complicado de manejar. Aunque imagino que hay muchas formas de manejar esto, lo que hice fue llamar loader.abandon () en onLoadFinished. Luego agregué check in onCreate para volver a conectarlo al cargador si aún no estaba abandonado. Si necesita los datos resultantes nuevamente, no querrá hacerlo. En la mayoría de los casos, desea los datos.

Tengo más detalles sobre el uso de esto para llamadas http aquí.


¿Seguro que getSupportLoaderManager().getLoader(0);no devolverá nulo (porque el cargador con esa identificación, 0, aún no existe)?
EmmanuelMess

1
Sí, será nulo a menos que un cambio de configuración provoque que la actividad se reinicie mientras el cargador estaba en progreso. Es por eso que tuve la verificación de nulo.
Matt Wolfe

3

Creé una biblioteca de tareas en segundo plano de código abierto muy pequeña que se basa en gran medida en Marshmallow AsyncTaskpero con funciones adicionales como:

  1. Retención automática de tareas a través de cambios de configuración;
  2. Devolución de llamada de UI (oyentes);
  3. No reinicia ni cancela la tarea cuando el dispositivo gira (como lo harían los cargadores);

La biblioteca utiliza internamente una Fragmentinterfaz de usuario sin ningún tipo de interfaz de usuario, que se conserva tras los cambios de configuración ( setRetainInstance(true)).

Puede encontrarlo en GitHub: https://github.com/NeoTech-Software/Android-Retainable-Tasks

Ejemplo más básico (versión 0.2.0):

Este ejemplo retiene completamente la tarea, usando una cantidad muy limitada de código.

Tarea:

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

Actividad:

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

1

Mi enfoque es utilizar el patrón de diseño de delegación, en general, podemos aislar la lógica empresarial real (leer datos de Internet o de una base de datos o lo que sea) de AsyncTask (el delegador) a BusinessDAO (el delegado), en su método AysncTask.doInBackground () , delegue la tarea real a BusinessDAO, luego implemente un mecanismo de proceso singleton en BusinessDAO, de modo que las llamadas múltiples a BusinessDAO.doSomething () solo activen una tarea real ejecutándose cada vez y esperando el resultado de la tarea. La idea es retener al delegado (es decir, BusinessDAO) durante el cambio de configuración, en lugar del delegador (es decir, AsyncTask).

  1. Cree / implemente nuestra propia aplicación, el propósito es crear / inicializar BusinessDAO aquí, de modo que el ciclo de vida de nuestro BusinessDAO tenga un alcance de aplicación, no de actividad, tenga en cuenta que necesita cambiar AndroidManifest.xml para usar 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. Nuestra Activity / Fragement existente no ha cambiado en su mayoría, aún implementa AsyncTask como una clase interna e involucra AsyncTask.execute () de Activity / Fragement, la diferencia ahora es que AsyncTask delegará la tarea real a BusinessDAO, por lo que durante el cambio de configuración, una segunda AsyncTask se inicializará y ejecutará, y llamará a BusinessDAO.doSomething () por segunda vez; sin embargo, la segunda llamada a BusinessDAO.doSomething () no activará una nueva tarea en ejecución, sino que esperará a que finalice la tarea en ejecución actual:

    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. Dentro de BusinessDAO, implemente el mecanismo de proceso singleton, por ejemplo:

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

No estoy 100% seguro de si esto funcionará; además, el fragmento de código de muestra debe considerarse como pseudocódigo. Solo estoy tratando de darte alguna pista desde el nivel de diseño. Cualquier comentario o sugerencia es bienvenido y apreciado.


Parece una solución muy bonita. Desde que respondió esto hace aproximadamente 2 años y medio, ¿lo probó desde entonces? ¡¡Estás diciendo que no estoy seguro de que esté funcionando, la cosa es que yo tampoco !! Estoy buscando una solución bien probada para este problema. ¿Tienes alguna sugerencia?
Alireza A. Ahmadi

1

Puede hacer que AsyncTask sea un campo estático. Si necesita un contexto, debe enviar el contexto de su aplicación. Esto evitará pérdidas de memoria; de lo contrario, mantendría una referencia a toda su actividad.


1

Si alguien encuentra el camino a este hilo, encontré que un enfoque limpio era ejecutar la tarea Async desde un app.Service(comenzó con START_STICKY) y luego volver a crear iterar sobre los servicios en ejecución para averiguar si el servicio (y, por lo tanto, la tarea asincrónica) todavía está corriendo;

    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 es así, vuelva a agregar el DialogFragment(o lo que sea) y si no es así, asegúrese de que el cuadro de diálogo se haya descartado.

Esto es particularmente pertinente si está utilizando las v4.support.*bibliotecas ya que (en el momento de escribir este artículo) tienen problemas conocidos con el setRetainInstancemétodo y la paginación de la vista. Además, al no retener la instancia, puede recrear su actividad utilizando un conjunto diferente de recursos (es decir, un diseño de vista diferente para la nueva orientación)


¿No es excesivo ejecutar un servicio solo para retener una AsyncTask? Un Servicio se ejecuta en su propio proceso y eso no viene sin costos adicionales.
WeNeigh

Interesante Vinay. No he notado que la aplicación sea más pesada en recursos (de todos modos, es bastante liviana en este momento). ¿Qué has encontrado? Veo un servicio como un entorno predecible para permitir que el sistema continúe con un trabajo pesado o E / S independientemente del estado de la interfaz de usuario. Comunicarse con el servicio para ver cuándo se ha completado algo parecía "correcto". Los servicios que comienzo a ejecutar un poco de trabajo se detienen al finalizar la tarea, por lo que generalmente sobreviven entre 10 y 30 segundos.
BrantApps

La respuesta de Commonsware aquí parece sugerir que los Servicios son una mala idea. Ahora estoy considerando AsyncTaskLoaders pero parecen tener sus propios problemas (inflexibilidad, solo para la carga de datos, etc.)
WeNeigh

1
Veo. Notablemente, este servicio al que se vinculó se estaba configurando explícitamente para ejecutarse en su propio proceso. Al maestro no pareció gustarle que este patrón se usara con frecuencia. No he proporcionado explícitamente esas propiedades de "ejecutar en un nuevo proceso cada vez", así que espero estar aislado de esa parte de la crítica. Me esforzaré por cuantificar los efectos. Los servicios como concepto, por supuesto, no son 'malas ideas' y son fundamentales para que cada aplicación haga algo remotamente interesante, sin juego de palabras. Su JDoc proporciona más orientación sobre su uso si aún no está seguro.
BrantApps

0

Escribo el mismo código para resolver este problema

El primer paso es hacer la clase de aplicación:

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

En 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">

Código en actividad:

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

}

Cuando la orientación de la actividad cambia, la variable mTask se inicia desde el contexto de la aplicación. Cuando la tarea finaliza, la variable se establece en nula y se elimina de la memoria.

Para mi es suficiente.


0

Eche un vistazo al siguiente ejemplo, cómo usar el fragmento retenido para retener la tarea en segundo plano:

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

    }
}

-1

Echa un vistazo aquí .

Existe una solución basada en la solución de Timmmm .

Pero lo mejoré:

  • Ahora la solución es ampliable, solo necesita ampliar FragmentAbleToStartTask

  • Puede seguir ejecutando varias tareas al mismo tiempo.

    Y en mi opinión, es tan fácil como iniciarActivityForResult y recibir el resultado

  • También puede detener una tarea en ejecución y verificar si se está ejecutando una tarea en particular

Lo siento por mi ingles


primer enlace está roto
Tejzeratul
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.