Данные не синхронизированы между пользовательским CursorLoader и CursorAdapter, поддерживающим ListView

Фон:

У меня есть пользовательский 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в макете строки.


РЕДАКТИРОВАТЬ 1

Немного покопавшись в этом, я думаю, что основная проблема здесь заключается в том, как CursorAdapterобрабатывает базовый Cursor. Я пытаюсь понять, как это работает.

Возьмите следующий сценарий для лучшего понимания.

  1. Предположим, что CursorLoaderзакончил загрузку и возвращает Cursor, который теперь имеет 5 строк.
  2. Adapterначинает отображать эти строки. Он перемещает Cursorна следующую позицию и вызываетgetView()
  3. В этот момент, даже когда представление списка находится в процессе рендеринга, строка (, скажем, с _id = 2 ), удаляется из базы данных.
  4. Вот в чем проблема-CursorAdapterпереместил Cursorв положение, соответствующее удаленной строке. Метод bindView()по-прежнему пытается получить доступ к столбцам для этой строки, используя этот Cursor, что недопустимо, и мы получаем исключения.

Вопрос:

  • Верно ли это понимание?Меня особенно интересует пункт 4 выше, где я делаю предположение, что при удалении строки Cursorне обновляется, если я не попрошу об этом.
  • Предполагая, что это правильно, как мне попросить мой CursorAdapterотменить/прервать его рендерингListViewдаже в процессе и попросите его использовать свежийCursor(вернулся через Loader#onContentChanged()иAdapter#notifyDatasetChanged())вместо?

P.S. Вопрос к модераторам :Выносить ли это редактирование в отдельный вопрос?


РЕДАКТИРОВАТЬ 2

Основываясь на предположениях из различных ответов, похоже, что в моем понимании того, как работает Loader, была фундаментальная ошибка. Оказывается,:

  1. Fragmentили Adapterвообще не должны работать напрямую с Loader.
  2. 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.

Что здесь не так?


РЕДАКТИРОВАТЬ 3

Итак, я повторно -написал свой Loaderдля расширения непосредственно из AsyncTaskLoader. Я по-прежнему не вижу, чтобы мои изменения в БД обновлялись, а метод onContentChanged()моего Loaderвызывался, когда я вставлял/удалял строку в БД:-(

Просто чтобы уточнить несколько вещей:

  1. Я использовал код для CursorLoaderи только что изменил одну строку, которая возвращает Cursor. Здесь,Я заменил вызов ContentProviderсвоим кодом DbManager(, который, в свою очередь, использует DatabaseHelperдля выполнения запроса и возврата Cursor)..

    Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras();

  2. Мои вставки/обновления/удаления в базе данных происходят откуда-то еще, а не через 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();
    }

}
28
задан curioustechizen 25 July 2012 в 08:49
поделиться