AsyncTask
es una gran cosa para ejecutar tareas complejas en otro hilo.
Pero cuando hay un cambio de orientación u otra configuración cambia mientras AsyncTask
aún se está ejecutando, la Activity
actual se destruye y se reinicia. Y como la instancia de AsyncTask
está conectada a esa actividad, falla y causa una ventana de mensaje de “cierre forzado”.
Por lo tanto, estoy buscando algún tipo de “mejores prácticas” para evitar estos errores y evitar que AsyncTask falle.
Lo que he visto hasta ahora es:
onRetainNonConfigurationInstance
Activity
y la reinicia cuando la Activity
se crea nuevamente. Algunos ejemplos de código:
Android AsyncTasks durante una rotación de pantalla, Parte I y Parte II
ShelvesActivity.java
¿Puede ayudarme a encontrar el mejor enfoque que resuelva mejor el problema y también sea fácil de implementar? El código en sí también es importante, ya que no sé cómo resolverlo correctamente.
NO use android:configChanges
para solucionar este problema. Esta es una muy mala práctica.
NO use Activity#onRetainNonConfigurationInstance()
tampoco. Esto es menos modular y no es adecuado para aplicaciones basadas en Fragment
.
Puede leer mi artículo describiendo cómo manejar los cambios de configuración utilizando Fragment
retenidos. Resuelve muy bien el problema de conservar una AsyncTask
en un cambio de rotación. Básicamente, necesitas alojar tu AsyncTask
dentro de un Fragment
, invocar setRetainInstance(true)
en el Fragment
e informar el progreso / los resultados de la AsyncTask
a su Activity
través del Fragment
retenido.
Normalmente resuelvo esto haciendo que mis AsyncTasks transmitan Intenciones en la callback .onPostExecute (), por lo que no modifican la Actividad que las inició directamente. Las Actividades escuchan estas transmisiones con BroadcastReceivers dynamics y actúan en consecuencia.
De esta forma, las AsyncTasks no tienen que preocuparse por la instancia de actividad específica que maneja su resultado. Simplemente “gritan” cuando terminan, y si una Actividad está alrededor de ese momento (está activa y enfocada / está en su estado reanudado) que está interesada en los resultados de la tarea, entonces se manejará.
Esto implica un poco más sobrecarga, ya que el tiempo de ejecución necesita manejar la transmisión, pero normalmente no me importa. Creo que usar el LocalBroadcastManager en lugar del sistema predeterminado de ancho acelera un poco las cosas.
Aquí hay otro ejemplo de una AsyncTask que usa un Fragment
para manejar los cambios de configuración de tiempo de ejecución (como cuando el usuario gira la pantalla) con setRetainInstance(true)
. También se demuestra una barra de progreso determinada (regularmente actualizada).
El ejemplo se basa en parte en los documentos oficiales, Retener un objeto durante un cambio de configuración .
En este ejemplo, el trabajo que requiere un hilo de fondo es la mera carga de una imagen de Internet en la interfaz de usuario.
Parece que Alex Lockwood tiene razón cuando se trata de manejar cambios en la configuración del tiempo de ejecución con AsyncTasks usando un “Fragmento Retenido” es la mejor práctica. onRetainNonConfigurationInstance()
queda obsoleto en Lint, en Android Studio. Los documentos oficiales nos advierten usando android:configChanges
, desde Manejo de la configuración Change Yourself , …
Manejar el cambio de configuración usted mismo puede hacer que sea mucho más difícil usar recursos alternativos, porque el sistema no los aplica automáticamente. Esta técnica debe considerarse un último recurso cuando debe evitar reinicios debido a un cambio de configuración y no se recomienda para la mayoría de las aplicaciones.
Luego está la cuestión de si uno debe usar una AsyncTask para el hilo de fondo.
La referencia oficial para AsyncTask advierte …
AsyncTasks debería utilizarse idealmente para operaciones cortas (unos segundos como máximo). Si necesita mantener los hilos en ejecución durante largos periodos de tiempo, se recomienda encarecidamente que utilice las diversas API proporcionadas por el paquete java.util.concurrent, como Ejecutor, ThreadPoolExecutor y FutureTask.
Alternativamente, uno podría usar un servicio, un cargador (usando un CursorLoader o AsyncTaskLoader) o un proveedor de contenido para realizar operaciones asincrónicas.
Rompo el rest de la publicación en:
Comience con una AsyncTask básica como una clase interna de una actividad (no necesita ser una clase interna, pero probablemente sea conveniente serlo). En esta etapa, AsyncTask no maneja los cambios en la configuración del tiempo de ejecución.
public class ThreadsActivity extends ActionBarActivity { private ImageView mPictureImageView; private class LoadImageFromNetworkAsyncTask extends AsyncTask { @Override protected Bitmap doInBackground(String... urls) { return loadImageFromNetwork(urls[0]); } @Override protected void onPostExecute(Bitmap bitmap) { mPictureImageView.setImageBitmap(bitmap); } } /** * Requires in AndroidManifext.xml * */ private Bitmap loadImageFromNetwork(String url) { Bitmap bitmap = null; try { bitmap = BitmapFactory.decodeStream((InputStream) new URL(url).getContent()); } catch (Exception e) { e.printStackTrace(); } return bitmap; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_threads); mPictureImageView = (ImageView) findViewById(R.id.imageView_picture); } public void getPicture(View view) { new LoadImageFromNetworkAsyncTask() .execute("http://sofes.miximages.com/android/SikTbWe.jpg"); } }
Agregue una clase anidada RetainedFragment que amplíe la clase Fragement y no tenga su propia IU. Agregue setRetainInstance (verdadero) al evento onCreate de este Fragmento. Proporcione procedimientos para establecer y obtener sus datos.
public class ThreadsActivity extends Activity { private ImageView mPictureImageView; private RetainedFragment mRetainedFragment = null; ... public static class RetainedFragment extends Fragment { private Bitmap mBitmap; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // The key to making data survive // runtime configuration changes. setRetainInstance(true); } public Bitmap getData() { return this.mBitmap; } public void setData(Bitmap bitmapToRetain) { this.mBitmap = bitmapToRetain; } } private class LoadImageFromNetworkAsyncTask extends AsyncTask { ....
En el extremo externo de la Clase de Actividad, onCreate () maneja el Fragmento Retenido: Referenciarlo si ya existe (en caso de que la Actividad se reinicie); crear y agregar si no existe; Luego, si ya existió, obtenga datos de RetainedFragment y configure su UI con esa información.
public class ThreadsActivity extends Activity { ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_threads); final String retainedFragmentTag = "RetainedFragmentTag"; mPictureImageView = (ImageView) findViewById(R.id.imageView_picture); mLoadingProgressBar = (ProgressBar) findViewById(R.id.progressBar_loading); // Find the RetainedFragment on Activity restarts FragmentManager fm = getFragmentManager(); // The RetainedFragment has no UI so we must // reference it with a tag. mRetainedFragment = (RetainedFragment) fm.findFragmentByTag(retainedFragmentTag); // if Retained Fragment doesn't exist create and add it. if (mRetainedFragment == null) { // Add the fragment mRetainedFragment = new RetainedFragment(); fm.beginTransaction() .add(mRetainedFragment, retainedFragmentTag).commit(); // The Retained Fragment exists } else { mPictureImageView .setImageBitmap(mRetainedFragment.getData()); } }
Iniciar el AsyncTask desde la interfaz de usuario
public void getPicture(View view) { new LoadImageFromNetworkAsyncTask().execute( "http://sofes.miximages.com/android/SikTbWe.jpg"); }
Agregue y codifique una barra de progreso determinada:
Diseño de la actividad.
La actividad con: clase interna AsyncTask subclasificada; clase interna RetainedFragment subclasificada que maneja los cambios de configuración de tiempo de ejecución (por ejemplo, cuando el usuario gira la pantalla); y una barra de progreso determinada que se actualiza a intervalos regulares. …
public class ThreadsActivity extends Activity { private ImageView mPictureImageView; private RetainedFragment mRetainedFragment = null; private ProgressBar mLoadingProgressBar; public static class RetainedFragment extends Fragment { private Bitmap mBitmap; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // The key to making data survive runtime configuration changes. setRetainInstance(true); } public Bitmap getData() { return this.mBitmap; } public void setData(Bitmap bitmapToRetain) { this.mBitmap = bitmapToRetain; } } private class LoadImageFromNetworkAsyncTask extends AsyncTask { @Override protected Bitmap doInBackground(String... urls) { // Simulate a burdensome load. int sleepSeconds = 4; for (int i = 1; i < = sleepSeconds; i++) { SystemClock.sleep(1000); // milliseconds publishProgress(i * 20); // Adjust for a scale to 100 } return com.example.standardapplibrary.android.Network .loadImageFromNetwork( urls[0]); } @Override protected void onProgressUpdate(Integer... progress) { mLoadingProgressBar.setProgress(progress[0]); } @Override protected void onPostExecute(Bitmap bitmap) { publishProgress(100); mRetainedFragment.setData(bitmap); mPictureImageView.setImageBitmap(bitmap); mLoadingProgressBar.setVisibility(View.INVISIBLE); publishProgress(0); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_threads); final String retainedFragmentTag = "RetainedFragmentTag"; mPictureImageView = (ImageView) findViewById(R.id.imageView_picture); mLoadingProgressBar = (ProgressBar) findViewById(R.id.progressBar_loading); // Find the RetainedFragment on Activity restarts FragmentManager fm = getFragmentManager(); // The RetainedFragment has no UI so we must reference it with a tag. mRetainedFragment = (RetainedFragment) fm.findFragmentByTag( retainedFragmentTag); // if Retained Fragment doesn't exist create and add it. if (mRetainedFragment == null) { // Add the fragment mRetainedFragment = new RetainedFragment(); fm.beginTransaction().add(mRetainedFragment, retainedFragmentTag).commit(); // The Retained Fragment exists } else { mPictureImageView.setImageBitmap(mRetainedFragment.getData()); } } public void getPicture(View view) { mLoadingProgressBar.setVisibility(View.VISIBLE); new LoadImageFromNetworkAsyncTask().execute( "http://sofes.miximages.com/android/SikTbWe.jpg"); } public void clearPicture(View view) { mRetainedFragment.setData(null); mPictureImageView.setImageBitmap(null); } }
En este ejemplo, la función de la biblioteca (a la que se hace referencia anteriormente con el prefijo explícito del paquete com.example.standardapplibrary.android.Network) que hace un trabajo real ...
public static Bitmap loadImageFromNetwork(String url) { Bitmap bitmap = null; try { bitmap = BitmapFactory.decodeStream((InputStream) new URL(url) .getContent()); } catch (Exception e) { e.printStackTrace(); } return bitmap; }
Agregue los permisos que necesita su tarea de fondo al AndroidManifest.xml ...
...
Agregue su actividad a AndroidManifest.xml ...
...
Recientemente, he encontrado una buena solución aquí . Se basa en guardar un objeto de tarea a través de RetainConfiguration. Desde mi punto de vista, la solución es muy elegante y en cuanto a mí, he comenzado a usarla. Solo necesita anidar su asynctask desde la base de tareas y eso es todo.
Basado en @Alex Lockwood answer y en @William & @quickdraw mcgraw respuestas en esta publicación: Cómo manejar los mensajes de Handler cuando actividad / fragmento está en pausa , escribí una solución genérica.
De esta forma se maneja la rotación, y si la actividad pasa a segundo plano durante la ejecución de la tarea asincrónica, la actividad recibirá las devoluciones de llamada (onPreExecute, onProgressUpdate, onPostExecute & onCancelled) una vez reasumido, por lo que no se lanzará IllegalStateException (consulte Cómo manejar Handler mensajes cuando actividad / fragmento está en pausa ).
Sería genial tener el mismo pero con tipos de argumentos generics, como una AsyncTask (por ejemplo: AsyncTaskFragment
El código:
import android.app.Activity; import android.os.AsyncTask; import android.os.Bundle; import android.os.Message; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v7.app.AppCompatActivity; public class AsyncTaskFragment extends Fragment { /* ------------------------------------------------------------------------------------------ */ // region Classes & Interfaces public static abstract class Task extends AsyncTask
Necesitarás PauseHandler:
import android.app.Activity; import android.os.Handler; import android.os.Message; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Message Handler class that supports buffering up of messages when the activity is paused ie in the background. * * https://stackoverflow.com/questions/8040280/how-to-handle-handler-messages-when-activity-fragment-is-paused */ public abstract class PauseHandler extends Handler { /** * Message Queue Buffer */ private final List messageQueueBuffer = Collections.synchronizedList(new ArrayList ()); /** * Flag indicating the pause state */ private Activity activity; /** * Resume the handler. */ public final synchronized void resume(Activity activity) { this.activity = activity; while (messageQueueBuffer.size() > 0) { final Message msg = messageQueueBuffer.get(0); messageQueueBuffer.remove(0); sendMessage(msg); } } /** * Pause the handler. */ public final synchronized void pause() { activity = null; } /** * Store the message if we have been paused, otherwise handle it now. * * @param msg Message to handle. */ @Override public final synchronized void handleMessage(Message msg) { if (activity == null) { final Message msgCopy = new Message(); msgCopy.copyFrom(msg); messageQueueBuffer.add(msgCopy); } else { processMessage(activity, msg); } } /** * Notification message to be processed. This will either be directly from * handleMessage or played back from a saved message when the activity was * paused. * * @param activity Activity owning this Handler that isn't currently paused. * @param message Message to be handled */ protected abstract void processMessage(Activity activity, Message message); }
Uso de muestra:
public class TestActivity extends AppCompatActivity implements AsyncTaskFragmentListener { private final static String ASYNC_TASK_FRAGMENT_A = "ASYNC_TASK_FRAGMENT_A"; private final static String ASYNC_TASK_FRAGMENT_B = "ASYNC_TASK_FRAGMENT_B"; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Button testButton = (Button) findViewById(R.id.test_button); final AsyncTaskFragment fragment = AsyncTaskFragment.getRetainedOrNewFragment(TestActivity.this, ASYNC_TASK_FRAGMENT_A); testButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if(!fragment.isRunning()) { fragment.setTask(new Task() { @Override protected Object doInBackground(Object... objects) { // Do your async stuff return null; } }); fragment.execute(); } } }); } @Override public void onPreExecute(String fragmentTag) {} @Override public void onProgressUpdate(String fragmentTag, Float percent) {} @Override public void onCancelled(String fragmentTag) {} @Override public void onPostExecute(String fragmentTag, Object result) { switch (fragmentTag) { case ASYNC_TASK_FRAGMENT_A: { // Handle ASYNC_TASK_FRAGMENT_A break; } case ASYNC_TASK_FRAGMENT_B: { // Handle ASYNC_TASK_FRAGMENT_B break; } } } }
Para aquellos que quieren esquivar Fragmentos, puede retener la AsyncTask ejecutándose en los cambios de orientación usando onRetainCustomNonConfigurationInstance () y algo de cableado.
(Tenga en cuenta que este método es la alternativa a la obsoleta onRetainNonConfigurationInstance () ).
Parece que esta solución no se menciona con frecuencia. Escribí un ejemplo de ejecución simple para ilustrar.
¡Aclamaciones!
public class MainActivity extends AppCompatActivity { private TextView result; private Button run; private AsyncTaskHolder asyncTaskHolder; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); result = (TextView) findViewById(R.id.textView_result); run = (Button) findViewById(R.id.button_run); asyncTaskHolder = getAsyncTaskHolder(); run.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { asyncTaskHolder.execute(); } }); } private AsyncTaskHolder getAsyncTaskHolder() { if (this.asyncTaskHolder != null) { return asyncTaskHolder; } //Not deprecated. Get the same instance back. Object instance = getLastCustomNonConfigurationInstance(); if (instance == null) { instance = new AsyncTaskHolder(); } if (!(instance instanceof ActivityDependant)) { Log.e("", instance.getClass().getName() + " must implement ActivityDependant"); } return (AsyncTaskHolder) instance; } @Override //Not deprecated. Save the object containing the running task. public Object onRetainCustomNonConfigurationInstance() { return asyncTaskHolder; } @Override protected void onStart() { super.onStart(); if (asyncTaskHolder != null) { asyncTaskHolder.attach(this); } } @Override protected void onStop() { super.onStop(); if (asyncTaskHolder != null) { asyncTaskHolder.detach(); } } void updateUI(String value) { this.result.setText(value); } interface ActivityDependant { void attach(Activity activity); void detach(); } class AsyncTaskHolder implements ActivityDependant { private Activity parentActivity; private boolean isRunning; private boolean isUpdateOnAttach; @Override public synchronized void attach(Activity activity) { this.parentActivity = activity; if (isUpdateOnAttach) { ((MainActivity) parentActivity).updateUI("done"); isUpdateOnAttach = false; } } @Override public synchronized void detach() { this.parentActivity = null; } public synchronized void execute() { if (isRunning) { Toast.makeText(parentActivity, "Already running", Toast.LENGTH_SHORT).show(); return; } isRunning = true; new AsyncTask() { @Override protected Void doInBackground(Void... params) { for (int i = 0; i < 100; i += 10) { try { Thread.sleep(500); publishProgress(i); } catch (InterruptedException e) { e.printStackTrace(); } } return null; } @Override protected void onProgressUpdate(Integer... values) { if (parentActivity != null) { ((MainActivity) parentActivity).updateUI(String.valueOf(values[0])); } } @Override protected synchronized void onPostExecute(Void aVoid) { if (parentActivity != null) { ((MainActivity) parentActivity).updateUI("done"); } else { isUpdateOnAttach = true; } isRunning = false; } }.execute(); } }
Puede usar los cargadores para esto. Marque Doc aquí
He implementado una biblioteca que puede resolver problemas con pausa de actividad y recreación mientras se ejecuta su tarea.
Debe implementar AsmykPleaseWaitTask
y AsmykBasicPleaseWaitActivity
. Su actividad y su tarea en segundo plano funcionarán bien incluso si gira la pantalla y cambia entre las aplicaciones
Evitar que una actividad se destruya y se cree a sí misma es declarar su actividad en el archivo de manifiesto: android: configChanges = “orientation | keyboardHidden | screenSize
Como se menciona en los documentos
La orientación de la pantalla ha cambiado: el usuario ha girado el dispositivo.
Nota: Si su aplicación se dirige al nivel API 13 o superior (según lo declarado por los atributos minSdkVersion y targetSdkVersion), también debe declarar la configuración de “tamaño de pantalla”, porque también cambia cuando un dispositivo cambia entre orientación vertical y paisaje.