自定义CursorLoader和支持ListView的CursorAdapter之间的数据不同步

cur*_*zen 28 android asynctaskloader android-loadermanager android-cursorloader android-loader

背景:

我有一个CursorLoader直接使用SQLite数据库而不是使用的自定义ContentProvider.这个加载器使用a ListFragment支持CursorAdapter.到现在为止还挺好.

为了简化操作,我们假设UI上有一个Delete按钮.当用户单击此按钮时,我会从数据库中删除一行,并调用onContentChanged()我的加载程序.此外,在onLoadFinished()回调时,我调用notifyDatasetChanged()我的适配器以刷新UI.

问题:

当删除命令快速连续发生时,意味着快速连续onContentChanged()调用,bindView()最终将使用陈旧数据.这意味着行已被删除,但ListView仍在尝试显示该行.这导致了Cursor异常.

我究竟做错了什么?

码:

这是一个自定义的CursorLoader(基于Diane Hackborn女士的建议)

/**
 * 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);
    }    
}
Run Code Online (Sandbox Code Playgroud)

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();
}
Run Code Online (Sandbox Code Playgroud)

CursorAdapter只是一个常规的newView()过度循环返回新膨胀的行布局XML并bindView()使用在行布局中将Cursor列绑定到Views.


编辑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())?

PS问题主持人:此编辑应该转移到单独的问题吗?


编辑2

基于各种答案的建议,我的理解Loader工作方式似乎存在根本性的错误.事实证明:

  1. FragmentAdapter不该上的直接操作Loader的.
  2. Loader应监测数据的所有变化,应该只是给AdapterCursoronLoadFinished(),只要数据的变化.

有了这种理解,我尝试了以下改变.- 没有任何操作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();
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是我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();
}
Run Code Online (Sandbox Code Playgroud)

现在,只要DB中有变化(添加/删除行),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()称为?在我的Loader中,我注册了我的观察者Cursor:

void registerContentObserver(Cursor cursor, ContentObserver observer) {
    cursor.registerContentObserver(mObserver);
}
Run Code Online (Sandbox Code Playgroud)

这意味着当它发生变化时,有责任Cursor通知mObserver.但是,然后AFAIK,'Cursor'不是一个"实时"对象,它更新它所指向的数据以及在数据库中修改数据的时间.

这是我的Loader的最新版本:

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();
    }

}
Run Code Online (Sandbox Code Playgroud)

Ale*_*ood 13

根据您提供的代码,我不是100%确定,但有几件事情突然出现:

  1. 第一件事就是你已经将这种方法包括在你的ListFragment:

    public void refresh() {     
        mLoader.onContentChanged();
    }
    
    Run Code Online (Sandbox Code Playgroud)

    使用时LoaderManager,很少需要(通常很危险)Loader直接操纵你.在第一次调用之后initLoader,LoaderManager它完全控制Loader并将通过在后台调用其方法来"管理"它.Loader在这种情况下直接调用s方法时必须非常小心,因为它可能会干扰对象的底层管理Loader.我不能肯定地说你的电话onContentChanged()是不正确的,因为你没有在你的帖子中提到它,但在你的情况下它不应该是必要的(也不应该提到参考mLoader).您ListFragment不关心如何检测到更改...也不关心如何加载数据.它所知道的是,新数据在onLoadFinished可用时会神奇地提供.

  2. 你也应该不叫mAdapter.notifyDataSetChanged()onLoadFinished.swapCursor会为你做这件事.

在大多数情况下,Loader框架应该完成所有涉及加载数据和管理Cursors 的复杂事情.ListFragment相比之下,您的代码应该很简单.


编辑#1:

从我所知道的,CursorLoader依赖于ForceLoadContentObserver(Loader<D>实现中提供的嵌套内部类)...所以看起来这里的问题是你正在实现你的自定义ContentObserver,但没有设置任何东西来识别它.许多"自我通知"的东西都是在实现中完成的Loader<D>,AsyncTaskLoader<D>因此隐藏起来不具备实际工作的具体Loader(例如CursorLoader)(即Loader<D>不知道CustomForceLoadContentObserver,为什么它应该收到任何通知? ).

您在更新的帖子中提到您无法final ForceLoadContentObserver mObserver;直接访问,因为它是一个隐藏字段.您的修复是实现您自己的自定义ContentObserver并调用registerObserver()您的覆盖loadInBackground方法(这将导致registerContentObserver您的调用Cursor).这就是您没有收到通知的原因...因为您使用了框架ContentObserver永远不会识别的自定义Loader.

要解决此问题,您应该直接使用您的类(extend AsyncTaskLoader<Cursor>而不是CursorLoader将您继承的部分复制并粘贴CursorLoader到您的类中).这样你就不会遇到隐藏ForceLoadContentObserver字段的任何问题.

编辑#2:

根据Commonsware的说法,没有一种简单的方法可以设置来自a的全局通知SQLiteDatabase,这就是为什么每次进行交易时SQLiteCursorLoader他的Loaderex库都依赖于自身的Loader调用onContentChanged().直接从数据源广播通知的最简单方法是实现ContentProvider并使用aCursorLoader.这样,您就可以相信CursorLoader每次Service更新基础数据源时都会向您广播通知.

我不怀疑还有其他解决方案(即可能通过设置全局ContentObserver......或者甚至可能使用没有 a 的ContentResolver#notifyChange方法),但最简洁和最简单的解决方案似乎只是实现私有.ContentProviderContentProvider

(ps确保你android:export="false"在清单中的提供者标签中设置,以便ContentProvider其他应用程序无法看到你!:p)