29

背景:

我有一个CursorLoader直接使用 SQLite 数据库而不是使用ContentProvider. 此加载程序与ListFragmentCursorAdapter. 到现在为止还挺好。

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

问题:

当删除命令快速连续发生时,意味着快速连续onContentChanged()调用,bindView()最终将使用 stale data。这意味着一行已被删除,但 ListView 仍在尝试显示该行。这会导致光标异常。

我究竟做错了什么?

代码:

这是一个自定义 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);
    }    
}

我班级的一个片段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();
}

MyCursorAdapter只是一个普通的,newView()被覆盖以返回新膨胀的行布局 XML 并bindView()使用Cursor将列绑定到View行布局中的 s。


编辑 1

在深入研究了这一点之后,我认为这里的根本问题是CursorAdapter处理底层Cursor. 我试图了解它是如何工作的。

采取以下场景以更好地理解。

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

问题:

  • 这种理解正确吗?我对上面的第 4 点特别感兴趣,我假设当一行被删除时,Cursor除非我要求,否则它不会被刷新。
  • 假设这是正确的,我如何要求我CursorAdapter丢弃/中止它ListView 正在进行的渲染,并要求它使用新的Cursor(通过Loader#onContentChanged()and返回Adapter#notifyDatasetChanged())来代替?

PS向版主提出的问题:是否应该将此编辑移至单独的问题?


编辑 2

根据各种答案的建议,我对 s 如何工作的理解似乎存在根本性错误Loader。事实证明:

  1. Fragmentor根本Adapter不应该直接操作。Loader
  2. 应该监视数据的Loader所有变化,并且只要数据变化就应该提供AdapterCursoronLoadFinished()

有了这种理解,我尝试了以下更改。- 没有任何操作Loader。refresh 方法现在什么都不做。

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

这是我的片段FragmentLoaderCallback

@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

只是为了澄清一些事情:

  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,“光标”不是“实时”对象,它会在数据库中修改数据时更新它指向的数据。

这是我的 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();
    }

}
4

5 回答 5

13

根据您提供的代码,我不能 100% 确定,但有几件事很突出:

  1. 突出的第一件事是您已将此方法包含在您的ListFragment:

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

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

  2. 你也不应该打电话mAdapter.notifyDataSetChanged()onLoadFinishedswapCursor会为你做这件事。

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


编辑#1:

据我所知,CursorLoader依赖于ForceLoadContentObserver(实现中提供的嵌套内部类Loader<D>)......所以这里的问题似乎是你正在实现你的 on custom ContentObserver,但没有设置任何东西来识别它。许多“自我通知”的东西是在Loader<D>andAsyncTaskLoader<D>实现中完成的,因此隐藏在执行实际工作的具体Loaders(例如CursorLoader)之外(即Loader<D>不知道CustomForceLoadContentObserver,那么它为什么要接收任何通知? )。

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

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

编辑#2:

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

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

(ps确保您android:export="false"在清单中设置了提供者标签,这样ContentProvider其他应用程序就看不到您了!:p)

于 2012-07-23T16:23:48.050 回答
3

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

有一种方法CursorLoader.setUpdateThrottle(long delayMS),它强制在 loadInBackground 完成和下一次加载被调度之间有一个最短时间。

于 2012-07-20T20:08:34.853 回答
2

选择:

我觉得使用 CursoLoader 对这项任务来说太重了。需要同步的是数据库的添加/删除,可以用同步的方式来完成。正如我在之前的评论中所说,当 mDNS 服务停止时,从它中删除 db(以同步方式),发送删除广播,在接收器中:从数据持有者列表中删除并通知。这应该足够了。只是为了避免使用额外的数组列表(用于支持适配器),使用 CursorLoader 是额外的工作。


您应该对ListFragment对象进行一些同步。

调用notifyDatasetChanged()应该是同步的。

synchronized(this) {  // this is ListFragment or ListView.
     notifyDatasetChanged();
}
于 2012-07-20T14:28:46.503 回答
2

当我遇到同样的问题时,我阅读了您的整个帖子,以下语句为我解决了这个问题:

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

于 2013-06-06T04:28:10.087 回答
0

A也有同样的问题。我通过以下方式解决了它:

@Override
public void onResume() {
    super.onResume();  // Always call the superclass method first
    if (some_condition) {
        getSupportLoaderManager().getLoader(LOADER_ID).onContentChanged();
    }
}
于 2013-06-13T13:09:36.937 回答