Подтвердить что ты не робот

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

История:

У меня есть пользовательский CursorLoader, который работает непосредственно с SQLite Database вместо использования 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<Cursor> onCreateLoader(int id, Bundle args) {
    return new VideoSqliteCursorLoader(getActivity());
}

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {

    mAdapter.swapCursor(data);
    mAdapter.notifyDataSetChanged();
}

@Override
public void onLoaderReset(Loader<Cursor> loader) {
    mAdapter.swapCursor(null);
}

public void refresh() {     
    mLoader.onContentChanged();
}

My CursorAdapter является просто регулярным с newView(), который перегружен, чтобы возвращать вновь раздутый макет строки XML и bindView() с помощью Cursor для привязки столбцов к View в макете строк.


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

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

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

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

Вопрос:

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

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


EDIT 2

Основываясь на предположении из разных ответов, похоже, что в моем понимании того, как работает 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<Cursor> onCreateLoader(int id, Bundle args) {
    return new VideoSqliteCursorLoader(getActivity());
}

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    Utils.logDebug(LOG_TAG, "onLoadFinished()");
    mAdapter.swapCursor(data);
}

@Override
public void onLoaderReset(Loader<Cursor> 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, вызываемого при вставке/удалении строки в БД: - (

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

  • Я использовал код для 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<Cursor> {
    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();
    }

}
4b9b3361

Ответ 1

Я не уверен на 100%, основываясь на коде, который вы предоставили, но пара штук торчит:

  • Первое, что торчит, это то, что вы включили этот метод в свой ListFragment:

    public void refresh() {     
        mLoader.onContentChanged();
    }
    

    При использовании LoaderManager редко бывает необходимо (и часто опасно) напрямую манипулировать вашим Loader. После первого вызова initLoader, LoaderManager имеет полный контроль над Loader и будет "управлять", вызывая его методы в фоновом режиме. Вы должны быть очень осторожны при вызове методов Loader непосредственно в этом случае, так как это может помешать базовому управлению Loader. Я не могу точно сказать, что ваши звонки на onContentChanged() неверны, поскольку вы не упоминаете об этом в своем сообщении, но это не должно быть необходимо в вашей ситуации (и ни одна из них не должна содержать ссылку на mLoader). Ваш ListFragment не заботится о том, как изменяются изменения... и не заботится о том, как загружаются данные. Все, что он знает, это то, что новые данные будут волшебным образом предоставлены в onLoadFinished, когда они будут доступны.

  • Вы также не должны вызывать mAdapter.notifyDataSetChanged() в onLoadFinished. swapCursor сделает это за вас.

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


Изменить # 1:

Из того, что я могу сказать, CursorLoader полагается на ForceLoadContentObserver (вложенный внутренний класс, представленный в реализации Loader<D>)... так что кажется, что проблема здесь в том, что вы реализуя ваш пользовательский ContentObserver, но ничего не создано для его распознавания. Многие элементы "самообновления" выполняются в реализации Loader<D> и AsyncTaskLoader<D> и поэтому скрыты от конкретного Loader (например, CursorLoader), которые выполняют фактическую работу (т.е. Loader<D> не имеет понятия о CustomForceLoadContentObserver, так почему он должен получать какие-либо уведомления?).

Вы упомянули в своем обновленном сообщении, что вы не можете напрямую обращаться к final ForceLoadContentObserver mObserver;, так как это скрытое поле. Исправлено создание собственного ContentObserver и вызов registerObserver() в методе overriden loadInBackground (что вызовет вызов registerContentObserver на ваш Cursor). Вот почему вы не получаете уведомления... потому что вы использовали пользовательский ContentObserver, который никогда не распознается инфраструктурой Loader.

Чтобы устранить проблему, вы должны иметь свой класс непосредственно extend AsyncTaskLoader<Cursor> вместо CursorLoader (т.е. просто скопируйте и вставьте части, которые вы наследуете из CursorLoader в свой класс). Таким образом, вы не столкнетесь с какими-либо проблемами в скрытом поле ForceLoadContentObserver.

Изменить # 2:

Согласно Commonsware, нет простого способа настроить глобальные уведомления, исходящие из SQLiteDatabase, поэтому SQLiteCursorLoader в своей библиотеке Loaderex полагается на Loader вызов onContentChanged() на себя каждый раз при совершении транзакции. Самый простой способ передачи уведомлений прямо из источника данных - реализовать ContentProvider и использовать CursorLoader. Таким образом, вы можете доверять, что уведомления будут транслироваться на ваш CursorLoader каждый раз, когда ваш Service обновляет базовый источник данных.

Я не сомневаюсь, что существуют другие решения (т.е., возможно, путем создания глобального ContentObserver... или, возможно, даже с помощью метода ContentResolver#notifyChange без ContentProvider)), но самое простое и простое решение похоже, просто реализовать частный ContentProvider.

(убедитесь, что вы установили android:export="false" в теге поставщика в манифесте, чтобы ваш ContentProvider не мог быть замечен другими приложениями!: p)

Ответ 2

На самом деле это не решение вашей проблемы, но для вас это может быть полезно:

Существует метод CursorLoader.setUpdateThrottle(long delayMS), который обеспечивает минимальное время между загрузкой loadInBackground и следующей запланированной загрузкой.

Ответ 3

Альтернатива:

Я считаю, что использование CursoLoader слишком тяжело для этой задачи. Что нужно синхронизировать, это добавление/удаление базы данных, и это можно сделать с помощью синхронизированного метода. Как я уже сказал в более раннем комментарии, когда служба mDNS останавливается, удалите из нее db (синхронизированным образом), отправьте удаление трансляции в приемнике: удалите из списка держателей данных и уведомите. Этого должно быть достаточно. Чтобы избежать использования дополнительного arraylist (для поддержки адаптера), использование CursorLoader является дополнительной работой.


Вы должны выполнить некоторую синхронизацию в объекте ListFragment.

Вызов notifyDatasetChanged() должен быть синхронизирован.

synchronized(this) {  // this is ListFragment or ListView.
     notifyDatasetChanged();
}

Ответ 4

Я прочитал весь ваш поток, поскольку у меня была одна и та же проблема. Следующее утверждение - это то, что разрешило эту проблему для меня:

getLoaderManager().restartLoader(0, null, this);

Ответ 5

A имела ту же проблему. Я решил это:

@Override
public void onResume() {
    super.onResume();  // Always call the superclass method first
    if (some_condition) {
        getSupportLoaderManager().getLoader(LOADER_ID).onContentChanged();
    }
}