domingo, 17 de junio de 2012

Programación Android: Como cargar más datos en un ListView al llegar al final

Hola de nuevo. Ahora quiero hablaros de un pequeño truco que he descubierto. La aplicación Android de MasQueCursos.org utiliza varios web services para leer los datos de Internet: lista de cursos, datos de un curso, lista de centros, datos de un centro, lista de categorías, etc. Para no sobrecargar las comunicaciones, en lugar de cargar todos los resultados a mostrar de golpe (por ejemplo cursos), lo hago en bloques de 25. Al principio puse botones para paginar igual que hay en la web: siguiente página, anterior página, primera página y última página, pero eso no quedaba demasiado bien en el móvil: los botones comen espacio para los resultados y la pantalla se veía demasiado llena. Así que decidí que lo ideal era cargar los 25 primeros resultados y cuando el usuario moviera la lista para ver el último, se cargaran automáticamente los siguientes 25 resultados.

Estuve investigando diferentes soluciones en Internet, pero ninguna me acababa de gustar. La mayoría usaban un evento mediante la función setOnScrollListener(), pero me dio algunos problemas y busqué otras soluciones. Al final resulta que ha sido relativamente fácil.

La solución consiste en añadir un elemento vacío al final de la lista de datos de forma que cuando Android llama a la función getView() del adaptador del ListView, si la llamada se hace para la última posición, cargamos los nuevos resultados.

A continuación pongo el código de la clase ListViewActivity, que es la que se encarga de gestionar la lista de resultados. Atención seguir este código requiere un mínimo de conocimientos de programación Android.

public class ListViewActivity
    extends Activity
    implements OnItemClickListener {

    // Miembros de la clase
    private ListViewData[] mListViewData;

    // Controles del layout
    private ListView mListView;

    // Parámetros de llamada
    private int mStart;
    private int mLimit;

    // Tamaño de la vista
    private int mCount;
    private int mActual;

    @Override
    public void onCreate(Bundle savedInstanceState) {

        // Llamada a la clase padre
        super.onCreate(savedInstanceState);

        // Inicialización de la vista
        setContentView(R.layout.listviews);

        // Obtención de referencias permanentes a los controles
        mListView = (ListView) findViewById(R.id.listview);

        // Tratamiento de eventos del ListView
        mListView.setOnItemClickListener(this);

        // Inicializaciones
        mListViewData = null;
        mActual = 0;
        mCount = 0;

        // Se Obtención y carga de datos en el ListView
        mStart = 0;
        mLimit = 25;
        _loadDataCount();
        _loadDataList();

    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

        // Validaciones iniciales (necesario al haber un item de más)
        if(mListViewData == null || mListViewData[position] == null)
            return;

        // Tratamiento de la pulsación sobre el item mListViewData[position]
        // ...
    }

    private void _loadDataCount() {

        // Aquí cargamos en mCount el tamaño total de los datos, ya sea mediante
        // una query a una base de datos interna o extarna o mediante una llamada
        // REST o lo que sea.
        // ...
    }

    private void _loadDataList() {

        // Aquí accedermos a la fuente de datos (base de datos, web service u
        // otro) para obtener los datos entre mStart y mLimit y los cargamos
        // por ejemplo en un array llamado data.
        // ...

        // Se cargan los datos en el ListView
        _fillListView(data);
    }

    private void _fillListView(ListViewData[] data) {

        // Se cuenta el número de items en el array de datos
        int dataCount = data.length();

        // Establecimiento de los textos de la cabecera
        mActual += dataCount;

        // Si no hay datos es un error error
        if(dataCount < 1) {

            // Hacer el tratamiento del error
            // ...

            // Fin por error
            return;
        }

        // Nos guardamos el array de datos anterior
        ListViewData[] oldData = mListViewData;

        // Se crea un array de n + 1 items para el ListView excepto si ya se han
        // cargado todos los datos. Si hay un item de más, éste quedará vacío
        int count = mActual;
        if(mActual < mCount)
            ++count;

        mListViewData = new ListViewData[count];

        // Si había un array anterior, se traspasa la información al nuevo
        int position = 0;
        if(oldData != null) {
            for(int i = 0; i < oldData.length; ++i) {
                if(oldData[i] != null) {
                    mListViewData[position++] = oldData[i];
                }
            }
        }

        // Traspaso de la información leída de la fuente de datos
        for (int i = 0; i < dataCount; i++) {
            mListViewData[position++] = new ListViewData(data[i]);
        }

        // Si hay más resultados, el último item irá vacío
        if(mActual < mCount)
            mListViewData[mActual] = null;

        // Se asigna el nuevo adaptador al ListView
        DataAdapter adapter = new DataAdapter();
        mListView.setAdapter(adapter);

        // Scroll del ListView al último item leído
        mListView.setSelection((mStart > 0) ? mStart - 1 : 0);
    }

    public class ListViewData {

        // En esta clase se definen los datos y métodos para acceder a ellos.
        // ...
    }

    public class DataAdapter extends ArrayAdapter<ListViewData> {

        public DataAdapter() {

            // Llamada a la clase padre
            super(ListViewActivity.this, R.layout.listitem, mListViewData);
        }

        public View getView(int position, View convertView, ViewGroup parent) {

            // Obtención del objeto que se usa para cargar los items
            LayoutInflater inflater = ListViewActivity.this.getLayoutInflater();

            View item;
            if(position == mActual) {

                // Inicializaciones
                item = inflater.inflate(R.layout.listitem, null);

                // Traspaso de información al item recién creado
                // ...

                // Se carga una nueva lista de cursos
                mStart = mActual;
                mLimit = 25;
                _loadDataList();
            }
            else {

                // Inicializaciones
                item = inflater.inflate(R.layout.listitem, null);


                // Traspaso de información al item recién creado
                // ...
            }

            // Finalización
            return(item);
        }
    }
}

PD: ¿Alguien sabe como hacer que el código fuente salga coloreado en el blog? Con las palabras clave resaltadas y eso.

Curso de programación Android

Hola a todos. Llevo algunos meses haciendo un curso de programación Android, por aquello de ampliar horizontes y estar al día de la tecnología más actual. Se trata de un curso a distancia, asistido por un profesor que resuelve tus dudas.

El curso regular lo he acabado y ahora tengo que hacer un proyecto, pero en lugar de hacer algo sólo para demostrar que ya se programar Android, he decidido hacer algo funcional: La aplicación para móviles de una de nuestras webs: MasQueCursos.org. La intención, aparte de acabar el curso, es aprender haciendo una aplicación de verdad para en el futuro poder realizar aplicaciones para terceros. Además cuando esté acabada subiré la aplicación a Google Play, poniéndola gratuita.

Iré informando aquí de los progresos. Además como me estoy encontrando problemas reales, publicaré algunos trucos de programación Android que voy encontrando.