2012-02-24 34 views
11

Nunca he estado contento con el código en mi CursorAdapter personalizado hasta el día de hoy decidí revisarlo y solucionar un pequeño problema que me molestaba durante mucho tiempo (curiosamente, ninguno de los usuarios de mi aplicación alguna vez reportó tal problema)¿Este CursorAdapter personalizado para un ListView está codificado correctamente para Android?

He aquí una pequeña descripción de mi pregunta:

Mi CursorAdapter personalizada anula newView() y bindView() en lugar de getView() como la mayoría de los ejemplos que veo. Yo uso el patrón ViewHolder entre estos 2 métodos. Pero mi problema principal era con el diseño personalizado que estoy usando para cada elemento de la lista, contiene un ToggleButton.

El problema era que el estado del botón no se mantenía cuando una vista de elemento de lista se desplazaba fuera de la vista y luego se desplazaba hacia atrás en la vista. Este problema existía porque el cursor nunca fue consciente de que los datos de la base de datos cambiaron cuando se presionó ToggleButton y siempre estaba tirando de los mismos datos. Intenté volver a consultar el cursor al hacer clic en ToggleButton y eso resolvió el problema, pero fue muy lento.

He resuelto este problema y estoy publicando toda la clase aquí para su revisión. He comentado el código a fondo para esta pregunta específica para explicar mejor mis decisiones de codificación.

¿Le parece que este código es bueno para usted? ¿Lo mejorarías/optimizarías o lo cambiarías de alguna manera?

P.S: Sé que CursorLoader es una mejora obvia, pero no tengo tiempo para lidiar con reescrituras de código tan grandes por el momento. Sin embargo, es algo que tengo en la hoja de ruta.

Aquí está el código:

public class NotesListAdapter extends CursorAdapter implements OnClickListener { 

    private static class ViewHolder { 
     ImageView icon; 
     TextView title; 
     TextView description; 
     ToggleButton visibility; 
    } 

    private static class NoteData { 
     long id; 
     int iconId; 
     String title; 
     String description; 
     int position; 
    } 

    private LayoutInflater mInflater; 

    private NotificationHelper mNotificationHelper; 
    private AgendaNotesAdapter mAgendaAdapter; 

    /* 
    * This is used to store the state of the toggle buttons for each item in the list 
    */ 
    private List<Boolean> mToggleState; 

    private int mColumnRowId; 
    private int mColumnTitle; 
    private int mColumnDescription; 
    private int mColumnIconName; 
    private int mColumnVisibility; 

    public NotesListAdapter(Context context, Cursor cursor, NotificationHelper helper, AgendaNotesAdapter adapter) { 
     super(context, cursor); 

     mInflater = LayoutInflater.from(context); 

     /* 
     * Helper class to post notifications to the status bar and database adapter class to update 
     * the database data when the user presses the toggle button in any of items in the list 
     */ 
     mNotificationHelper = helper; 
     mAgendaAdapter = adapter; 

     /* 
     * There's no need to keep getting the column indexes every time in bindView() (as I see in 
     * a few examples) so I do it once and save the indexes in instance variables 
     */ 
     findColumnIndexes(cursor); 

     /* 
     * Populate the toggle button states for each item in the list with the corresponding value 
     * from each record in the database, but isn't this a slow operation? 
     */ 
     for(mToggleState = new ArrayList<Boolean>(); !cursor.isAfterLast(); cursor.moveToNext()) { 
      mToggleState.add(cursor.getInt(mColumnVisibility) != 0); 
     } 
    } 

    @Override 
    public View newView(Context context, Cursor cursor, ViewGroup parent) { 
     View view = mInflater.inflate(R.layout.list_item_note, null); 

     /* 
     * The ViewHolder pattern is here only used to prevent calling findViewById() all the time 
     * in bindView(), we only need to find all the views once 
     */ 
     ViewHolder viewHolder = new ViewHolder(); 

     viewHolder.icon = (ImageView)view.findViewById(R.id.imageview_icon); 
     viewHolder.title = (TextView)view.findViewById(R.id.textview_title); 
     viewHolder.description = (TextView)view.findViewById(R.id.textview_description); 
     viewHolder.visibility = (ToggleButton)view.findViewById(R.id.togglebutton_visibility); 

     /* 
     * I also use newView() to set the toggle button click listener for each item in the list 
     */ 
     viewHolder.visibility.setOnClickListener(this); 

     view.setTag(viewHolder); 

     return view; 
    } 

    @Override 
    public void bindView(View view, Context context, Cursor cursor) { 
     Resources resources = context.getResources(); 

     int iconId = resources.getIdentifier(cursor.getString(mColumnIconName), 
       "drawable", context.getPackageName()); 

     String title = cursor.getString(mColumnTitle); 
     String description = cursor.getString(mColumnDescription); 

     /* 
     * This is similar to the ViewHolder pattern and it's need to access the note data when the 
     * onClick() method is fired 
     */ 
     NoteData noteData = new NoteData(); 

     /* 
     * This data is needed to post a notification when the onClick() method is fired 
     */ 
     noteData.id = cursor.getLong(mColumnRowId); 
     noteData.iconId = iconId; 
     noteData.title = title; 
     noteData.description = description; 

     /* 
     * This data is needed to update mToggleState[POS] when the onClick() method is fired 
     */ 
     noteData.position = cursor.getPosition(); 

     /* 
     * Get our ViewHolder with all the view IDs found in newView() 
     */ 
     ViewHolder viewHolder = (ViewHolder)view.getTag(); 

     /* 
     * The Html.fromHtml is needed but the code relevant to that was stripped 
     */ 
     viewHolder.icon.setImageResource(iconId); 
     viewHolder.title.setText(Html.fromHtml(title)); 
     viewHolder.description.setText(Html.fromHtml(description)); 

     /* 
     * Set the toggle button state for this list item from the value in mToggleState[POS] 
     * instead of getting it from the database with 'cursor.getInt(mColumnVisibility) != 0' 
     * otherwise the state will be incorrect if it was changed between the item view scrolling 
     * out of view and scrolling back into view 
     */ 
     viewHolder.visibility.setChecked(mToggleState.get(noteData.position)); 

     /* 
     * Again, save the note data to be accessed when onClick() gets fired 
     */ 
     viewHolder.visibility.setTag(noteData); 
    } 

    @Override 
    public void onClick(View view) { 
     /* 
     * Get the new state directly from the toggle button state 
     */ 
     boolean visibility = ((ToggleButton)view).isChecked(); 

     /* 
     * Get all our note data needed to post (or remove) a notification 
     */ 
     NoteData noteData = (NoteData)view.getTag(); 

     /* 
     * The toggle button state changed, update mToggleState[POS] to reflect that new change 
     */ 
     mToggleState.set(noteData.position, visibility); 

     /* 
     * Post the notification or remove it from the status bar depending on toggle button state 
     */ 
     if(visibility) { 
      mNotificationHelper.postNotification(
        noteData.id, noteData.iconId, noteData.title, noteData.description); 
     } else { 
      mNotificationHelper.cancelNotification(noteData.id); 
     } 

     /* 
     * Update the database note item with the new toggle button state, without the need to 
     * requery the cursor (which is slow, I've tested it) to reflect the new toggle button state 
     * in the list because the value was saved in mToggleState[POS] a few lines above 
     */ 
     mAgendaAdapter.updateNote(noteData.id, null, null, null, null, visibility); 
    } 

    private void findColumnIndexes(Cursor cursor) { 
     mColumnRowId = cursor.getColumnIndex(AgendaNotesAdapter.KEY_ROW_ID); 
     mColumnTitle = cursor.getColumnIndex(AgendaNotesAdapter.KEY_TITLE); 
     mColumnDescription = cursor.getColumnIndex(AgendaNotesAdapter.KEY_DESCRIPTION); 
     mColumnIconName = cursor.getColumnIndex(AgendaNotesAdapter.KEY_ICON_NAME); 
     mColumnVisibility = cursor.getColumnIndex(AgendaNotesAdapter.KEY_VISIBILITY); 
    } 

} 

Respuesta

4

Su solución es óptima una I añadiremos a mis armas :) Tal vez, voy a tratar de poner un poco de optimización para las llamadas a la base de datos.

De hecho, debido a las condiciones de la tarea, sólo hay tres soluciones:

  1. actualización sólo una fila, requery cursor y volver a dibujar todos los elementos. (Recta, fuerza bruta).
  2. Actualice la fila, almacene en caché los resultados y use la memoria caché para dibujar elementos.
  3. Almacenar en caché los resultados, usar la memoria caché para elementos de dibujo. Y cuando salga de esta actividad/fragmento, envíe los resultados a la base de datos.

Para la tercera solución, puede usar SparseArray para buscar los cambios.

private SparseArray<NoteData> mArrayViewHolders; 

public void onClick(View view) { 
    //here your logic with NoteData. 
    //start of my improve 
    if (mArrayViewHolders.get(selectedPosition) == null) { 
     // put the change into array 
     mArrayViewHolders.put(selectedPosition, noteData); 
    } else { 
     // rollback the change 
     mArrayViewHolders.delete(selectedPosition); 
    } 
    //end of my improve 
    //we don't commit the changes to database. 
} 

Una vez más: desde el principio esta matriz está vacía. Cuando alternas el botón por primera vez (hay un cambio), agregas NoteData a la matriz. Cuando alternas el botón por segunda vez (hay una reversión), eliminas NoteData de la matriz. Y así.

Cuando termine, solo solicite la matriz e inserte los cambios en la base de datos.

+0

Me gustó la idea de un 'SparseArray', no sabía nada de esa clase. Parece una forma más eficiente de almacenar en caché los estados de los botones en lugar de guardarlos todos en una 'Lista'. Pero no me gusta la idea de solo comprometer los resultados a la base de datos cuando el usuario abandona la actividad. Eso necesitaría código adicional para manejar esa situación. Entonces, al final, creo que estoy optando por la segunda solución que enumeraste. Que es básicamente lo que estaba haciendo en primer lugar. Todavía me gustó tu respuesta, pero voy tal vez 1 o 2 días más :) –

1

Lo que está viendo es la vista de uso de Android. No creo que estés haciendo algo mal al consultar el cursor otra vez. Simplemente no use la función cursor.requery().

En su lugar, establezca la alternancia en falso al principio siempre y luego pregunte el cursor y actívelo si es necesario.

Tal vez estaba haciendo eso y entendí mal algo, sin embargo, no creo que deba tener resultados lentos al hacerlo.

Pseudo-código:

getView(){ 
setToggleFalse(); 
boolean value = Boolean.valueOf(cursor.getString('my column')); 
if (value){ 
    setToggleTrue(); 
} 
} 
1

Yo esperaría antes de ir a CursorLoader. Como parece, los derivados de CursorLoader no funcionan con CursorLoader.

Cuestiones relacionadas