SimpleCursorTreeAdapter e CursorLoader para o ExpandableListView

Eu estou tentando consultar um provedor de forma assíncrona usando umCursorLoader com umSimpleCursorTreeAdapter

Aqui está o meuFragment classe que implementa oCursorLoader

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

E aqui está o meu adaptador que subclassesSimpleCursorTreeAdapter

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

  }

}

O problema é que quando eu clico em um dos grupos pai, uma de três coisas acontece no que parece ser uma maneira inconsistente.

1) Ou o grupo abre e os filhos aparecem abaixo dele

2) O grupo não abre e osetChildrenCursor() chamada lança umNullPointerException erro que é pego no bloco try catch

3) O grupo não abre e nenhum erro é lançado

Aqui está uma saída de depuração em um cenário no qual um grupo é expandido e mostrando os filhos:

Quando todos os grupos são exibidos, ele exibe:

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 é o loader_id do cursor do grupo

Então, se eu selecionar um grupo em particular (vamos chamá-lo de grupo A), ele mostrará:

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

O grupo não se expande e oNullPointerException está capturado. Então, se eu selecionar outro grupo (vamos chamá-lo de grupo B), ele exibe:

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

Desta vez, oNullPointerException não é lançado. E, em vez de expandir o grupo B, o grupo A é expandido.

Alguém pode explicar o comportamento que osetChildrenCursor() chamada está expondo?

Eu estou pensando que há um problema com a forma como os CursorLoaders de grupo / filho são instanciadosonCreateLoader(). Para o grupoCursorLoader Eu só quero todos os grupos no meu celular. A criançaCursorLoader deve conter todos os contatos dentro de um grupo. Alguém tem alguma idéia do que poderia ser o problema?

ATUALIZAR

Graças ao conselho do @ Yam, eu modifiquei ogetChildrenCursor() método. Agora estou selecionando a posição groupCursor não o valor de ContactsContract.Groups._ID para passar para a chamada initLoader (). Eu também alterei a lógica para chamar restartLoader () somente quando o loader não é nulo e o loader isReset é 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;
}

Isso definitivamente faz mais sentido e não exibe alguns dos comportamentos erráticos de um grupo que está se expandindo às vezes e não em outras ocasiões.

No entanto, existem contatos que estão sendo exibidos em um grupo ao qual eles não pertencem. E também alguns grupos que têm contatos, mas não mostram contatos. Então parece que ogetChildrenCursor() problemas agora podem ser resolvidos.

Mas agora parece ser uma questão de como os CursorLoaders são instanciados noonCreateLoader() método. É oCursorLoader retornou noonCreateLoader() método para o cursor filho sendo instanciado incorretamente?

ATUALIZAR

Então eu identifiquei um dos meus problemas. NogetChildrenCursor() método se eu passar o groupId para oinitLoader() método então noonCreateLoader() método, quando oCursorLoader é criado, ele irá obter o parâmetro groupid correto para a consulta. No entanto, noonLoadFinished() a chamada parasetChildrenCursor() está sendo passado o ID do carregador para o primeiro parâmetro não o groupPosition. Eu estou supondo que eu tenho que mapear IDs de carregador para agrupar posições em alguma estrutura de dados. Mas não tenho certeza se essa é a melhor abordagem. Alguém tem alguma sugestão?

questionAnswers(3)

yourAnswerToTheQuestion