Вопрос по android – SimpleCursorTreeAdapter и CursorLoader для ExpandableListView

22

Я пытаюсь асинхронно запросить поставщика с помощьюCursorLoader сSimpleCursorTreeAdapter

Вот мойFragment класс, который реализует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);
  }
}

А вот мой адаптер, который подклассы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); 
    } 

  }

}

Проблема в том, что когда я щелкаю одну из родительских групп, происходит одно из трех действий, которые кажутся противоречивыми.

1) Либо группа открывается, а дети появляются под ней

2) группа не открывается иsetChildrenCursor() вызов бросаетNullPointerException ошибка, которая попадает в блок try catch

3) группа не открывается и не выдается ошибка

Вот некоторые выходные данные отладки в сценарии, в котором группа расширена и показывает дочерние элементы:

Когда все группы отображаются, он выводит:

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 является loader_id группового курсора

Затем, если я выберу одну конкретную группу (давайте просто назовем ее группой А), она выдаст:

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

Группа не расширяется иNullPointerException пойман Затем, если я выберу другую группу (давайте просто назовем ее группой B), она выдаст:

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

На этот разNullPointerException не брошен. И вместо расширения группы B группа A расширяется.

Может ли кто-нибудь объяснить поведение, котороеsetChildrenCursor() колл выставляется?

Я думаю, что есть проблема с тем, как создается экземпляр CursorLoaders группы / потомка вonCreateLoader(), Для группыCursorLoader Я просто хочу, чтобы все группы в моем телефоне. РебенокCursorLoader должен содержать все контакты в группе. У кого-нибудь есть идеи, в чем может быть проблема?

UPDATE

Благодаря совету @ Yam я изменилgetChildrenCursor() метод. Сейчас я выбираю позицию groupCursor, а не значение ContactsContract.Groups._ID для передачи в вызов initLoader (). Я также изменил логику для вызова restartLoader (), только когда загрузчик не равен нулю, а загрузчик 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;
}

Это определенно имеет больше смысла и не демонстрирует некоторого хаотичного поведения группы, расширяющейся иногда, а не в другие времена.

Однако есть контакты, которые отображаются в группе, к которой они не принадлежат. А также некоторые группы, в которых есть контакты, но они не показывают никаких контактов. Так что кажется, чтоgetChildrenCursor() проблемы теперь могут быть решены.

Но теперь похоже, что вопрос о том, как создаются CursorLoaders вonCreateLoader() метод. ЭтоCursorLoader вернулся вonCreateLoader() метод для экземпляра дочернего курсора неправильно?

UPDATE

Итак, я определил одну из моих проблем. вgetChildrenCursor() метод, если я передам groupId вinitLoader() метод затем вonCreateLoader() метод, когдаCursorLoader создается, он получит правильный параметр groupid для запроса. Однако вonLoadFinished() призыв кsetChildrenCursor() передается идентификатор загрузчика для первого параметра, а не для groupPosition. Я предполагаю, что мне нужно отобразить идентификаторы загрузчика для группировки позиций в некоторой структуре данных. Но я не уверен, что это лучший подход. У кого-нибудь есть предложения?

GroupCursor был установлен в методе onLoadFinished () объекта LoaderManager. Я прошел через код с помощью отладчика и в методе getChildenCursor () groupCursor всегда определяется. toobsco42
Я не знаю, это действительно звучит так, как будто вы не всегда получаете населенный childCursor ... Barak
Что именно вы имеете в виду под населением childCursor? Как я могу проверить что-то подобное? toobsco42
Я только что сделал это, но он не использовал CursorLoader, так что меня бросило ... В моей реализации getChildrenCursor возвращает курсор. С менеджером загрузок, куда на самом деле идут эти курсор / данные? Если вы не вводите курсор в конструктор, то, что вводится вgetChildrenCursor как группаКурсор? Barak

Ваш Ответ

3   ответа
1

У меня плохой опыт использования ExpandableListView. Его поведение в разных версиях Android различно. Если вы еще не слишком углубились в это, вы можете подумать о редизайне вашего интерфейса.

Во всяком случае, на ваши вопросы, я предлагаю вам рассмотреть эти 3 пункта.

Во-первых, в вашем вызове инициализировать загрузчик курсора детей

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

Идентификатор groupId, который вы передали, является значением ContactsContract.Groups._ID. Затем вы используете этот идентификатор в первом параметре setChildrenCursor. Это, вероятно, неправильно. Вместо того, чтобы передавать groupId в initLoader, попробуйте передать в позиции курсора группы. Например:

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

Во-вторых, вы можете видеть, что в коде, который я предложил выше, вам, вероятно, следует вызывать restartLoader только тогда, когда loader не равен null, а loader isReset равен false.

В-третьих, вам нужно вернуть значение для вызова getChildrenCursor, который, я считаю, должен быть нулевым.

Это определенно имеет смысл и кажется ближе к тому, что я хочу. Но у меня все еще есть проблемы, которые я упомянул в разделе ОБНОВЛЕНИЕ в вопросе выше. toobsco42
2

В моем случае я использую первый аргумент initLoader (или restartLoader), чтобы задать положение группы для обратного вызова, и использую Bundle, чтобы получить дочерние данные в getChildrenCursor.

Как следование;

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) {
    }
}
Это определенно имеет смысл и кажется ближе к тому, что я хочу. Но у меня все еще есть проблемы, которые я упомянул в разделе ОБНОВЛЕНИЕ в вопросе выше. toobsco42
Это не сработает, если вам когда-нибудь понадобится перезапустить загрузчик, поскольку дополнительные функции используются только во время создания загрузчика.
16

Итак, я понял, что мне нужно сопоставить загрузчики с групповыми позициями, и это решило мою проблему:

Вот мойFragment класс, который реализует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);
  }
}

А вот мой адаптер, который подклассы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;
  }

}
По словам двух инженеров Google (Romain Guy & Dianne Hackborn),groups.google.com/forum/?fromgroups#!starred/android-developers/… они рекомендуют использовать SparseArrays поверх HashMaps. Поэтому я поверю на слово. toobsco42
Согласно этому анализу:mobile.dzone.com/articles/tweaking-your-android  они достигают аналогичных результатов для числа элементов, которые у меня есть в хэш-карте, которая составляет менее 1000. toobsco42
Обратите внимание, что Android имеет SparseIntArray (см.developer.android.com/reference/android/util/…) который лучше использовать, чем HashMap & Integer, Integer & gt; и служит той же цели.
должен ли метод сброса быть похожим на mAdapter.setChildrenCursor (position, null); вместо mAdapter.setChildrenCursor (id, null);
Ницца. Я читал что-то подобное раньше, ключом является «упаковка / распаковка примитивных типов», потому что Integer - это объектная оболочка для целых. SparseArrays имеют дело только с примитивными типами, так что нет никакого бокса / распаковки, что, я думаю, приводит к меньшему количеству сборщиков мусора.

Похожие вопросы