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