Fragmentos

Desde la salida de Honeycomb los Fragmentos son una parte importante del framework para reutilizar porciones de interfaz segun el tamaño de la pantalla. Esto me recuerda muchísimo al concepto de “portlet” de los famosos Portales web.

 

Manejando cambio de orientación con un Progress Dialog

Puede que tengamos una pantalla, que en algún momento ejecute un cambio o solicitud de manera asíncrona. Para esto debemos mostrarle al usuario algo mientras, ya sea de manera determinada (progreso del trabajo o tarea) o indeterminada (el famoso spinner).

Cuando hay un cambio de orientación, si la actividad no maneja el cambio de orientación, probablemente sea destruida y recreada. Entonces ¿Qué pasa con el ProgressDialog?.

Iniciemos con dos reglas que debemos seguir:

  1. Si va a crear el AsyncTask dentro de la actividad como un inner class, asegúrese de que sea estática. Las inner class no estáticas, guardan una referencia de la outer class, en nuestro caso, la Actividad. Esto quiere decir que estamos “filtrando” (leaking) memoria, pues esa referencia de la actividad queda ahi mientras siga vivo el AsyncTask, y esto hay que evitarlo.
  2. Para crear el ProgressDialog, utilice los métodos que tiene disponible la actividad. Me refiero a onCreateDialog, showDialog y dismissDialog. Esto por que a la hora de cambiar la orientación de la pantalla y al destruir y recrear la actividad, Android va a mantener el estado de los dialogs que estaban presentes y se encargará de mostrarlos nuevamente cuando la actividad es recreada.
Ahora bien, aún siguiendo estas reglas, el principal problema es que la actividad que creó el AsyncTask puede que se destruya por lo que el AsyncTask pierda la referencia (y así debe ser). La solución es simplemente desacoplar la actividad del AsyncTask. Empleando los métodos del ciclo de vida de la activad, es posible “notificar” o “actualizar” el AsyncTask, para que se percate del cambio de actividad y pueda mostrar o ocultar el ProgressDialog.

El consenso es utilizar el método onRetainNonConfigurationInstance, para “guardar” la referencia del AsynctTask, así, cuando la nueva actividad es creada, ella se dará cuenta si la tarea todavia está en progreso, para poder “acoplarse” a ella.

public class EjemploProgressDialogActivity extends Activity {
    public static final String TAG = "EXAMPLE_DIALOG";

    public static final int PROGRESS_DIALOG = 1;

    MyTask task;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Log.d(TAG, "onCreate");
        Object obj = getLastNonConfigurationInstance();
        if (obj != null && obj instanceof MyTask) {
            Log.d(TAG, "Tarea previa ejecutandose");
            task = (MyTask) obj;
            task.attach(this);
        } else {
            task = new MyTask(this);
            task.execute(10);
            Log.d(TAG, "Nueva tarea creada y ejecutada");
        }
    }

    @Override
    protected Dialog onCreateDialog(int id) {

        switch (id) {
        case PROGRESS_DIALOG:
            ProgressDialog pd = new ProgressDialog(this);
            pd.setTitle("Trabajando");
            pd.setMessage("Por favor espere...");
            return pd;

        default:
            break;
        }
        return super.onCreateDialog(id);
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.d(TAG, "onPause");
    }

    @Override
    protected void onRestart() {
        super.onRestart();
        Log.d(TAG, "onRestart");

    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d(TAG, "onResume");
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.d(TAG, "onStart");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        // Aqui es donde se hace la magia
        if (task != null) {
            task.deattach();
            return task;
        }
        return super.onRetainNonConfigurationInstance();
    }

    private static class MyTask extends AsyncTask {
        WeakReference ctx;

        public MyTask(Activity activity) {
            super();
            attach(activity);
        }

        @Override
        protected void onPreExecute() {
            Activity activity = ctx.get();
            if (activity != null && !activity.isFinishing()) {
                Log.d(TAG, "Mostrando Progress Dialog");
                activity.showDialog(PROGRESS_DIALOG);
            }
        }

        @Override
        protected Void doInBackground(Integer... params) {
            int seconds = params[0];
            try {
                Log.d(TAG, "Tarea va a durar " + seconds + " segundos");
                Thread.sleep(seconds * 1000);
                Log.d(TAG, "Tarea Lista");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            if (ctx != null && ctx.get() != null) {
                Activity activity = ctx.get();
                if (!activity.isFinishing()) {
                    Log.d(TAG, "Ocultando Progress Dialog");
                    activity.dismissDialog(PROGRESS_DIALOG);
                }
            }
        }

        public void attach(Activity activity) {
            this.ctx = new WeakReference(activity);
        }

        public void deattach() {
            ctx = null;
        }

    }
}

En la línea 14, pueden ver que en el método onCreate se pregunta si se salvó algún objeto previamente y luego se hacen los chequeos respectivos para asegurarse que el objeto sea el AsyncTask. En este ejemplo en particular, se crea el AsyncTask en el método onCreate, por lo que en el caso de que no exista, se crea.

En la línea 18 se ejecuta el método attach,para que el AsyncTask tenga la nueva referencia de la Actividad.

En la línea 77 que es el momento en que la actividad da su último suspiro, el AsyncTask se desacopla y se “salva” la referencia al AsyncTask para que la siguiente actividad (si es el caso) la retome, como vimos en el método onCreate.

Si ven la implementación del AsyncTask, primero que todo, se guarda la referencia a la actividad dentro de un WeakReference, para así evitar “filtrar” memoria.

En los métodos onPreExecute y onPostExecute se hacen validaciones para asegurarse de que la referencia de la actividad sea válidad (que exista la referencia y que la actividad no esté en proceso de morirse).

Si ejecutáramos éste código,  sin mover el dispositivo, esta sería la salida en la bitácora:

10-04 21:54:46.254: DEBUG/EXAMPLE_DIALOG(6155): onCreate
10-04 21:54:46.294: DEBUG/EXAMPLE_DIALOG(6155): Mostrando Progress Dialog
10-04 21:54:46.514: DEBUG/EXAMPLE_DIALOG(6155): Tarea va a durar 10 segundos
10-04 21:54:46.514: DEBUG/EXAMPLE_DIALOG(6155): Nueva tarea creada y ejecutada
10-04 21:54:46.514: DEBUG/EXAMPLE_DIALOG(6155): onStart
10-04 21:54:46.524: DEBUG/EXAMPLE_DIALOG(6155): onResume
10-04 21:54:56.518: DEBUG/EXAMPLE_DIALOG(6155): Tarea Lista
10-04 21:54:56.524: DEBUG/EXAMPLE_DIALOG(6155): Ocultando Progress Dialog
10-04 21:55:12.395: DEBUG/EXAMPLE_DIALOG(6155): onPause
10-04 21:55:12.554: DEBUG/EXAMPLE_DIALOG(6155): onStop

Pero si cambiamos la orientación del dispositivo, esto sería el resultado:

te
10-04 21:56:08.584: DEBUG/EXAMPLE_DIALOG(6155): Mostrando Progress Dialog
10-04 21:56:08.754: DEBUG/EXAMPLE_DIALOG(6155): Nueva tarea creada y ejecutada
10-04 21:56:08.754: DEBUG/EXAMPLE_DIALOG(6155): onStart
10-04 21:56:08.754: DEBUG/EXAMPLE_DIALOG(6155): Tarea va a durar 10 segundos
10-04 21:56:08.764: DEBUG/EXAMPLE_DIALOG(6155): onResume
10-04 21:56:10.424: DEBUG/EXAMPLE_DIALOG(6155): onPause
10-04 21:56:10.424: DEBUG/EXAMPLE_DIALOG(6155): onStop
10-04 21:56:10.544: DEBUG/EXAMPLE_DIALOG(6155): onCreate
10-04 21:56:10.544: DEBUG/EXAMPLE_DIALOG(6155): Tarea previa ejecutandose
10-04 21:56:10.544: DEBUG/EXAMPLE_DIALOG(6155): onStart
10-04 21:56:10.704: DEBUG/EXAMPLE_DIALOG(6155): onResume
10-04 21:56:18.755: DEBUG/EXAMPLE_DIALOG(6155): Tarea Lista
10-04 21:56:18.755: DEBUG/EXAMPLE_DIALOG(6155): Ocultando Progress Dialog
10-04 21:56:21.626: DEBUG/EXAMPLE_DIALOG(6155): onPause
10-04 21:56:21.784: DEBUG/EXAMPLE_DIALOG(6155): onStop

Imprimí además cuando se ejecutan otros métodos del ciclo de vida, para tener una referencia de cuando ocurre qué.

Softkeyboard aparece al entrar en una ventana

Me encontré un “error” o “bug” muy particular en Android. Lo pongo entre comillas porque aún no tengo confirmación de que así lo sea.

El “bug” es que ciertos layouts, pueden provocar que el teclado virtual (o softkeyboard) se active tan pronto como la actividad (o ventana) toma foco (se coloca en la pila de ventananas). Según mis pruebas, (que pueden ver en el post) todo pasa cuando el layout tiene un widget contenedor, llámese un ListView, GridView, Gallery, etc. y debajo de él un campo de texto. Esta combinación, parece provocar, en ciertos devices como el HTC Desire  y el LG GX2, ambos con Android 2.2, la activación del softkeyboad.

El mes pasado puse una pregunta en el grupo de google para Desarrolladores de Android (ver pregunta aquí), pero lo único que me contestaron fue como “mitigarlo”.

Dentro de las recomendaciones que me hicieron, fue que creara un repositorio con el código, para que otras personas pudieran probarlo. Pueden bajar el código en Github:

https://github.com/fr4gus/AndroidSoftKeyboardBug

Me gustaría saber si alguien más lo puede replicar, con cual o cuales devices y si tiene alguna idea de si es un bug o no.

-f4

 

Usando efectívamente el AsyncTask Parte 2

Ver parte 1 aqui.

El otro tip del que les quería hablar con los AsyncTask es más que todo recalcar la importancia de implementar expícitamente la lógica dentro del método doInBackground que permita cancelar el AsyncTask.

Si recuerdan de la parte uno, el AsyncTask guarda una referencia “debil” de la actividad que lo ejecuta. Además, verifica que la Actividad este siendo ejecutada con normalidad, es decir, que la actividad no esté siendo terminada.

Con el código de la parte uno, si el usuario se sale de dicha actividad, vamos a tener el hilo de la actividad corriendo hasta que termine con lo que estaba haciendo. Esto puede  representar un problema ya que tenemos un AsyncTask ejecutandose sin necesidad. Esto es un gasto tanto en procesamiento, memoria, como en la posibilidad de ejecutar otras AsyncTask (recordemos que existe un límite).

La solución a esto es, si la actividad no ha terminado y ya no la ocupamos más, podamos llamar el método calcel(boolean). Esto provocará que el método isCancelled regrese “true”, además, para asegurar que onCancelled() sea llamado en vez de onPostExecute(Object), es importante checkear isCancelled dentro de doInBackground, asegurando que la tarea termine lo más pronto posible.

Por ejemplo:

public class MyActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Inicialización de la actividad, layout, etc
        MyLongTask task = new MyLongTask(this);
        task.execute("http://blog.fr4gus.com/api/test.json");
    }

    @Override
    protected void onPause() {
        // Persistamos cualquier cosa que ocupemos
    }
    
    static class MyLongTask extends AsyncTask<String, Void, Void> {
        WeakReference<MyActivity> context;

        public MyLongTask(MyActivity activity) {
            context = new WeakReference<MyActivity>(activity);
        }

        @Override
        protected void onPreExecute() {
            // Avísele al usuario que estamos trabajando
        }

        @Override
        protected Void doInBackground(String... params) {
            while(! isCancelled() ){
            // Aquí hacemos una tarea laaarga
            // Y ademas chequeamos que la tarea no ha sido cancelada

            }
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            MyActivity activity = context.get();
            if (activity != null && !activity.isFinishing()) {
                // Aquí actualizamos la UI con el resultado
            }
        }
    }
    
}

Los AsyncTask son una herramienta muy util, pero también nos pueden perjudicar si no sabemos utilizarlas correctamente. Pueden provocar leaks de memoria, o uso de recursos innecesariamente. El hecho de que para Android Honeycomb hayan regresado al esquema original con un solo hilo, indica que la idea es que hayan pocos hilos actualizando la interface. De todas maneras hay que aprovechar otras herramientas similares como el método runOnUiThread o los Handlers. La idea es minimizar los riesgos y aprovechar los recursos al máximo.

Usando efectívamente el AsyncTask Parte 1

Imaginemos, por ejemplo, la aplicación de Twitter, que al iniciar su ejecución hace un llamado a un método del API de Twitter. Éste llamado tomará su tiempo dependiendo del tipo de conexión del usuario. Mientras se obtienen los tweets más recientes, no queremos que la aplicación se congele provocando un ANR (Application Not Responding) y un eventual Force Close de la aplicación. Por ello, Android provee al desarrollador de diferentes herramientas para ejecutar tareas en hilos a parte del hilo principal, entre ellas, las clases Handler(desde API Level 1) y AsyncTask (desde API Level 3).

El AsyncTask es ampliamente utilizado para ejecutar tareas “simples” o “atómicas” en un hilo aparte. Su ciclo de ejecución es bastante intuitivo y práctico, comparado con el uso de la clase Handler. En Android 1.5, los AsyncTask eran encolados y un único hilo se encargaba de ejecutarlos uno por uno. A partir de 1.6 hasta Android 2.3.4 (inclusive), los AsyncTask son ejecutados cada uno por un hilo aparte (sin asegurar su orden). Aqui la clase AsyncTask maneja una cola de ejecución de 10 hilos(dato no confirmado) y una cola de 10 hilos en espera (aunque según Roman Guy aquí, ese límite ya no existe, aunque no indica en que versión :-/), dándonos aproximádamente 20 AsyncTask “simultáneos”. Cuando este límite se excede, se dispara la excepción RejectedExecutionException. Está planeado despues de Honeycomb (Android 3.0) volver a la ejecución sencilla, para evitar los errores de ejecución paralela.

Esta clase es muy útil pero a la vez se convierte, probablemente, en una de las mayores fuentes de memory leaks y application crashes (Force Close) en Android. En este primer artículo hablaremos primero sobre los Inner clases y cómo definir una AsycTask adecuadamente.

1. Inner clases

Tal vez la razón número uno de leaks en una aplicación Android son las Inner classes dentro de una clase Activity. Las Inner class, tienen una referencia implícita hacia la Outer class. Cuando se crea un AsyncTask, la tendencia es hacerlas dentro de la Actividad donde la ocupamos. Veamos un ejemplo

public class MyActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Inicialización de la actividad, layout, etc
        MyLongTask task = new MyLongTask();
        task.execute("http://blog.fr4gus.com/api/test.json");
    }

    @Override
    protected void onPause() {
        // Persistamos cualquier cosa que ocupemos
    }

    class MyLongTask extends AsyncTask<String, Void, Void>{

        @Override
        protected void onPreExecute() {
            // Avísele al usuario que estamos trabajando
        }

        @Override
        protected Void doInBackground(String... params) {
            // Aquí hacemos una tarea laaarga
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            // Aquí actualizamos la UI con el resultado
        }
    }

}

El problema es que éstas inner classes no-estáticas no tienen un ciclo de vida controlado por el desarrollador. La solución está en hacer las classes Inner, estáticas y pasarle la referencia de la actividad o contexto, pero envolviéndola dentro de un WeakReference. Esto permitirá al Garbage Collector, liberar la memoria del Activity aunque el AsyncTask siga ejecutándose. Es importante que a la hora de usar el WeakReference, se aseguren que la referencia al Activity sea aun válida, verificando que no sea nula y que la activdad no esta terminando.

public class MyActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Inicialización de la actividad, layout, etc
        MyLongTask task = new MyLongTask(this);
        task.execute("http://blog.fr4gus.com/api/test.json");
    }

    @Override
    protected void onPause() {
        // Persistamos cualquier cosa que ocupemos
    }

    static class MyLongTask extends AsyncTask<String, Void, Void> {
        WeakReference<MyActivity> context;

        public MyLongTask(MyActivity activity) {
            context = new WeakReference<MyActivity>(activity);
        }

        @Override
        protected void onPreExecute() {
            // Avísele al usuario que estamos trabajando
        }

        @Override
        protected Void doInBackground(String... params) {
            // Aquí hacemos una tarea laaarga
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
            MyActivity activity = context.get();
            if (activity != null &#038;&#038; !activity.isFinishing()) {
                // Aquí actualizamos la UI con el resultado
            }
        }
    }

}

Parte 2

Fuentes

  1. Documentación de Android sobre memory leaks http://developer.android.com/resources/articles/avoiding-memory-leaks.html
  2. Límite de ejecución de los AyncTask: http://stackoverflow.com/questions/2492909/asynctask-rejectedexecutionexception-and-task-limit
  3. Implementación de AyncTaskEx de CommonsGuy: https://github.com/commonsguy/cwac-task