2012-02-24 15 views
11

Nigdy nie byłem zadowolony z kodu na moim niestandardowym CursorAdapter, aż do dzisiaj zdecydowałem się go przejrzeć i rozwiązać mały problem, który przeszkadzał mi przez długi czas (co ciekawe, żaden z użytkowników mojej aplikacji nigdy nie zgłosił takiego problemu).Czy to niestandardowy CursorAdapter dla ListView poprawnie zakodowany dla Androida?

Oto mały opis moje pytanie:

Mam zwyczaj CursorAdapter nadpisuje newView() i bindView() zamiast getView() jak większość przykładów widzę. Używam wzorca ViewHolder między tymi dwiema metodami. Ale moim głównym problemem był niestandardowy układ, którego używam dla każdego elementu listy, zawiera on ToggleButton.

Problem polegał na tym, że stan przycisku nie był zachowywany, gdy widok pozycji listy przewinął się poza obszar widoku, a następnie przewinięto z powrotem do widoku. Ten problem istniał, ponieważ cursor nigdy nie było świadome, że dane bazy danych uległy zmianie po naciśnięciu przycisku ToggleButton i zawsze pobierały te same dane. Podczas próby kliknięcia przycisku ToggleButton próbowałem wysłać zapytanie, ale problem został rozwiązany bardzo powoli.

Rozwiązałem ten problem i publikuję tutaj całą klasę do sprawdzenia. Dokładnie skomentowałem kod dla tego konkretnego pytania, aby lepiej wyjaśnić moje decyzje dotyczące kodowania.

Czy ten kod wygląda dobrze? Czy w jakiś sposób poprawisz/zoptymalizujesz lub zmienisz to?

P.S: Wiem, że CursorLoader jest oczywistą poprawą, ale nie mam czasu na radzenie sobie z tak dużymi zmianami kodu na razie. Jest to jednak coś, co mam w planie działania.

Oto kod:

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); 
    } 

} 

Odpowiedz

4

Twoje rozwiązanie jest optymalne dodam go do moich broni :) Może postaram się wnieść trochę optymalizację dla połączeń do bazy danych.

Rzeczywiście, z powodu warunków zadania, istnieją tylko trzy rozwiązania:

  1. Aktualizacja tylko jeden wiersz, Requery kursor i przerysować wszystkie elementy. (Prosta, brutalna siła).
  2. Zaktualizuj wiersz, buforuj wyniki i użyj pamięci podręcznej do rysowania elementów.
  3. Pamięć podręczna wyników, użyj pamięci podręcznej do rysowania elementów. A kiedy opuścisz tę czynność/fragment, przekaż wyniki do bazy danych.

Dla trzeciego rozwiązania można użyć SparseArray do wyszukiwania zmian.

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. 
} 

Po raz kolejny: od początku ta tablica jest pusta. Kiedy pierwszy raz przełączasz przycisk (następuje zmiana), dodajesz NoteData do tablicy. Kiedy przełączysz przycisk po raz drugi (następuje wycofanie), usuwasz NoteData z tablicy. I tak dalej.

Po zakończeniu wystarczy zamówić tablicę i wprowadzić zmiany do bazy danych.

+0

Podobał mi się pomysł "SparseArray", nie wiedziałem o tej klasie. Wygląda to na bardziej efektywny sposób buforowania stanów przycisków zamiast zapisywania ich wszystkich w "Liście". Ale nie podoba mi się pomysł, aby wyniki były przekazywane do bazy danych tylko wtedy, gdy użytkownik opuszcza działanie. To wymagałoby dodatkowego kodu do obsłużenia tej sytuacji. Podsumowując, wydaje mi się, że wybieram drugie rozwiązanie, które wymieniłeś. Zasadniczo to, co robiłem w pierwszej kolejności. Nadal podobała mi się twoja odpowiedź, ale zamierzam być może jeszcze 1 lub 2 dni :) –

1

To, co widzisz, to ponowne użycie aplikacji Android. Nie sądzę, że robisz coś złego, ponownie pytając o kursor. Po prostu nie używaj funkcji cursor.requery().

Zamiast tego najpierw ustaw zawsze przełącznik na false, a następnie pytaj kursora i włącz go, jeśli musisz.

Może robiłeś to i źle coś zrozumiałem, ale nie sądzę, że powinieneś mieć powolne rezultaty.

Pseudo-kod:

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

będę czekać przed pójściem do CursorLoader. Jak się wydaje, pochodne CursorLoadera nie działają z CursorLoader.