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

时间:2012-07-18 12:00:48

标签: android android-loadermanager android-cursorloader asynctaskloader android-loader

背景:

我有一个自定义CursorLoader,它直接与SQLite数据库一起使用,而不是使用ContentProvider。此加载程序与ListFragment支持的CursorAdapter一起使用。到目前为止一切都很好。

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

问题:

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

我做错了什么?

代码:

这是一个自定义的CursorLoader(基于Diane Hackborn女士的this advice

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

我的CursorAdapter只是一个常规的newView()被覆盖,以使用bindView()Cursor列绑定到{View来覆盖新增的行布局XML和CursorAdapter 1}}在行布局中。


编辑1

在深入研究之后,我认为这里的根本问题是Cursor处理基础CursorLoader的方式。我试图理解它是如何工作的。

采取以下方案以便更好地理解。

  1. 假设Cursor已完成加载,并返回Adapter,现在有5行。
  2. Cursor开始显示这些行。它会将getView()移至下一个位置并调用CursorAdapter
  3. 此时,即使列表视图处于呈现过程中,也会从数据库中删除一行(例如,带_id = 2)。
  4. 问题出在哪里 - Cursor已将bindView()移至与已删除行对应的位置。 Cursor方法仍然尝试使用此Cursor来访问此行的列,这是无效的,我们会获得例外。
  5. 问题:

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

    P.S。主持人的问题:此编辑应该转移到单独的问题吗?


    编辑2

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

    1. AdapterLoader不应直接在Loader上运作。
    2. Adapter应监控数据中的所有更改,并且只要数据发生更改,就应在Cursor中为onLoadFinished()提供新的Loader
    3. 有了这种理解,我尝试了以下改变。   - Loader无法执行任何操作。刷新方法现在什么都不做。

      另外,为了调试ContentObserverpublic 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()

      现在,只要DB中有更改(添加/删除行),就应该调用ContentObserver的{​​{1}}方法 - 正确吗?我没有看到这种情况发生。我的ListView从未显示任何变化。我发现任何更改的唯一情况是,如果我在onContentChanged()明确调用Loader

      这里出了什么问题?


      编辑3

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

      只是为了澄清一些事情:

      1. 我使用Loader的代码,只修改了一行返回CursorLoader的代码。在此,我使用Cursor代码替换了ContentProvider的调用(后者又使用DbManager执行查询并返回DatabaseHelper)。

        Cursor

      2. 我在数据库上的插入/更新/删除是从其他地方发生的,而不是通过Cursor cursor = AppGlobals.INSTANCE.getDbManager().getAllCameras();发生的。在大多数情况下,数据库操作发生在后台Loader中,在某些情况下,来自Service。我直接使用我的Activity类来执行这些操作。

      3. 我仍然没有得到的是 - 谁告诉我的DbManager已添加/删除/修改了一行?换句话说,调用Loader的位置?在我的Loader中,我在ForceLoadContentObserver#onChange()

        上注册我的观察者
        Cursor

        这意味着void registerContentObserver(Cursor cursor, ContentObserver observer) { cursor.registerContentObserver(mObserver); } 有责任在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();
            }
        
        }
        

5 个答案:

答案 0 :(得分:13)

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

  1. 首先要说明的是,您已将此方法纳入ListFragment

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

    使用LoaderManager时,很少需要(通常很危险)直接操纵Loader。第一次调用initLoader后,LoaderManager可以完全控制Loader,并通过在后台调用其方法来“管理”它。在这种情况下,在直接调用Loader方法时必须非常小心,因为它可能会干扰Loader的基础管理。我无法肯定地说您对onContentChanged()的来电不正确,因为您没有在帖子中提及,但在您的情况下它不应该是必要的(也不应该提及{{1} }})。您的mLoader并不关心如何检测到更改...也不关心数据的加载方式。它只知道在ListFragment可用时会神奇地提供新数据。

  2. 您也不应在onLoadFinished中致电mAdapter.notifyDataSetChanged()swapCursor会为您执行此操作。

  3. 在大多数情况下,onLoadFinished框架应该执行所有涉及加载数据和管理Loader的复杂事情。您的Cursor代码应该比较简单。


    编辑#1:

    据我所知,ListFragment依赖于CursorLoaderForceLoadContentObserver实现中提供的嵌套内部类)...所以看起来似乎是这里的问题是您正在实现自定义Loader<D>,但没有设置任何内容来识别它。许多“自我通知”内容都是在ContentObserverLoader<D>实现中完成的,因此隐藏在具体的AsyncTaskLoader<D>(例如Loader)之外做实际的工作(即CursorLoader不知道Loader<D>,为什么它会收到任何通知?)。

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

    要解决此问题,您应该直接让您的班级Loader代替extend AsyncTaskLoader<Cursor>(即只复制并粘贴您从{{1}继承的部分进入你的班级)。这样,您就不会遇到隐藏CursorLoader字段的任何问题。

    编辑#2:

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

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

    (请确保您在清单中的提供者代码中设置ContentProvider,以便其他应用无法看到您的ContentProvider!:p)

答案 1 :(得分:3)

对于您的问题,这不是真正的解决方案,但它可能对您有用:

有一个方法CursorLoader.setUpdateThrottle(long delayMS),它强制执行loadInBackground完成之间的最短时间,以及正在安排的下一个加载。

答案 2 :(得分:2)

替代:

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


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

应同步对notifyDatasetChanged()的调用。

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

答案 3 :(得分:1)

我阅读了整个帖子,因为我遇到了同样的问题,以下陈述是为我解决了这个问题:

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

答案 4 :(得分:0)

A有同样的问题。我解决了它:

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