У меня есть пользовательский CursorLoader
, который работает напрямую с базой данных SQLite вместо использования ContentProvider
. Этот загрузчик работает с ListFragment
, поддерживаемым CursorAdapter
. Все идет нормально.
Для упрощения предположим, что в пользовательском интерфейсе есть кнопка «Удалить». Когда пользователь нажимает это, я удаляю строку из БД, а также вызываю onContentChanged()
в моем загрузчике. Кроме того, при обратном вызове onLoadFinished()
я вызываю notifyDatasetChanged()
на своем адаптере, чтобы обновить пользовательский интерфейс.
Когда команды удаления происходят в быстрой последовательности, что означает, что onContentChanged()
вызывается в быстрой последовательности,bindView()
в конечном итоге работает с устаревшими данными . Это означает, что строка была удалена, но ListView все еще пытается отобразить эту строку. Это приводит к исключениям курсора.
Что я делаю не так?
Это пользовательский CursorLoader (, основанный на этом совете г-жи Дианы Хакборн)
/**
* An implementation of CursorLoader that works directly with SQLite database
* cursors, and does not require a ContentProvider.
*
*/
public class VideoSqliteCursorLoader extends CursorLoader {
/*
* This field is private in the parent class. Hence, redefining it here.
*/
ForceLoadContentObserver mObserver;
public VideoSqliteCursorLoader(Context context) {
super(context);
mObserver = new ForceLoadContentObserver();
}
public VideoSqliteCursorLoader(Context context, Uri uri,
String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
super(context, uri, projection, selection, selectionArgs, sortOrder);
mObserver = new ForceLoadContentObserver();
}
/*
* Main logic to load data in the background. Parent class uses a
* ContentProvider to do this. We use DbManager instead.
*
* (non-Javadoc)
*
* @see android.support.v4.content.CursorLoader#loadInBackground()
*/
@Override
public Cursor loadInBackground() {
Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras();
if (cursor != null) {
// Ensure the cursor window is filled
int count = cursor.getCount();
registerObserver(cursor, mObserver);
}
return cursor;
}
/*
* This mirrors the registerContentObserver method from the parent class. We
* cannot use that method directly since it is not visible here.
*
* Hence we just copy over the implementation from the parent class and
* rename the method.
*/
void registerObserver(Cursor cursor, ContentObserver observer) {
cursor.registerContentObserver(mObserver);
}
}
Фрагмент моего класса ListFragment
, показывающий обратные вызовы LoaderManager
; а также метод refresh()
, который я вызываю всякий раз, когда пользователь добавляет/удаляет запись.
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mListView = getListView();
/*
* Initialize the Loader
*/
mLoader = getLoaderManager().initLoader(LOADER_ID, null, this);
}
@Override
public Loader onCreateLoader(int id, Bundle args) {
return new VideoSqliteCursorLoader(getActivity());
}
@Override
public void onLoadFinished(Loader loader, Cursor data) {
mAdapter.swapCursor(data);
mAdapter.notifyDataSetChanged();
}
@Override
public void onLoaderReset(Loader loader) {
mAdapter.swapCursor(null);
}
public void refresh() {
mLoader.onContentChanged();
}
Мой CursorAdapter
является обычным с newView()
над -для возврата недавно раздутого XML макета строки и bindView()
с использованием Cursor
для привязки столбцов к View
в макете строки.
Немного покопавшись в этом, я думаю, что основная проблема здесь заключается в том, как CursorAdapter
обрабатывает базовый Cursor
. Я пытаюсь понять, как это работает.
Возьмите следующий сценарий для лучшего понимания.
CursorLoader
закончил загрузку и возвращает Cursor
, который теперь имеет 5 строк.Adapter
начинает отображать эти строки. Он перемещает Cursor
на следующую позицию и вызываетgetView()
CursorAdapter
переместил Cursor
в положение, соответствующее удаленной строке. Метод bindView()
по-прежнему пытается получить доступ к столбцам для этой строки, используя этот Cursor
, что недопустимо, и мы получаем исключения.Cursor
не обновляется, если я не попрошу об этом.CursorAdapter
отменить/прервать его рендерингListView
даже в процессе и попросите его использовать свежийCursor
(вернулся через Loader#onContentChanged()
иAdapter#notifyDatasetChanged()
)вместо?P.S. Вопрос к модераторам :Выносить ли это редактирование в отдельный вопрос?
Основываясь на предположениях из различных ответов, похоже, что в моем понимании того, как работает Loader
, была фундаментальная ошибка. Оказывается,:
Fragment
или Adapter
вообще не должны работать напрямую с Loader
.Loader
должен отслеживать все изменения в данных и должен просто давать Adapter
новый Cursor
в onLoadFinished()
всякий раз, когда данные изменяются.Вооружившись этим пониманием, я попытался внести следующие изменения. -Никаких действий с Loader
. Метод обновления теперь ничего не делает.
Кроме того, для отладки того, что происходит внутри Loader
и ContentObserver
, я придумал это:
public class VideoSqliteCursorLoader extends CursorLoader {
private static final String LOG_TAG = "CursorLoader";
//protected Cursor mCursor;
public final class CustomForceLoadContentObserver extends ContentObserver {
private final String LOG_TAG = "ContentObserver";
public CustomForceLoadContentObserver() {
super(new Handler());
}
@Override
public boolean deliverSelfNotifications() {
return true;
}
@Override
public void onChange(boolean selfChange) {
Utils.logDebug(LOG_TAG, "onChange called; selfChange = "+selfChange);
onContentChanged();
}
}
/*
* This field is private in the parent class. Hence, redefining it here.
*/
CustomForceLoadContentObserver mObserver;
public VideoSqliteCursorLoader(Context context) {
super(context);
mObserver = new CustomForceLoadContentObserver();
}
/*
* Main logic to load data in the background. Parent class uses a
* ContentProvider to do this. We use DbManager instead.
*
* (non-Javadoc)
*
* @see android.support.v4.content.CursorLoader#loadInBackground()
*/
@Override
public Cursor loadInBackground() {
Utils.logDebug(LOG_TAG, "loadInBackground called");
Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras();
//mCursor = AppGlobals.INSTANCE.getDbManager().getAllCameras();
if (cursor != null) {
// Ensure the cursor window is filled
int count = cursor.getCount();
Utils.logDebug(LOG_TAG, "Count = " + count);
registerObserver(cursor, mObserver);
}
return cursor;
}
/*
* This mirrors the registerContentObserver method from the parent class. We
* cannot use that method directly since it is not visible here.
*
* Hence we just copy over the implementation from the parent class and
* rename the method.
*/
void registerObserver(Cursor cursor, ContentObserver observer) {
cursor.registerContentObserver(mObserver);
}
/*
* A bunch of methods being overridden just for debugging purpose.
* We simply include a logging statement and call through to super implementation
*
*/
@Override
public void forceLoad() {
Utils.logDebug(LOG_TAG, "forceLoad called");
super.forceLoad();
}
@Override
protected void onForceLoad() {
Utils.logDebug(LOG_TAG, "onForceLoad called");
super.onForceLoad();
}
@Override
public void onContentChanged() {
Utils.logDebug(LOG_TAG, "onContentChanged called");
super.onContentChanged();
}
}
А вот фрагменты моих Fragment
иLoaderCallback
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mListView = getListView();
/*
* Initialize the Loader
*/
getLoaderManager().initLoader(LOADER_ID, null, this);
}
@Override
public Loader onCreateLoader(int id, Bundle args) {
return new VideoSqliteCursorLoader(getActivity());
}
@Override
public void onLoadFinished(Loader loader, Cursor data) {
Utils.logDebug(LOG_TAG, "onLoadFinished()");
mAdapter.swapCursor(data);
}
@Override
public void onLoaderReset(Loader loader) {
mAdapter.swapCursor(null);
}
public void refresh() {
Utils.logDebug(LOG_TAG, "CamerasListFragment.refresh() called");
//mLoader.onContentChanged();
}
Теперь, всякий раз, когда в БД (добавляется/удаляется строка ), метод onChange()
метода ContentObserver
должен называться -правильно? Я не вижу, чтобы это происходило. Мой ListView
никогда не показывает никаких изменений. Единственный раз, когда я вижу какие-либо изменения, это если я явно вызываю onContentChanged()
на Loader
.
Что здесь не так?
Итак, я повторно -написал свой Loader
для расширения непосредственно из AsyncTaskLoader
. Я по-прежнему не вижу, чтобы мои изменения в БД обновлялись, а метод onContentChanged()
моего Loader
вызывался, когда я вставлял/удалял строку в БД:-(
Просто чтобы уточнить несколько вещей:
Я использовал код для CursorLoader
и только что изменил одну строку, которая возвращает Cursor
. Здесь,Я заменил вызов ContentProvider
своим кодом DbManager
(, который, в свою очередь, использует DatabaseHelper
для выполнения запроса и возврата Cursor
)..
Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras();
Мои вставки/обновления/удаления в базе данных происходят откуда-то еще, а не через Loader
. В большинстве случаев операции с БД происходят в фоновом режиме Service
, а в некоторых случаях из Activity
. Я напрямую использую свой класс DbManager
для выполнения этих операций.
Чего я до сих пор не понимаю, так это-кто сообщает моему Loader
, что строка была добавлена/удалена/изменена? Другими словами, где ForceLoadContentObserver#onChange()
называется? В моем загрузчике я регистрирую своего наблюдателя наCursor
:
void registerContentObserver(Cursor cursor, ContentObserver observer) {
cursor.registerContentObserver(mObserver);
}
Это будет означать, что Cursor
несет ответственность за уведомление mObserver
об изменении. Но тогда, AFAIK, «Курсор» не является «живым» объектом, который обновляет данные, на которые он указывает, когда данные изменяются в БД.
Вот последняя версия моего загрузчика:
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.support.v4.content.AsyncTaskLoader;
public class VideoSqliteCursorLoader extends AsyncTaskLoader {
private static final String LOG_TAG = "CursorLoader";
final ForceLoadContentObserver mObserver;
Cursor mCursor;
/* Runs on a worker thread */
@Override
public Cursor loadInBackground() {
Utils.logDebug(LOG_TAG, "loadInBackground()");
Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras();
if (cursor != null) {
// Ensure the cursor window is filled
int count = cursor.getCount();
Utils.logDebug(LOG_TAG, "Cursor count = "+count);
registerContentObserver(cursor, mObserver);
}
return cursor;
}
void registerContentObserver(Cursor cursor, ContentObserver observer) {
cursor.registerContentObserver(mObserver);
}
/* Runs on the UI thread */
@Override
public void deliverResult(Cursor cursor) {
Utils.logDebug(LOG_TAG, "deliverResult()");
if (isReset()) {
// An async query came in while the loader is stopped
if (cursor != null) {
cursor.close();
}
return;
}
Cursor oldCursor = mCursor;
mCursor = cursor;
if (isStarted()) {
super.deliverResult(cursor);
}
if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) {
oldCursor.close();
}
}
/**
* Creates an empty CursorLoader.
*/
public VideoSqliteCursorLoader(Context context) {
super(context);
mObserver = new ForceLoadContentObserver();
}
@Override
protected void onStartLoading() {
Utils.logDebug(LOG_TAG, "onStartLoading()");
if (mCursor != null) {
deliverResult(mCursor);
}
if (takeContentChanged() || mCursor == null) {
forceLoad();
}
}
/**
* Must be called from the UI thread
*/
@Override
protected void onStopLoading() {
Utils.logDebug(LOG_TAG, "onStopLoading()");
// Attempt to cancel the current load task if possible.
cancelLoad();
}
@Override
public void onCanceled(Cursor cursor) {
Utils.logDebug(LOG_TAG, "onCanceled()");
if (cursor != null && !cursor.isClosed()) {
cursor.close();
}
}
@Override
protected void onReset() {
Utils.logDebug(LOG_TAG, "onReset()");
super.onReset();
// Ensure the loader is stopped
onStopLoading();
if (mCursor != null && !mCursor.isClosed()) {
mCursor.close();
}
mCursor = null;
}
@Override
public void onContentChanged() {
Utils.logDebug(LOG_TAG, "onContentChanged()");
super.onContentChanged();
}
}