22

estoy tratando de consultar de forma asincrónica un proveedor mediante el uso de un CursorLoader con un SimpleCursorTreeAdapterSimpleCursorTreeAdapter y CursorLoader para ExpandableListView

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

    } 

} 

The 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 bien el grupo se abre y los niños aparecen debajo de ella

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

3) el grupo no se abre y no hay error se lanza

Aquí hay alguna información de depuración en un escenario en el que se expande un grupo y mostrando a los niños:

Cuando todos los grupos están disp layed que SALIDAS:

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 grupo

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

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 el NullPointerException. Entonces, si selecciono otro grupo (vamos a llamarlo grupo B) que da salida:

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, el 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 está mostrando la llamada setChildrenCursor()?

Creo que hay un problema con la creación de instancias de CursorLoaders grupales/secundarios en onCreateLoader(). Para el grupo CursorLoader solo quiero todos los grupos en mi teléfono. El hijo CursorLoader debe contener todos los contactos dentro de un grupo. ¿Alguien tiene alguna idea de lo que podría ser el problema?

ACTUALIZACIÓN

Gracias a @ consejo de Yam 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 presenta algunos de los comportamientos erráticos de un grupo en expansión veces y no otras veces.

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 de getChildrenCursor() ahora pueden resolverse.

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

ACTUALIZACIÓN

Así que han identificado a uno de mis problemas. En el método getChildrenCursor() si paso el ID de grupo en el método initLoader(), 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 al setChildrenCursor() se está transfiriendo 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?

+0

que acabo de hacer esto, pero no estaba usando un CursorLoader, por lo que me está tirando ...En mi implementación, getChildrenCursor devuelve un cursor. Con el gestor de carga, ¿a dónde va realmente ese cursor/datos? Si no está introduciendo un cursor en el constructor, ¿qué se está introduciendo en 'getChildrenCursor' como groupCursor? – Barak

+0

groupCursor se ha establecido en el método onLoadFinished() del LoaderManager. He recorrido el código con un depurador y en el método getChildenCursor(), groupCursor siempre está definido. – toobsco42

+0

No sé, realmente suena como que no siempre obtienes un niño pobladoCursor ... – Barak

Respuesta

16

Entonces me di cuenta de que necesitaba para mapear loaderids a groupPositions y esto solucionó mi problema:

Aquí es 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í es 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; 
    } 

} 
+2

Nota, Android tiene un SparseIntArray (ver http://developer.android.com/reference/android/util/SparseIntArray.html) que es mejor usar que HashMap y cumple la misma función. –

+0

Gracias por la recomendación. Le voy a dar una oportunidad. – toobsco42

+1

De acuerdo con este análisis: http://mobile.dzone.com/articles/tweaking-your-android logran resultados similares para la cantidad de elementos que tengo en el hashmap que es menos de 1,000. – toobsco42

1

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.

primer lugar, en su llamada a init el cargador del cursor niños

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

El groupId que ha pasado 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 ID de grupo en el cargador de inicio, 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, se puede ver que en el código he sugerido más arriba, probablemente debería llamar restartLoader sólo cuando cargador no es nulo y el cargador isReset es falsa.

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

+0

Esto definitivamente tiene sentido y parece estar más cerca de lo que exactamente quiero. Pero todavía tengo problemas que mencioné en ACTUALIZAR en la pregunta anterior. – toobsco42

2

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

Me gusta;

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) { 
    } 
} 
+1

esto no funcionará si alguna vez necesita reiniciar el cargador, ya que los extras solo se usan durante el tiempo de construcción del cargador. –

Cuestiones relacionadas