SimpleCursorTreeAdapter y CursorLoader for ExpandableListView

CursorLoader consultar asincrónicamente un proveedor usando un CursorLoader con un SimpleCursorTreeAdapter

Aquí está mi clase Fragment que implementa el CursorLoader

 public class GroupsListFragment extends ExpandableListFragment implements LoaderManager.LoaderCallbacks { private final String DEBUG_TAG = getClass().getSimpleName().toString(); private static final String[] CONTACTS_PROJECTION = new String[] { ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME }; private static final String[] GROUPS_SUMMARY_PROJECTION = new String[] { ContactsContract.Groups.TITLE, ContactsContract.Groups._ID, ContactsContract.Groups.SUMMARY_COUNT, ContactsContract.Groups.ACCOUNT_NAME, ContactsContract.Groups.ACCOUNT_TYPE, ContactsContract.Groups.DATA_SET }; GroupsAdapter mAdapter; @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); populateContactList(); getLoaderManager().initLoader(-1, null, this); } public Loader onCreateLoader(int id, Bundle args) { // This is called when a new Loader needs to be created. Log.d(DEBUG_TAG, "onCreateLoader for loader_id " + id); CursorLoader cl; if (id != -1) { // child cursor Uri contactsUri = ContactsContract.Data.CONTENT_URI; String selection = "((" + ContactsContract.CommonDataKinds.GroupMembership.DISPLAY_NAME + " NOTNULL) AND (" + ContactsContract.CommonDataKinds.GroupMembership.HAS_PHONE_NUMBER + "=1) AND (" + ContactsContract.CommonDataKinds.GroupMembership.DISPLAY_NAME + " != '') AND (" + ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID + " = ? ))"; String sortOrder = ContactsContract.CommonDataKinds.GroupMembership.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; String[] selectionArgs = new String[] { String.valueOf(id) }; cl = new CursorLoader(getActivity(), contactsUri, CONTACTS_PROJECTION, selection, selectionArgs, sortOrder); } else { // group cursor Uri groupsUri = ContactsContract.Groups.CONTENT_SUMMARY_URI; String selection = "((" + ContactsContract.Groups.TITLE + " NOTNULL) AND (" + ContactsContract.Groups.TITLE + " != '' ))"; String sortOrder = ContactsContract.Groups.TITLE + " COLLATE LOCALIZED ASC"; cl = new CursorLoader(getActivity(), groupsUri, GROUPS_SUMMARY_PROJECTION, selection, null, sortOrder); } return cl; } public void onLoadFinished(Loader loader, Cursor data) { // Swap the new cursor in. int id = loader.getId(); Log.d(DEBUG_TAG, "onLoadFinished() for loader_id " + id); if (id != -1) { // child cursor if (!data.isClosed()) { Log.d(DEBUG_TAG, "data.getCount() " + data.getCount()); try { mAdapter.setChildrenCursor(id, data); } catch (NullPointerException e) { Log.w("DEBUG","Adapter expired, try again on the next query: " + e.getMessage()); } } } else { mAdapter.setGroupCursor(data); } } public void onLoaderReset(Loader loader) { // This is called when the last Cursor provided to onLoadFinished() // is about to be closed. int id = loader.getId(); Log.d(DEBUG_TAG, "onLoaderReset() for loader_id " + id); if (id != -1) { // child cursor try { mAdapter.setChildrenCursor(id, null); } catch (NullPointerException e) { Log.w("TAG", "Adapter expired, try again on the next query: " + e.getMessage()); } } else { mAdapter.setGroupCursor(null); } } /** * Populate the contact list */ private void populateContactList() { // Set up our adapter mAdapter = new GroupsAdapter(getActivity(),this, android.R.layout.simple_expandable_list_item_1, android.R.layout.simple_expandable_list_item_1, new String[] { ContactsContract.Groups.TITLE }, // Name for group layouts new int[] { android.R.id.text1 }, new String[] { ContactsContract.Contacts.DISPLAY_NAME }, // Name for child layouts new int[] { android.R.id.text1 }); setListAdapter(mAdapter); } } 

Y aquí está mi adaptador que subclases SimpleCursorTreeAdapter

 public class GroupsAdapter extends SimpleCursorTreeAdapter { private final String DEBUG_TAG = getClass().getSimpleName().toString(); private ContactManager mActivity; private GroupsListFragment mFragment; // Note that the constructor does not take a Cursor. This is done to avoid // querying the database on the main thread. public GroupsAdapter(Context context, GroupsListFragment glf, int groupLayout, int childLayout, String[] groupFrom, int[] groupTo, String[] childrenFrom, int[] childrenTo) { super(context, null, groupLayout, groupFrom, groupTo, childLayout, childrenFrom, childrenTo); mActivity = (ContactManager) context; mFragment = glf; } @Override protected Cursor getChildrenCursor(Cursor groupCursor) { // Given the group, we return a cursor for all the children within that group int groupId = groupCursor.getInt(groupCursor .getColumnIndex(ContactsContract.Groups._ID)); Log.d(DEBUG_TAG, "getChildrenCursor() for groupId " + groupId); Loader loader = mActivity.getLoaderManager().getLoader(groupId); if ( loader != null && loader.isReset() ) { mActivity.getLoaderManager().restartLoader(groupId, null, mFragment); } else { mActivity.getLoaderManager().initLoader(groupId, null, mFragment); } } } 

El problema es que cuando hago clic en uno de los grupos principales una de las tres cosas sucede en lo que parece ser una moda incoherente.

1) O el grupo se abre y los niños aparecen debajo de él

2) El grupo no se abre y la llamada a setChildrenCursor() arroja un error NullPointerException que queda atrapado en el bloque try catch

3) El grupo no se abre y no se arroja ningún error

Aquí hay algunos resultados de depuración en un escenario en el que un grupo se expande y muestra los elementos secundarios:

Cuando se muestran todos los grupos, sale:

 05-20 10:08:22.765: D/GroupsListFragment(22132): onCreateLoader for loader_id -1 05-20 10:08:23.613: D/GroupsListFragment(22132): onLoadFinished() for loader_id -1 

-1 es el loader_id del cursor de grupo

Luego, si selecciono un grupo en particular (llamémoslo grupo A), genera:

 05-20 23:22:31.140: D/GroupsAdapter(13844): getChildrenCursor() for groupId 67 05-20 23:22:31.140: D/GroupsListFragment(13844): onCreateLoader for loader_id 67 05-20 23:22:31.254: D/GroupsListFragment(13844): onLoadFinished() for loader_id 67 05-20 23:22:31.254: D/GroupsListFragment(13844): data.getCount() 4 05-20 23:22:31.254: W/GroupsListFragment(13844): Adapter expired, try again on the next query: null 

El grupo no se expande y la NullPointerException queda atrapada. Luego, si selecciono otro grupo (llamémoslo grupo B), genera:

 05-20 23:25:38.089: D/GroupsAdapter(13844): getChildrenCursor() for groupId 3 05-20 23:25:38.089: D/GroupsListFragment(13844): onCreateLoader for loader_id 3 05-20 23:25:38.207: D/GroupsListFragment(13844): onLoadFinished() for loader_id 3 05-20 23:25:38.207: D/GroupsListFragment(13844): data.getCount() 6 

Esta vez, la NullPointerException no se lanza. Y en lugar de que el grupo B se expanda, el grupo A se expande.

¿Alguien puede explicar el comportamiento que setChildrenCursor() llamada a setChildrenCursor() ?

Estoy pensando que hay un problema con la forma en que el grupo / el niño CursorLoaders se instancia en onCreateLoader() . Para el grupo CursorLoader simplemente quiero todos los grupos en mi teléfono. El niño CursorLoader debe contener todos los contactos dentro de un grupo. ¿Alguien tiene alguna idea de lo que podría ser el problema?

ACTUALIZAR

Gracias al consejo de getChildrenCursor() ahora he modificado el método getChildrenCursor() . Ahora estoy seleccionando la posición del grupoCursor no el valor de ContactsContract.Groups._ID para pasar a la llamada initLoader (). También cambié la lógica para llamar a restartLoader () solo cuando el cargador no es nulo y el cargador isReset es falso.

 protected Cursor getChildrenCursor(Cursor groupCursor) { // Given the group, we return a cursor for all the children within that // group int groupPos = groupCursor.getPosition(); Log.d(DEBUG_TAG, "getChildrenCursor() for groupPos " + groupPos); Loader loader = mActivity.getLoaderManager().getLoader(groupPos); if (loader != null && !loader.isReset()) { mActivity.getLoaderManager().restartLoader(groupPos, null, mFragment); } else { mActivity.getLoaderManager().initLoader(groupPos, null, mFragment); } return null; } 

Esto definitivamente tiene más sentido y no muestra algo del comportamiento errático de un grupo que se expande a veces y no en otros.

Sin embargo, hay contactos que se muestran debajo de un grupo al que no pertenecen. Y también algunos grupos que sí tienen contactos pero no muestran ningún contacto. Parece que los problemas con getChildrenCursor() ahora pueden resolverse.

Pero ahora parece ser un problema de cómo se crean instancias de onCreateLoader() en el método onCreateLoader() . ¿ CursorLoader devuelve CursorLoader en el método onCreateLoader() para que el cursor secundario se cree una instancia de forma incorrecta?

ACTUALIZAR

Entonces identifiqué uno de mis problemas. En el método getChildrenCursor() si paso groupId al método initLoader() entonces en el método onCreateLoader() , cuando se crea el CursorLoader obtendrá el parámetro groupid correcto para la consulta. Sin embargo, en onLoadFinished() la llamada a setChildrenCursor() está setChildrenCursor() a la id del cargador para el primer parámetro, no a la posición de grupo. Supongo que tengo que mapear identificadores de cargadores para agrupar posiciones en alguna estructura de datos. Pero no estoy seguro de si este es el mejor enfoque. ¿Alguien tiene alguna sugerencia?

Así que me di cuenta de que necesitaba asignar loaderids a groupPositions y esto resolvió mi problema:

Aquí está mi clase Fragment que implementa el CursorLoader

 public class GroupsListFragment extends ExpandableListFragment implements LoaderManager.LoaderCallbacks { private final String DEBUG_TAG = getClass().getSimpleName().toString(); private static final String[] CONTACTS_PROJECTION = new String[] { ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME }; private static final String[] GROUPS_PROJECTION = new String[] { ContactsContract.Groups.TITLE, ContactsContract.Groups._ID }; GroupsAdapter mAdapter; @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); populateContactList(); // Prepare the loader. Either re-connect with an existing one, // or start a new one. Loader loader = getLoaderManager().getLoader(-1); if (loader != null && !loader.isReset()) { getLoaderManager().restartLoader(-1, null, this); } else { getLoaderManager().initLoader(-1, null, this); } } public Loader onCreateLoader(int id, Bundle args) { // This is called when a new Loader needs to be created. Log.d(DEBUG_TAG, "onCreateLoader for loader_id " + id); CursorLoader cl; if (id != -1) { // child cursor Uri contactsUri = ContactsContract.Data.CONTENT_URI; String selection = "((" + ContactsContract.Contacts.DISPLAY_NAME + " NOTNULL) AND (" + ContactsContract.Contacts.HAS_PHONE_NUMBER + "=1) AND (" + ContactsContract.Contacts.DISPLAY_NAME + " != '') AND (" + ContactsContract.CommonDataKinds.GroupMembership.GROUP_ROW_ID + " = ? ))"; String sortOrder = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; String[] selectionArgs = new String[] { String.valueOf(id) }; cl = new CursorLoader(getActivity(), contactsUri, CONTACTS_PROJECTION, selection, selectionArgs, sortOrder); } else { // group cursor Uri groupsUri = ContactsContract.Groups.CONTENT_URI; String selection = "((" + ContactsContract.Groups.TITLE + " NOTNULL) AND (" + ContactsContract.Groups.TITLE + " != '' ))"; String sortOrder = ContactsContract.Groups.TITLE + " COLLATE LOCALIZED ASC"; cl = new CursorLoader(getActivity(), groupsUri, GROUPS_PROJECTION, selection, null, sortOrder); } return cl; } public void onLoadFinished(Loader loader, Cursor data) { // Swap the new cursor in. int id = loader.getId(); Log.d(DEBUG_TAG, "onLoadFinished() for loader_id " + id); if (id != -1) { // child cursor if (!data.isClosed()) { Log.d(DEBUG_TAG, "data.getCount() " + data.getCount()); HashMap groupMap = mAdapter.getGroupMap(); try { int groupPos = groupMap.get(id); Log.d(DEBUG_TAG, "onLoadFinished() for groupPos " + groupPos); mAdapter.setChildrenCursor(groupPos, data); } catch (NullPointerException e) { Log.w("DEBUG","Adapter expired, try again on the next query: " + e.getMessage()); } } } else { mAdapter.setGroupCursor(data); } } public void onLoaderReset(Loader loader) { // This is called when the last Cursor provided to onLoadFinished() // is about to be closed. int id = loader.getId(); Log.d(DEBUG_TAG, "onLoaderReset() for loader_id " + id); if (id != -1) { // child cursor try { mAdapter.setChildrenCursor(id, null); } catch (NullPointerException e) { Log.w("TAG", "Adapter expired, try again on the next query: " + e.getMessage()); } } else { mAdapter.setGroupCursor(null); } } /** * Populate the contact list */ private void populateContactList() { // Set up our adapter mAdapter = new GroupsAdapter(getActivity(),this, android.R.layout.simple_expandable_list_item_1, android.R.layout.simple_expandable_list_item_1, new String[] { ContactsContract.Groups.TITLE }, // Name for group layouts new int[] { android.R.id.text1 }, new String[] { ContactsContract.Contacts.DISPLAY_NAME }, // Name for child layouts new int[] { android.R.id.text1 }); setListAdapter(mAdapter); } } 

Y aquí está mi adaptador que subclases SimpleCursorTreeAdapter

 public class GroupsAdapter extends SimpleCursorTreeAdapter { private final String DEBUG_TAG = getClass().getSimpleName().toString(); private ContactManager mActivity; private GroupsListFragment mFragment; protected final HashMap mGroupMap; // Note that the constructor does not take a Cursor. This is done to avoid // querying the database on the main thread. public GroupsAdapter(Context context, GroupsListFragment glf, int groupLayout, int childLayout, String[] groupFrom, int[] groupTo, String[] childrenFrom, int[] childrenTo) { super(context, null, groupLayout, groupFrom, groupTo, childLayout, childrenFrom, childrenTo); mActivity = (ContactManager) context; mFragment = glf; mGroupMap = new HashMap(); } @Override protected Cursor getChildrenCursor(Cursor groupCursor) { // Given the group, we return a cursor for all the children within that group int groupPos = groupCursor.getPosition(); int groupId = groupCursor.getInt(groupCursor .getColumnIndex(ContactsContract.Groups._ID)); Log.d(DEBUG_TAG, "getChildrenCursor() for groupPos " + groupPos); Log.d(DEBUG_TAG, "getChildrenCursor() for groupId " + groupId); mGroupMap.put(groupId, groupPos); Loader loader = mActivity.getLoaderManager().getLoader(groupId); if ( loader != null && !loader.isReset() ) { mActivity.getLoaderManager().restartLoader(groupId, null, mFragment); } else { mActivity.getLoaderManager().initLoader(groupId, null, mFragment); } return null; } //Accessor method public HashMap getGroupMap() { return mGroupMap; } } 

En mi caso, utilizo el primer argumento de initLoader (o restartLoader) para dar la posición de grupo para la callback y uso Bundle para obtener datos de niños en getChildrenCursor.

Como siguiendo;

 public class ExpandableListAdapter extends SimpleCursorTreeAdapter implements LoaderManager.LoaderCallbacks { private Context mContext; private LoaderManager mManager; public ExpandableListAdapter( Context context, ExpandableListAdapterListener listener, LoaderManager manager, Cursor groupCursor, int groupLayout, String[] groupFrom, int[] groupTo, int childLayout, String[] childFrom, int[] childTo) { super(context, groupCursor, groupLayout, groupFrom, groupTo, childLayout, childFrom, childTo); mContext = context; mManager = manager; } @Override protected Cursor getChildrenCursor(Cursor groupCursor) { final long idGroup = groupCursor.getLong(groupCursor.getColumnIndex("_id")); Bundle bundle = new Bundle(); bundle.putLong("idGroup", idGroup); int groupPos = groupCursor.getPosition(); if (mManager.getLoader(groupPos) != null && !mManager.getLoader(groupPos).isReset()) { mManager.restartLoader(groupPos, bundle, this); } else { mManager.initLoader(groupPos, bundle, this); } return null; } @Override public Loader onCreateLoader(int groupPos, Bundle bundle) { long idGroup = bundle.getLong("idGroup"); return new CursorLoader( mContext, Provider.URI, new String[]{Table.ID, Table.ID_GROUP, Table.TITLE, Table.CONTEXT}, Table.ID_GROUP + " = ?", new String[]{String.valueOf(idGroup)}, Table.CREATED + " DESC" ); } @Override public void onLoadFinished(Loader loader, Cursor cursor) { setChildrenCursor(loader.getId(), cursor); } @Override public void onLoaderReset(Loader loader) { } } 

Tengo una mala experiencia con el uso de ExpandableListView. Su comportamiento en diferentes versiones de Android es diferente. Si aún no está muy metido en eso, le recomendamos que rediseñe su interfaz.

De todos modos, a sus preguntas, le sugiero que revise estos 3 puntos.

Primero, en su llamada para iniciar el cargador de cursor de los niños

 mActivity.getLoaderManager().initLoader(groupId, null, mFragment); 

El grupo al que usted ingresó es el valor de ContactsContract.Groups._ID. Luego, usa esta identificación en el primer parámetro de setChildrenCursor. Esto es probablemente incorrecto. En lugar de pasar el groupId al iniciador, intente pasar en la posición del cursor de grupo. Por ejemplo:

 int iGroupPos = groupCursor.getPosition(); if ( loader != null && !loader.isReset()) mActivity.getLoaderManager().restartLoader(iGroupPos, null, mFragment); else mActivity.getLoaderManager().initLoader(iGroupPos, null, mFragment); 

En segundo lugar, puede ver que en el código que sugerí anteriormente, probablemente debería llamar a restartLoader solo cuando el cargador no es nulo y el cargador isReset es falso.

En tercer lugar, debe devolver un valor para la llamada a getChildrenCursor, que creo que probablemente sea nulo.