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

7 浏览
0 Comments

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

背景:

我有一个自定义的CursorLoader,它直接与SQLite数据库进行交互,而不是使用ContentProvider。这个Loader与一个由CursorAdapter支持的ListFragment一起工作。到目前为止一切都很好。

为了简化事情,让我们假设UI上有一个删除按钮。当用户单击它时,我会从数据库中删除一行,并在我的Loader上调用onContentChanged()。此外,在onLoadFinished()回调中,我调用我的适配器上的notifyDatasetChanged()以刷新UI。

问题:

当删除操作迅速连续发生时,意味着onContentChanged()被迅速调用,bindView()会使用过时的数据。这意味着一行已被删除,但ListView仍在尝试显示该行,这会导致游标异常。

我做错了什么?

代码:

这是一个自定义的CursorLoader(基于Ms. 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);
    }    
}

以下是我的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()使用游标将列绑定到行布局中的View

编辑1

经过一番研究,我认为这里的根本问题在于CursorAdapter如何处理基础的Cursor。我正在尝试理解它的工作方式。

为了更好地理解,考虑以下情形。

  1. 假设CursorLoader已经完成加载并返回一个现在有5行的Cursor
  2. 适配器开始显示这些行。它将Cursor移到下一个位置并调用getView()
  3. 此时,即使列表视图正在呈现过程中,一个行(比如_id = 2)已经从数据库中删除了。
  4. 问题就在这里 - CursorAdapter已将Cursor移动到对应于已删除行的位置。bindView()方法仍然尝试使用这个Cursor访问此行的列,但是这个Cursor是无效的,我们会得到异常。

问题:

  • 这种理解是正确的吗?我特别关心上述第4点,在此我假定当一行被删除时,Cursor不会刷新,除非我要求刷新它。
  • 假设这是正确的,那么我该如何要求我的CursorAdapter放弃/中止正在进行的ListView呈现,并要求它使用新鲜的Cursor(通过Loader#onContentChanged()Adapter#notifyDatasetChanged()返回)呢?

P.S. 问题提交给管理人员:这个编辑应该移到单独的问题中吗?


编辑2

根据各种答案的建议,我的Loader的工作原理有一个基本错误。以下是结果:

  1. 片段或适配器根本不应直接操作Loader。
  2. Loader应该监视所有数据的更改,并且每当数据更改时,均应在onLoadFinished()中为Adapter提供新的Cursor。

凭借这个理解,我尝试了以下更改。

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

现在,每当DB发生更改(添加/删除行)时,ContentObserver的onChange()方法应该被调用-正确吗?我没有看到这种情况发生。我的ListView从未显示任何更改。唯一见到任何更改的时间是我在Loader上显式调用onContentChanged()的时候。

这里出了什么问题?


编辑3

好的,所以我重新编写了我的Loader直接从AsyncTaskLoader扩展。我仍然看不到我的DB更改被刷新,也没有看到我的Loader的onContentChanged()方法在我插入/删除DB中的行时被调用:-(

仅为澄清一些事情:

  1. 我使用了CursorLoader代码,只修改了一个返回Cursor的代码行。 在这里,我用我的DbManager代码替换了对ContentProvider的调用(而我的DbManager代码则使用DatabaseHelper来执行查询并返回Cursor)。

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

  2. 我的数据库插入/更新/删除发生在其他地方,而不是通过Loader。 在大多数情况下,DB操作在后台Service中进行,在少数情况下,在Activity中进行。 我直接使用我的DbManager类执行这些操作。

我仍然不明白的是 - 是谁告诉我的Loader添加/删除/修改了一行数据呢?换句话说,在哪里调用ForceLoadContentObserver#onChange()呢? 在我的Loader中,我在Cursor上注册了观察者:

void registerContentObserver(Cursor cursor, ContentObserver observer) {
    cursor.registerContentObserver(mObserver);
}

这意味着责任在Cursor上向mObserver发送通知。 但是,据我所知,\'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 {
    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();
    }
}

admin 更改状态以发布 2023年5月21日
0
0 Comments

这并不是解决您问题的方法,但它可能仍然对您有用:

有一个方法CursorLoader.setUpdateThrottle(long delayMS),它强制要求在loadInBackground完成和下一次加载计划之间间隔一定的最小时间。

0
0 Comments

基于您提供的代码,我不能完全确定,但有几个问题需要注意:

  1. 首先,引起注意的是您在ListFragment中包含了这个方法:

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

    使用LoaderManager时,直接操作Loader是很少必要(通常是危险的)。在第一次调用initLoader后,LoaderManager完全控制Loader,并通过后台调用其方法"管理"它。在这种情况下,直接调用Loader的方法时必须非常小心,因为它可能会干扰Loader的基本管理。我不能确定您对onContentChanged()的调用是否不正确,因为您在帖子中没有提及它,但在您的情况下不需要它(也不需要持有mLoader的引用)。您的ListFragment不关心如何检测更改...也不关心数据如何加载。它只知道在onLoadFinished中自动提供新的数据。

  2. onLoadFinished中,您也不应调用mAdapter.notifyDataSetChanged()swapCursor将为您执行此操作。

在大多数情况下,Loader框架应该处理所有涉及加载数据和管理Cursor的复杂事情。相对而言,您的ListFragment代码应该很简单。


##编辑#1:

据我所知,CursorLoader依赖ForceLoadContentObserver(在Loader实现中提供的嵌套的内部类)...所以问题似乎在于您正在实现自定义的ContentObserver,但是没有设置能够识别它的内容。在LoaderAsyncTaskLoader实现中完成了许多“自我通知”的工作,并且因此从具体的Loader(例如CursorLoader)中隐藏了这些工作 (即Loader对于CustomForceLoadContentObserver一无所知,那么它为什么会接收到任何通知呢?)。

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

为了解决此问题,你应该直接将你的类extend AsyncTaskLoader而不是CursorLoader(即只需复制并粘贴你从CursorLoader继承的部分)。这样,你就不会遇到任何与隐藏的ForceLoadContentObserver字段相关的问题。

编辑#2:

根据Commonsware的说法,设置来自SQLiteDatabase的全局通知并不容易,这就是为什么他的Loaderex库中的SQLiteCursorLoader依赖于Loader在每次交易时调用onContentChanged()。从数据源直接广播通知的最简单方法是实现一个ContentProvider并使用CursorLoader。这样,每次你的Service更新底层数据源时,都可以确信通知将被广播到你的CursorLoader

我不怀疑还有其他解决方案(例如通过设置全局ContentObserver……或甚至使用没有ContentProviderContentResolver#notifyChange方法),但最干净和最简单的解决方案似乎是只实现一个私有ContentProvider

(请注意,在提供程序标记中设置android:export="false",以便你的ContentProvider不能被其他应用程序看到!:p)

0