Join FlipAndroid.COM Telegram Group: https://t.me/joinchat/F_aqThGkhwcLzmI49vKAiw


SimpleCursorTreeAdapter y CursorLoader para ExpandableListView

Estoy intentando asincrónicamente consultar a 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<Cursor> { 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<Cursor> 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<Cursor> 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<Cursor> 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 de padres una de tres cosas sucede en lo que parece ser una moda inconsistente.

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

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

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

Esta es una salida de depuración en un escenario en el que se amplía un grupo y muestra a los niños:

Cuando se muestran todos los grupos, se emite:

 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

Entonces, si selecciono un grupo en particular (vamos a llamarlo grupo A) sale:

 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 se captura la NullPointerException . Entonces si selecciono otro grupo (llamémosle el grupo B) sale:

 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 

En esta ocasión, la NullPointerException no se inicia. Y en lugar del grupo B en expansión, el grupo A se amplía.

¿Puede alguien explicar el comportamiento que la llamada setChildrenCursor() está exhibiendo?

Estoy pensando que hay un problema con la forma en que el grupo / hijo CursorLoaders se instancia en onCreateLoader() . Para el grupo CursorLoader sólo quiero que 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 @ Yam ahora he modificado el método getChildrenCursor() . Ahora estoy seleccionando la posición groupCursor no el valor de ContactsContract.Groups._ID para pasar a la llamada initLoader (). También cambié la lógica para llamar a restartLoader () sólo cuando loader no es null y loader isReset es false.

 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 algunos de los comportamientos erráticos de un grupo que se expande a veces y no otras veces.

Sin embargo, hay contactos que se muestran en un grupo al que no pertenecen. Y también algunos grupos que tienen contactos en ellos, pero no mostrará ningún contacto. Por lo tanto, parece que los problemas getChildrenCursor() pueden ahora ser resueltos.

Pero ahora parece ser un problema de cómo los CursorLoaders se instancian en el método onCreateLoader() . ¿ CursorLoader devuelve CursorLoader en el método onCreateLoader() para que el cursor secundario se instancia incorrectamente?

ACTUALIZAR

Así que he identificado uno de mis problemas. En el método getChildrenCursor() si paso el 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 el onLoadFinished() la llamada a setChildrenCursor() se pasa el identificador del cargador para el primer parámetro no groupPosition. Supongo que tengo que asignar ids de cargador a posiciones de grupo en alguna estructura de datos. Pero no estoy seguro de si este es el mejor enfoque. ¿Alguien tiene alguna sugerencia?

3 Solutions collect form web for “SimpleCursorTreeAdapter y CursorLoader para ExpandableListView”

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<Cursor> { 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<Cursor> 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<Cursor> 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<Integer,Integer> 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<Cursor> 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<Integer, Integer> 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<Integer, Integer>(); } @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<Integer, Integer> getGroupMap() { return mGroupMap; } } 

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

Como siguiendo;

 public class ExpandableListAdapter extends SimpleCursorTreeAdapter implements LoaderManager.LoaderCallbacks<Cursor> { 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<Cursor> 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<Cursor> loader, Cursor cursor) { setChildrenCursor(loader.getId(), cursor); } @Override public void onLoaderReset(Loader<Cursor> loader) { } } 

Tengo mala experiencia con el uso de ExpandableListView. Su comportamiento en diferentes versiones de Android son diferentes. Si no está demasiado profundamente en él, puede que desee considerar el rediseño de su interfaz.

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

En primer lugar, en su llamada a init los niños cargador de cursor

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

El grupo que pasó es el valor de ContactsContract.Groups._ID. A continuación, utilice este identificador en el primer parámetro de setChildrenCursor. Esto es probablemente incorrecto. En lugar de pasar el ID de grupo al initLoader, 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 he sugerido anteriormente, probablemente debería llamar a restartLoader sólo cuando loader no es nulo y loader isReset es false.

En tercer lugar, es necesario devolver un valor para la llamada getChildrenCursor, que creo que probablemente debería ser nulo.

FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.