Curso avanzado de Android

C

Buenas!

Llevo tiempo por mi cuenta avanzando con Android. Pero he llegado a un punto de esos en los que empiezan a aparecer conceptos más complejos que los foros no resuelven.
El caso es que la empresa me paga el curso que elija. He mirado por internet y si algo me fastidia es que está lleno de cursos presenciales (sería en Madrid) donde el capítulo 1 es: "Instalación del SDK" ...
Y en el fondo sé que voy a ver lo que yo por mi cuenta ya he visto. Pero sospecho que no me van a enseñar las tripas de cuestiones más enrevesadas como un problema con el que me estoy pegando horas y horas y no consigo resolver (ListView dentro de un ViewPager ... ).

¿Alguien conoce alguna academía donde impartan un curso decente que merezca la pena?

Es que ya le pasó a un compañero que hizo uno para programar en iPhone y el profesor era un n00b que seguía un manual y no tenía ni idea de nada. Y tampoco es cuestión de tirar tiempo y dinero.

Gracias de antebrazo!

P.D.: Por si alguien que pilota me puede ayudar:
Tengo una Activity con un viewpager. El adaptador del viewpager instancia internamente un adaptador para llenar un listview que está en el layout que carga el viewpager. Todas las páginas tienen un listview que se carga.
El caso es que he conseguido que funcione bien la carga de cada layout. Pero cuando cambio de página y pulso en una fila o bien hago scrolling, me pega un casque mortal. He mirado por stackoverflow y hay temas que mencionan cosas por el estilo. Pero no me termino de aclarar cómo puedo solucionarlo. Si alguien pilota y tiene tiempo y ganas, le puedo dar más detalles. Thx ;)

MTX_Anubis

Sin ver código ni nada. ¿No estarás reutilizando el adaptador en diferentes listviews no? Si puedes poner al menos la excepción que salta estaría bien xD.

Yo he hecho eso que quieres hacer tú y no tuve problemas. Lo que hice fue hacer un fragment para la página y ahí llevaba toda la lógica

C

Por lo que he leído en StackOverflow, debe ser eso que mencionas. Por ahí deben de ir los tiros.
Creo que tiene que ver con que en el instantiateItem me debe estar sobreescribiendo la View por otra distinta. La prueba es que meto un punto de interrupción en el getCount del adaptador del listview y está devolviendo el número de filas que tiene otro listview de otra vista y no la que está en pantalla.
Y claro, el casque pasa justamente cuando pincho en una fila del listview segundo que carga.
Se ve que las vistas y sus adaptadores no están relacionadas por algo que estoy haciendo mal.

Para ir más al grano, el chocho debe de estar en la función UpdateView. Esa función la llamo tanto en el instantiateItem como en el onPostExecute del AsyncTask. La diferencia está en comprobar si el arraylist es nulo (entonces ejecuto el AsyncTask para cargar el arraylist con los datos) o tiene algo (entonces instancio el adaptador del listview). Es en el punto en el que instancio el adaptador del listview donde algo estoy haciendo mal. Quizás no sea ahí donde tenga que cargar el listview, no sé...

Este el OnCreate de la Activity

public void onCreate(Bundle savedInstanceState) {
    	super.onCreate(savedInstanceState);
        setContentView(R.layout.lay_act_executions);

    Bundle bundle = getIntent().getExtras();
    piIdProcess = bundle.getString("p_id");
    setTitle("Ejecuciones");        

    miAdapter = new ColumnasAdapter(this,piIdProcess);
    columnas = (ViewPager) findViewById(R.id.columnas);
    columnas.setAdapter(miAdapter);
    
    TitlePageIndicator titleIndicator = (TitlePageIndicator)findViewById(R.id.titulos);
    titleIndicator.setViewPager(columnas);
    titleIndicator.setFooterIndicatorStyle(IndicatorStyle.Triangle);
    titleIndicator.setCurrentItem(miAdapter.getCount()-1);
    titleIndicator.setOnPageChangeListener(this);
}

Aunque no es relevante, también adjunto la clase donde cargo la estructura de datos que se pintan en cada fila:

public class cExecution implements Parcelable {

private String mStrSerie = "";
private String mStrOut = "";
private String mStrHora = "";
private String mStrTiempo = "";
private String mStrLevel = "";

public void setSerie(String pStrCode) {
	this.mStrSerie = pStrCode;
}

public void setOut(String pStrOut) {
	this.mStrOut = pStrOut;
}

public void setHora(String pStrHora) {
	this.mStrHora = pStrHora;
}

public void setTiempo(String pStrTiempo) {
	this.mStrTiempo = pStrTiempo;
}

public void setLevel(String pStrLevel) {
	this.mStrLevel = pStrLevel;
}

public String getSerie() {
	return this.mStrSerie;
}

public String getOut() {
	return this.mStrOut;
}

public String getHora() {
	return this.mStrHora;
}

public String getTiempo() {
	return this.mStrTiempo;
}

public String getLevel() {
	return this.mStrLevel;
}

@Override
public int describeContents() {
	// TODO Auto-generated method stub
	return 0;
}

@Override
public void writeToParcel(Parcel dest, int flags) {
	// TODO Auto-generated method stub
	
}

}

Este es el adaptador del listview:

public class cAdaptadorExecutions extends BaseAdapter {

private static ArrayList<cExecution> alExecutions;

private LayoutInflater mInflater;

public cAdaptadorExecutions(Context context,ArrayList<cExecution> palExecutions) {
	
	alExecutions = palExecutions;
	mInflater = LayoutInflater.from(context);
}

public int getCount() {
	return alExecutions.size();
}

public Object getItem(int position) {
	return alExecutions.get(position);
}

public long getItemId(int position) {
	return position;
}


public View getView(int position, View convertView, ViewGroup parent) {
	
	ViewHolder holder = null;
	
	if (convertView == null) {
		convertView = mInflater.inflate(R.layout.layout_fila_exec, null);
		holder = new ViewHolder(convertView, alExecutions.get(position).getLevel());
		convertView.setTag(holder);
	} else {
		holder = (ViewHolder) convertView.getTag();
	}
	
	//holder.tvSerie.setText(alExecutions.get(position).getSerie());
	holder.tvOut.setText(alExecutions.get(position).getOut());
	holder.tvHora.setText(alExecutions.get(position).getHora());
	holder.tvTiempo.setText("Segundos: "+alExecutions.get(position).getTiempo());	
	//holder.tvLevel.setText(alExecutions.get(position).getLevel());
	
	return convertView;
}

private class ViewHolder {
	TextView tvLevel;
    TextView tvSerie;
    TextView tvOut;
    TextView tvHora;
    TextView tvTiempo;
    ImageView imvIcono;
    
    public ViewHolder(View fila, String level) {
    	//tvCode = (TextView) fila.findViewById(R.id.txtCode);
    	tvOut = (TextView) fila.findViewById(R.id.txtOut);
    	tvHora = (TextView) fila.findViewById(R.id.txtHora);
    	tvTiempo = (TextView) fila.findViewById(R.id.txtTiempo);
    	imvIcono = (ImageView) fila.findViewById(R.id.imvIcono);
    	
    	if (level.equals("0")) {
    		imvIcono.setImageResource(R.drawable.unknown);
    	} else if (level.equals("1")) {
    		imvIcono.setImageResource(R.drawable.ok);
    	} else if (level.equals("2")) {
    		imvIcono.setImageResource(R.drawable.exclamation);
    	} else if (level.equals("3")) {
    		imvIcono.setImageResource(R.drawable.cancel);
    	} else {
    		imvIcono.setImageResource(R.drawable.unknown);
    	}

    	
    	
  
    }
}	

}

Y ahora viene la madre del cordero donde debe de estar cagándola. El adaptador del ViewPager. Es algo complejo porque al tener que consultar una base de datos, uso un AsyncTask. Sí... sé que lo suyo es leer el xml que me devuelva un script en .php, por ejemplo. Pero como es un pequeño proyecto interno estoy tirando directamente la query, no es echéis encima mío xDDD. Y no hace falta empollarse la creación de las páginas. Pillé el ejemplo de aquí:

https://bitbucket.org/ayastrebov/android-infinite-viewpager/src/b69ee64cfbe4/ViewPager-Sample/src/com/stonerhawk/viewpager/MyPagerAdapter.java

public class ColumnasAdapter extends PagerAdapter implements TitleProvider {

private static final DateFormat dfTitle = new SimpleDateFormat("E, dd MMM");
private static final int daysSize = 7;
private static Date[] dates = new Date[ daysSize ];

private LayoutInflater mInflater = null;
private Context ctx;

ProgressDialog dialog;

public String mIdProcess; 
ListView lvExecutions;
private cAdaptadorExecutions ExecutionAdapter;

public ColumnasAdapter(Context context, String piIdProcess) 
{
		ctx = context;
		mIdProcess = piIdProcess;
		dialog = null;
		dialog = new ProgressDialog(ctx);
        prepareDates();
		this.mInflater = LayoutInflater.from(ctx);
}

private void prepareDates() 
{
        Date today = new Date();

        Calendar calFecha = Calendar.getInstance();

        // Situamos la variable fecha en el día de hoy
        calFecha.setTime(today);
        // Le restamos los días del rango
        calFecha.add( Calendar.DATE, daysSize*(-1));

        // Llenamos el array con los días siguientes al inicio del rango hasta hoy
        for (int i = 0; i < daysSize; i++) 
        {
        		calFecha.add( Calendar.DATE, 1 );
                dates[ i ] = calFecha.getTime();
        }
}

@Override
public int getCount() {
    return dates.length;
}

@Override
public Object instantiateItem(View collection, int position) {

	if (this.dialog == null ) {
		this.dialog = new ProgressDialog(this.ctx);
	}
	
	View v = drawView(position, ( (ViewPager) collection).getChildAt(position));
            
    ((ViewPager) collection).addView(v);

    return v;
}

private View drawView(int position, View view) 
{
        view = mInflater.inflate(R.layout.lay_act_vp_executions, null);
        updateView(position, view, null);

        return view;
}

private void updateView(int position, View view, ArrayList<cExecution> palExecutions)
{
	lvExecutions = (ListView) view.findViewById(R.id.lsvExecutions);

	if (palExecutions != null) {
        ExecutionAdapter = new cAdaptadorExecutions(this.ctx,palExecutions);
        lvExecutions.setAdapter(ExecutionAdapter);

        if (this.dialog.isShowing()) this.dialog.dismiss();
	} else {
		DateFormat dfQuery = new SimpleDateFormat("yyyy-MM-dd");
		new LoadContentTask().execute(position, view, dfQuery.format( dates[position] ), this.mIdProcess);

		if (! this.dialog.isShowing()) {
        	this.dialog.setMessage("Cargando...");
        	this.dialog.setIndeterminate(false);
        	this.dialog.show();
          }

	}
}

private class LoadContentTask extends AsyncTask<Object, Object, Object> 
{
        
        private Integer position;
        private View view;
        
        @Override
        protected void onPreExecute() {

        }
        
        @Override
        protected Object doInBackground(Object... arg) 
        {
                position = (Integer) arg[0];
                view = (View) arg[1];
    			ArrayList<cExecution> alExecutions = new ArrayList<cExecution>();
                
    			try
    			{
                    java.sql.Connection conn;
                    String cadenaFecha = (String) arg[2];
    				alExecutions.clear();
    				
    				Class.forName("com.mysql.jdbc.Driver");
    				conn = DriverManager.getConnection("jdbc:mysql://xxx.yyy.z.pp:kkkk/xxxxx","xxxxx","xDDDDD");
    	    		Statement st = (Statement) conn.createStatement();
    	    		ResultSet rsExecutions = st.executeQuery("vease una select que está bien xD");
	    	    	cExecution Execution = new cExecution();
    	    		while (rsExecutions.next())
    	    		{
    	    			Execution = new cExecution();
    	    			Execution.setSerie(rsExecutions.getString(1));
    	    			Execution.setOut(rsExecutions.getString(2));
    	    			Execution.setHora(rsExecutions.getString(3));
    	    			Execution.setTiempo(rsExecutions.getString(4));
    	    			Execution.setLevel(rsExecutions.getString(5));
    	    			alExecutions.add(Execution);
    	    		}
    	    		st.close();

    			}
    			catch (ClassNotFoundException e)
    			{
    				e.printStackTrace();
    			}
    			catch (SQLException e)
    			{
    				e.printStackTrace();
    			}

    			return alExecutions;
        }

        protected void onPostExecute(Object result) 
        {
                updateView(position,view,(ArrayList<cExecution>) result);
                view.postInvalidate();
                //notifyDataSetChanged();
     }

}

@Override
public void destroyItem(View collection, int position, Object view) {
    ((ViewPager) collection).removeView((LinearLayout) view);
}

@Override
public boolean isViewFromObject(View view, Object object) {
    return view == ((LinearLayout) object);
}

@Override
public void finishUpdate(View arg0) {
}

@Override
public void restoreState(Parcelable arg0, ClassLoader arg1) {
}

@Override
public Parcelable saveState() {
    return null;
}

@Override
public void startUpdate(View arg0) {
}

@Override
public String getTitle(int position) {
    return dfTitle.format( dates[position] );        	
}

}

Por los foros la gente apela al notifySetDataChanged pero no ayuda en nada hacer eso. Los hilos de stackoverflow que más se acercan son estos:

http://stackoverflow.com/questions/3874506/android-listview-problem-when-refreshing

http://stackoverflow.com/questions/9714633/viewpager-listview-and-asynctask-first-page-blank-all-data-in-the-second-pag

Aquí hay un ejemplo de un pavo donde hace algo muy parecido a lo mío pero más sencillo al no tener que usar un hilo para cargar el listview:

http://blog.stylingandroid.com/archives/542

Cualquier ayuda sería de agradecer. Estoy desesperado y cuando me obsesiono, ni puedo dormir ni ná!

C

Pantallazo de la actividad por si alguien tiene curiosidad.

Es una aplicación interna para los del departamento de i+d que accede al sistema de log del bakoffice y ver si todo está ok. La vamos a usar sólo un colega y yo que tenemos Android. El caso es practicar con algo que de paso me sea útil xD.

Tig

No he tocado ViewPager, pero ¿por qué no usas un CursorAdapter si hay consulta a DB?

No he mirado el código en detalle como para comprender lo que quieres hacer, pero meter un AsyncTask dentro de un Adapter me parece una muy mala idea. Piensa que cada scroll vas a lanzar 5-6 hilos fácilmente, y ten en cuenta que muchos de estos hilos van a compartir la vista que luego van a querer modificar.

Además, AsyncTask está ahora mismo poco recomendado, merecería la pena que te miraras los Loaders y LoaderManager, que viene dentro del support package. Pero vaya, que yo descartaría el uso de hilos dentro de un Adapter.

Leyendo tu último post y lo que quieres conseguir, se me ocurren varias soluciones que no sé si has probado ya.

1) Un Service corriendo en background que chequee el estado de las tareas periódicamente y lance Intents cuando se actualice el estado. Con un BroadcastReceiver podrías recibir los cambios de estado y sería sencillo actualizar la vista.

2) Si pudierais implementar notificaciones push en el backoffice podrías notificar a la aplicación con los cambios. Esto es más complejo.

Hay más soluciones, estas 2 se me ocurren a bote pronto.

Sobre cursos avanzados de Android, te diría que StackOverflow es lo más avanzado que vas a encontrar...

1 respuesta
C

Gracias #5 por el interés.

Toda la razón en lo que comentas del AsyncTask. De hecho, en los primeros "pinitos" que hice con hilos, utilizo la forma más básica de envío de mensajes (además, me recuerda mucho al SendMessage de la API de Windows que me trae buenos recuerdos xD).

Miraré lo que comentas de LoaderManager y el BroadcastReceiver.

En principio no se ejecutan 'n' hilos por cada scroll. Me refería al evento instantiateItem del ViewPager que se ejecuta cuando cambias de página. Ese se ejecuta una vez al cambiar y, lo peor, dos veces al iniciar (una para la página cero y otra para la adyacente).

Desalentador lo que comentas sobre los cursos. Efectivamente he mirado por ahí y dan pena. Te enseñan lo que cualquiera por su cuenta echándole unas horas puede aprender por internet. Se ve que esto está muy verde. La prueba es que los componentes de terceros son todo opensource y de cuatro monos. La falta de cursos presenciales también es prueba.

Por si alguien quiere echarme una mano y ya de paso aprender xD, adjunto un ejemplo en el que se hace exactamente lo que hago yo pero sin introducir el tema de la AsyncTask:

http://blog.stylingandroid.com/archives/542

El ejemplo contiene unas páginas de viewpager y en cada una un listview. Pero claro, en el ejemplo el autor simplifica mucho las cosas cargando en el instantiateItem de forma muy sencilla una lista basada en un array:

@Override
public Object instantiateItem( View pager, int position )
{
    ListView v = new ListView( context );
    String[] from = new String[] { "str" };
    int[] to = new int[] { android.R.id.text1 };
    List<Map<String, String>> items =
        new ArrayList<Map<String, String>>();
    for ( int i = 0; i < 20; i++ )
    {
        Map<String, String> map =
            new HashMap<String, String>();
        map.put( "str", String.format( "Item %d", i + 1 ) );
        items.add( map );
    }
    SimpleAdapter adapter = new SimpleAdapter( context, items,
        android.R.layout.simple_list_item_1, from, to );
    v.setAdapter( adapter );
    ( (ViewPager) pager ).addView( v, 0 );
    return v;
}

La cuestión es que yo en esa función necesito lanzar la consulta, cargar un array de objetos y pasarlo al adapter del listview.

¿Dónde ejecutaríais ese código en este caso?

Si consigo hacerlo de una forma decente para que no pete lo postearé al más puro estilo stackoverflow. Y si alguno lo conseguís entonaré el famoso: it works! xD

1 respuesta
Tig

#6 si es seguro que InstantiateItem sólo se ejecuta una vez (tiene sentido por el nombre xD), es muy sencillo. Pongo pseudocódigo porque estoy en Windows y aquí no tengo nada

new Thread(

ArrayList lista = leerListaDeDB();

final Adapter adapter = new Adapter(context, blablabla, lista);

runOnUIThread(listView.setAdapter(adapter));

).start();

Esto debería bastar, pero ya te digo que para bases de datos es mejor usar un CursorAdapter, luego con un simple

adapter.requery(); adapter.notifyDatasetChanged();

Suerte!

C

Es para matarme... en el adaptador del listview había declarado el arraylist de objetos como static...

private static ArrayList<cExecution> alExecutions;

Sin retarded que soy. Ahora sí:

private ArrayList<cExecution> alExecutions;

La sensación es como cuando te dicen que te vas a morir de cáncer y después de estar unos días comiéndote el tarro te dicen que se habían equivocado de expediente. La sorpresa es grata y además te has puesto al día en temas de médicos, legales, etc.
Pues esto es lo mismo. He aprendido del ViewPager lo que no está escrito...

it works!

C

y vuelve la burra al trigo...

Después de corregir el error, los listview se cargan perfectamente. Pero tengo otro problema. Cuando cambias de página, el viewpager carga el listview de la página adyacente en la función instantiateItem. Y claro, cuando pulsas en un item del listview, al obtener sus datos, me está devolviendo el de la lista de la página adyacente y no el de la página que se visualiza. Es por el puto funcionamiento que tiene el ViewPager de cargar la página adyacente y no la que está en pantalla que la ha cargado cuando te has movido en la que estaba al lado de la actual.

En stackoverflow he visto que a un pavo le pasó lo mismo. Pero no ha obtenido respuesta:
http://stackoverflow.com/questions/10387380/handling-click-events-for-listview-in-viewpager-android

¿Alguna idea al respecto? Gracias!

Edit: Joder... 2 horas perdidas y acabo de dar con el tema. Lo comparto por si alguno un día le pasa.
Este era el código que tenía en el listview

lvExecutions.setOnItemClickListener(new OnItemClickListener() {

			@Override
			public void onItemClick(AdapterView<?> a, View v, int position, long id) {
				mExecution = null;
				mExecution = (cExecution) lvExecutions.getItemAtPosition(position);
				quickActions.show(v);
			}
        });

Estaba recogiendo el contenido de la fila del listView. Pero claro, ese listview es el último cargado. Depurando me he dado cuenta de que el primer parámetro del onItemClick es el adaptador de la vista (sí... el nombre era obvio). Y depurando he visto cómo el primer parámetro era el mismo para los clicks que hacía en diferentes elementos de un listview, pero luego era diferente para los de otro lsitview. Total, que el código ha quedado así:

lvExecutions.setOnItemClickListener(new OnItemClickListener() {

			@Override
			public void onItemClick(AdapterView<?> a, View v, int position, long id) {
				mExecution = null;
				mExecution = (cExecution) a.getAdapter().getItem(position);
				quickActions.show(v);
			}
        });

Ahora accedo al elemento position pero de la vista a.

Por fin!! Dios!! xD

Usuarios habituales

  • Tig
  • MTX_Anubis