Tarea en segundo plano, diálogo de progreso, cambio de orientación: ¿hay alguna solución que funcione al 100%?

235

Descargo algunos datos de Internet en el hilo de fondo (uso AsyncTask) y visualizo un cuadro de diálogo de progreso durante la descarga. La orientación cambia, la actividad se reinicia y luego mi AsyncTask se completa: quiero cerrar el cuadro de diálogo de progreso e iniciar una nueva actividad. Pero llamar a despedirDialog a veces arroja una excepción (probablemente porque la Actividad fue destruida y la nueva Actividad aún no se ha iniciado).

¿Cuál es la mejor manera de manejar este tipo de problema (actualizar la interfaz de usuario desde el hilo de fondo que funciona incluso si el usuario cambia de orientación)? ¿Alguien de Google proporcionó alguna "solución oficial"?

fhucho
fuente
44
Mi publicación de blog sobre este tema podría ayudar. Se trata de retener tareas de larga duración a través de cambios de configuración.
Alex Lockwood
1
Esta pregunta también está relacionada.
Alex Lockwood
Solo FTR hay un misterio relacionado aquí ... stackoverflow.com/q/23742412/294884
Fattie

Respuestas:

336

Paso # 1: Presente su AsyncTaskuna staticclase anidada, o una clase completamente separada, pero no una (anidada no estática) de clase interna.

Paso # 2: AsyncTaskMantén el control a Activitytravés de un miembro de datos, establece a través del constructor y un establecedor.

Paso # 3: Al crear el AsyncTask, suministre la corriente Activityal constructor.

Paso # 4: En onRetainNonConfigurationInstance(), devuelva el AsyncTask, después de separarlo de la actividad original, que ahora se va.

Paso # 5: onCreate()si getLastNonConfigurationInstance()no lo está null, envíalo a tu AsyncTaskclase y llama a tu setter para asociar tu nueva actividad con la tarea.

Paso # 6: No se refiera al miembro de datos de actividad de doInBackground().

Si sigues la receta anterior, todo funcionará. onProgressUpdate()y onPostExecute()se suspenden entre el inicio onRetainNonConfigurationInstance()y el final del siguiente onCreate().

Aquí hay un proyecto de muestra que demuestra la técnica.

Otro enfoque es deshacerse del AsyncTasky mover su trabajo a un IntentService. Esto es particularmente útil si el trabajo a realizar puede ser largo y debe continuar independientemente de lo que el usuario haga en términos de actividades (por ejemplo, descargar un archivo grande). Puede usar una transmisión ordenada Intentpara que la actividad responda al trabajo que se está realizando (si todavía está en primer plano) o genere una Notificationpara informar al usuario si el trabajo se ha realizado. Aquí hay una publicación de blog con más información sobre este patrón.

CommonsWare
fuente
8
¡Muchas gracias por su excelente respuesta a este problema común! Para ser exhaustivo, puede agregar al paso 4 que tenemos que separar (establecer en nulo) la actividad en AsyncTask. Sin embargo, esto está bien ilustrado en el proyecto de muestra.
Kevin Gaudin
3
Pero, ¿qué sucede si necesito tener acceso a los miembros de Activity?
Eugene
3
@ Andrew: Cree una clase interna estática o algo que se aferre a varios objetos y devuélvala.
CommonsWare
11
onRetainNonConfigurationInstance()está en desuso y la alternativa sugerida es usar setRetainInstance(), pero no devuelve un objeto. ¿Es posible manejar asyncTaskel cambio de configuración con setRetainInstance()?
Indrek Kõue
10
@SYLARRR: Absolutamente. Tener la Fragmentbodega el AsyncTask. Tener el Fragmentllamado setRetainInstance(true)sobre sí mismo. Tener la AsyncTaskúnica charla con el Fragment. Ahora, en un cambio de configuración, Fragmentno se destruye ni se recrea (aunque la actividad sí lo sea), por lo que AsyncTaskse retiene durante el cambio de configuración.
CommonsWare
13

La respuesta aceptada fue muy útil, pero no tiene un diálogo de progreso.

Afortunadamente para usted, lector, ¡he creado un ejemplo extremadamente completo y funcional de una AsyncTask con un diálogo de progreso !

  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.
Timmmm
fuente
La respuesta aceptada es sobre clases estáticas (no miembros). Y eso es necesario para evitar que AsyncTask tenga un puntero (oculto) a la instancia de clase externa que se convierte en una pérdida de memoria al destruir la actividad.
Bananeweizen
Sí, no estoy seguro de por qué pongo eso sobre los miembros estáticos, ya que en realidad también los usé ... raro. Respuesta editada
Timmmm
¿Podrías actualizar tu enlace? Realmente necesito esto.
Romain Pellerin
Lo siento, no he podido restaurar mi sitio web, ¡lo haré pronto! Pero mientras tanto, es básicamente el mismo que el código en esta respuesta: stackoverflow.com/questions/8417885/…
Timmmm
1
El enlace es falso; solo conduce a un índice inútil sin indicación de dónde está el código.
FractalBob
9

Trabajé durante una semana para encontrar una solución a este dilema sin recurrir a la edición del archivo de manifiesto. Los supuestos para esta solución son:

  1. Siempre necesitas usar un diálogo de progreso
  2. Solo se realiza una tarea a la vez
  3. Necesita que la tarea persista cuando se gira el teléfono y el diálogo de progreso se cierra automáticamente.

Implementación

Deberá copiar los dos archivos que se encuentran al final de esta publicación en su espacio de trabajo. Solo asegúrate de que:

  1. Todos tus Activitymensajes deben extenderseBaseActivity

  2. En onCreate(), super.onCreate()debe llamarse después de inicializar los miembros a los que su ASyncTasks debe acceder . Además, anule getContentViewId()para proporcionar la identificación de diseño del formulario.

  3. Anular onCreateDialog() como de costumbre para crear diálogos gestionados por la actividad.

  4. Consulte el código a continuación para ver una clase interna estática de muestra para realizar sus AsyncTasks. Puede almacenar su resultado en mResult para acceder más tarde.


final static class MyTask extends SuperAsyncTask<Void, Void, Void> {

    public OpenDatabaseTask(BaseActivity activity) {
        super(activity, MY_DIALOG_ID); // change your dialog ID here...
                                       // and your dialog will be managed automatically!
    }

    @Override
    protected Void doInBackground(Void... params) {

        // your task code

        return null;
    }

    @Override
    public boolean onAfterExecute() {
        // your after execute code
    }
}

Y finalmente, para lanzar tu nueva tarea:

mCurrentTask = new MyTask(this);
((MyTask) mCurrentTask).execute();

¡Eso es! Espero que esta solución robusta ayude a alguien.

BaseActivity.java (organice las importaciones usted mismo)

protected abstract int getContentViewId();

public abstract class BaseActivity extends Activity {
    protected SuperAsyncTask<?, ?, ?> mCurrentTask;
    public HashMap<Integer, Boolean> mDialogMap = new HashMap<Integer, Boolean>();

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

        setContentView(getContentViewId());

        mCurrentTask = (SuperAsyncTask<?, ?, ?>) getLastNonConfigurationInstance();
        if (mCurrentTask != null) {
            mCurrentTask.attach(this);
            if (mDialogMap.get((Integer) mCurrentTask.dialogId) != null
                && mDialogMap.get((Integer) mCurrentTask.dialogId)) {
        mCurrentTask.postExecution();
            }
        }
    }

    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
    super.onPrepareDialog(id, dialog);

        mDialogMap.put(id, true);
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        if (mCurrentTask != null) {
            mCurrentTask.detach();

            if (mDialogMap.get((Integer) mCurrentTask.dialogId) != null
                && mDialogMap.get((Integer) mCurrentTask.dialogId)) {
                return mCurrentTask;
            }
        }

        return super.onRetainNonConfigurationInstance();
    }

    public void cleanupTask() {
        if (mCurrentTask != null) {
            mCurrentTask = null;
            System.gc();
        }
    }
}

SuperAsyncTask.java

public abstract class SuperAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
    protected BaseActivity mActivity = null;
    protected Result mResult;
    public int dialogId = -1;

    protected abstract void onAfterExecute();

    public SuperAsyncTask(BaseActivity activity, int dialogId) {
        super();
        this.dialogId = dialogId;
        attach(activity);
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        mActivity.showDialog(dialogId); // go polymorphism!
    }    

    protected void onPostExecute(Result result) {
        super.onPostExecute(result);
        mResult = result;

        if (mActivity != null &&
                mActivity.mDialogMap.get((Integer) dialogId) != null
                && mActivity.mDialogMap.get((Integer) dialogId)) {
            postExecution();
        }
    };

    public void attach(BaseActivity activity) {
        this.mActivity = activity;
    }

    public void detach() {
        this.mActivity = null;
    }

    public synchronized boolean postExecution() {
        Boolean dialogExists = mActivity.mDialogMap.get((Integer) dialogId);
        if (dialogExists != null || dialogExists) {
            onAfterExecute();
            cleanUp();
    }

    public boolean cleanUp() {
        mActivity.removeDialog(dialogId);
        mActivity.mDialogMap.remove((Integer) dialogId);
        mActivity.cleanupTask();
        detach();
        return true;
    }
}
Oleg Vaskevich
fuente
4

¿Alguien de Google proporcionó alguna "solución oficial"?

Si.

La solución es más una propuesta de arquitectura de aplicación que un código .

Propusieron 3 patrones de diseño que permiten que una aplicación funcione sincronizada con un servidor, independientemente del estado de la aplicación (funcionará incluso si el usuario finaliza la aplicación, el usuario cambia de pantalla, la aplicación se termina, cualquier otro estado posible donde una operación de datos de fondo podría interrumpirse, esto lo cubre)

La propuesta se explica en el discurso de las aplicaciones de cliente REST de Android durante Google I / O 2010 por Virgil Dobjanschi. Dura 1 hora, pero vale la pena verlo.

La base de esto es abstraer las operaciones de red a una Serviceque funciona de forma independiente a cualquiera Activityen la aplicación. Si está trabajando con bases de datos, el uso de ContentResolvery Cursorle proporcionaría un patrón de Observador listo para usar que es conveniente para actualizar la IU sin ninguna lógica adicional, una vez que haya actualizado su base de datos local con los datos remotos recuperados. Cualquier otro código posterior a la operación se ejecutará a través de una devolución de llamada pasada a Service(yo uso una ResultReceiversubclase para esto).

De todos modos, mi explicación es bastante vaga, definitivamente deberías ver el discurso.

Christopher Francisco
fuente
2

Si bien la respuesta de Mark (CommonsWare) realmente funciona para los cambios de orientación, falla si la Actividad se destruye directamente (como en el caso de una llamada telefónica).

Puede manejar los cambios de orientación Y los raros eventos de Actividad destruidos utilizando un objeto Aplicación para hacer referencia a su ASyncTask.

Hay una excelente explicación del problema y la solución aquí :

El crédito va completamente a Ryan por resolver esto.

Scott Biggs
fuente
1

Después de 4 años, Google resolvió el problema simplemente llamando a setRetainInstance (true) en Activity onCreate. Conservará su instancia de actividad durante la rotación del dispositivo. También tengo una solución simple para Android anterior.

Singagirl
fuente
1
El problema que observa la gente ocurre porque Android destruye una clase de actividad en rotación, extensión de teclado y otros eventos, pero una tarea asincrónica aún mantiene una referencia para la instancia destruida e intenta usarla para actualizaciones de la interfaz de usuario. Puede indicar a Android que no destruya la actividad ni en forma manifiesta ni pragmática. En este caso, una referencia de tarea asíncrona sigue siendo válida y no se observa ningún problema. Dado que la rotación puede requerir un trabajo adicional como volver a cargar las vistas, etc., Google no recomienda preservar la actividad. Entonces tú decides.
Singagirl
Gracias, sabía sobre la situación pero no sobre setRetainInstance (). Lo que no entiendo es su afirmación de que Google usó esto para resolver los problemas planteados en la pregunta. ¿Puedes vincular la fuente de información? Gracias.
jj_
onRetainNonConfigurationInstance () Esta función se llama puramente como una optimización, y no debe confiar en que se llame. <De la misma fuente: developer.android.com/reference/android/app/…
Dhananjay M
0

debe llamar a todas las acciones de actividad utilizando el controlador de actividad. Por lo tanto, si está en algún hilo, debe crear un Runnable y publicarlo utilizando el controlador de Activitie. De lo contrario, su aplicación se bloqueará a veces con una excepción fatal.

xpepermint
fuente
0

Esta es mi solución: https://github.com/Gotchamoh/Android-AsyncTask-ProgressDialog

Básicamente los pasos son:

  1. Utilizo onSaveInstanceStatepara guardar la tarea si aún se está procesando.
  2. En onCreateMe sale la tarea si se guardó.
  3. En onPausedescarto el ProgressDialogsi se muestra.
  4. En onResumeMuestro el ProgressDialogsi la tarea aún se está procesando.
Gotcha
fuente