android - SimpleCursorTreeAdapter y CursorLoader for ExpandableListView
simplecursoradapter expandablelistadapter (3)
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;
}
}
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<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 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?
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 uso Bundle para obtener datos de 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 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.