如何使notifyChange()在两个活动之间工作?

时间:2015-09-23 13:56:45

标签: android android-contentprovider

我有一个活动ActitvityA,它包含一个由CursorLoader填充的列表视图。我想切换到ActivityB并更改一些数据,并查看ActivityA中listview中反映的那些更改。

public class ActivityA implements LoaderManager.LoaderCallbacks<Cursor>
{ 
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_a);
        getSupportLoaderManager().initLoader(LOADER_ID, null, this);
        mCursorAdapter = new MyCursorAdapter(   
            this,
            R.layout.my_list_item,
            null,
            0 );
    }
        .
        .
        .

    /** Implementation of LoaderManager.LoaderCallbacks<Cursor> methods */
    @Override
    public Loader<Cursor> onCreateLoader(int loaderId, Bundle arg1) {
        CursorLoader result;
        switch ( loaderId ) {           
        case LOADER_ID:
            /* Rename v _id is required for adapter to work */
            /* Use of builtin ROWID http://www.sqlite.org/autoinc.html */
            String[] projection = {
                    DBHelper.COLUMN_ID + " AS _id",     //http://www.sqlite.org/autoinc.html
                    DBHelper.COLUMN_NAME    // columns in select
            }
            result = new CursorLoader(  ActivityA.this,
                                        MyContentProvider.CONTENT_URI,
                                        projection,
                                        null,
                                        new String[] {},
                                        DBHelper.COLUMN_NAME + " ASC");
            break;
        default: throw new IllegalArgumentException("Loader id has an unexpectd value.");
    }
    return result;
}


    /** Implementation of LoaderManager.LoaderCallbacks<Cursor> methods */
    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        switch (loader.getId()) {
            case LOADER_ID:
                mCursorAdapter.swapCursor(cursor);
                break;
            default: throw new IllegalArgumentException("Loader has an unexpected id.");
        }
    }
        .
        .
        .
}

从ActivityA我切换到ActivityB,我在那里更改基础数据。

// insert record into table TABLE_NAME
ContentValues values = new ContentValues();
values.put(DBHelper.COLUMN_NAME, someValue);
context.getContentResolver().insert( MyContentProvider.CONTENT_URI, values);

MyContentProvider的详细信息:

public class MyContentProvider extends ContentProvider {
    .
    .
    .

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        int uriCode = sURIMatcher.match(uri);
        SQLiteDatabase database = DBHelper.getInstance().getWritableDatabase();
        long id = 0;
        switch (uriType) {
        case URI_CODE:
            id = database.insertWithOnConflict(DBHelper.TABLE_FAVORITE, null, values,SQLiteDatabase.CONFLICT_REPLACE);
            break;
        default:
            throw new IllegalArgumentException("Unknown URI: " + uri);
        }
        getContext().getContentResolver().notifyChange(uri, null);  // I call the notifyChange with correct uri
        return ContentUris.withAppendedId(uri, id);
    }


    @Override
    public Cursor query(Uri uri,
                        String[] projection,
                        String selection,
                        String[] selectionArgs,
                        String sortOrder) {

        // Using SQLiteQueryBuilder instead of query() method
        SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();

        int uriCode = sURIMatcher.match(uri);
        switch (uriCode) {
        case URI_CODE:
            // Set the table
            queryBuilder.setTables(DBHelper.TABLE_NAME);
            break;
        default:
            throw new IllegalArgumentException("Unknown URI: " + uri);
        }
        SQLiteDatabase database = DBHelper.getInstance().getWritableDatabase();
        Cursor cursor = queryBuilder.query( database, projection, selection, selectionArgs, null, null, sortOrder);
        // Make sure that potential listeners are getting notified
        cursor.setNotificationUri(getContext().getContentResolver(), uri);
        return cursor;
    }
}

据我所知,这应该足够了。但它不起作用。 返回ActivityA后,列表视图未更改

我已经使用调试器进行了操作,这就是发生的事情。

首先访问ActivityA,按顺序调用的方法

MyContentProvider.query()    
ActivityA.onLoadFinished()

列表视图显示正确的值。 现在我切换到activityB并更改数据

MyContentProvider.insert()  // this one calls getContext().getContentResolver().notifyChange(uri, null);
MyContentProvider.query()
//As we can see the MyContentProvider.query is executed. I guess in response to notifyChange().
// What I found puzzling why now, when ActivityB is still active ?

返回ActivityA

!!! ActivityA.onLoadFinished() is not called    

我已经阅读了关于此的任何内容,仔细查看了很多stackoverflow问题,但所有这些问题/答案都围绕着我实现的setNotificationUri()和notifyChangeCombo()。为什么这不适用于各种活动?

例如,如果使用

在ActivityA.onResume()中强制刷新
getContentResolver().notifyChange(MyContentProvider.CONTENT_URI, null, false);

然后刷新列表视图。但是,无论数据是否发生变化,这都会强制刷新每个简历。

2 个答案:

答案 0 :(得分:4)

经过两天长时间的搔痒和pskink的无私奉献之后,我给自己描绘了一个错误的画面。 我的ActivityA实际上要复杂得多。它使用带有PagerAdapter的ViewPager和实例化列表视图。 起初我在onCreate()方法中创建了这样的组件:

@Override
public void onCreate(Bundle savedInstanceState)
{
        ...
    super.onCreate(savedInstanceState);
    // 1 .ViewPager
    viewPager = (ViewPager) findViewById(R.id.viewPager);
    ...
    viewPager.setAdapter( new MyPagerAdapter() );
    viewPager.setOnPageChangeListener(this); */
    ...
    // 2. Loader
    getSupportLoaderManager().initLoader(LOADER_ID, null, this);
    ...
    // 3. CursorAdapter
    myCursorAdapter = new MyCursorAdapter(
                    this,
                    R.layout.list_item_favorites_history,
                    null,
      0);
}

在某条线上,我注意到这是错误的创建顺序。为什么它没有产生一些错误是因为PagerAdapter.instantiateItem()被称为aftter onCreate()完成。我不知道为什么或如何导致原始问题。也许某些东西没有与listviews,适配器和内容观察者正确连接。我没有深入研究。

我将订单更改为:

@Override
public void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    ...
    // 1. CursorAdapter
    myCursorAdapter = new MyCursorAdapter(
                    this,
                    R.layout.list_item_favorites_history,
                    null,
                    0);
    ...
    // 2. Loader
    getSupportLoaderManager().initLoader(LOADER_ID, null, this);
    ...
    // 3 .ViewPager
    viewPager = (ViewPager) findViewById(R.id.viewPager);
    ...
    viewPager.setAdapter( new MyPagerAdapter() );
    viewPager.setOnPageChangeListener(this); */
    ...        
}

这使得它在大约75%的案例中起作用。当我研究CatLog输出时,我注意到ActivityA()。onStop()在不同的时间被调用。当它工作时,它被称为迟到,我可以在logcat中看到onLoadFinished()执行。有时ActivityA.onStop()在查询后立即执行,然后根本不调用onLoadFinished()。这让我想起了DeeV jas在他的回答中发布的有关从ContentResolver取消注册的游标的内容。这可能就是这种情况。 是什么让事情以某种方式被曝光的事实是,简单的示威者pskink坚持做了工作,我的应用程序没有,虽然他们在关键点是相同的。这引起了我对异步事物和onCreate()方法的注意。实际上我的ActivityB很复杂,因此它为ActivityA提供了足够的时间来停止。 我注意到的(这确实让事情变得更难排序)是如果我在调试模式下运行我的75%版本(没有断点),那么成功率就会降到0. ActivityA在游标加载完成之前就停止了所以我的onLoadFinished永远不会调用(),也不会更新列表视图。

两个关键点:

  • 以某种方式创建了ViewPager,CursorAdapter和 CursorLoader很重要
  • ActivityA可能会(之前)停止 光标已加载。

但即使这不是。如果我看一下简化序列,那么我看到ActivityA.onStop()在内容提供者插入记录之前执行。当ActivityB处于活动状态时,我看不到任何查询。但是当我返回到ActivityA时,将执行一个查询,然后执行laodFinished()并刷新listview。我的应用程序不是这样。它仍然在ActivityB中执行查询,为什么???这破坏了我关于onStop()成为罪魁祸首的理论。

(非常感谢pskink和DeeV)

<强>更新

在这个问题上花了很多时间后,我终于找到了问题的原因。

简短说明:

我有以下课程:

ActivityA - contains a list view populated via cursor loader.
ActivityB - that changes data in database
ContentProvider - content provider used for data manipulation and also used by cursorloader.

问题:

在ActivityB中进行数据操作后,ActivityA中的列表视图中不会显示更改。列表视图未刷新。

经过大量观察和研究logcat痕迹后,我发现事情按以下顺序进行:

ActivityA is started

    ActivityA.onCreate()
        -> getSupportLoaderManager().initLoader(LOADER_ID, null, this);

    ContentProvider.query(uri)  // query is executes as it should

    ActivityA.onLoadFinished()  // in this event handler we change cursor in list view adapter and listview is populated


ActivityA starts ActivityB

    ActivityA.startActivity(intent)

    ActivityB.onCreate()
        -> ContentProvider.insert(uri)      // data is changed in the onCreate() method. Retrieved over internet and written into DB.
            -> getContext().getContentResolver().notifyChange(uri, null);   // notify observers

    ContentProvider.query(uri)
    /*  We can see that a query in content provider is executed.
        This is WRONG in my case. The only cursor for this uri is cursor in cursor loader of ActivityA.
        But ActivityA is not visible any more, so there is no need for it's observer to observe. */

    ActivityA.onStop()
    /*  !!! Only now is this event executed. That means that ActivityA was stopped only now.
        This also means (I guess) that all the loader/loading of ActivityA in progress were stopped.
        We can also see that ActivityA.onLoadFinished() was not called, so the listview was never updated.
        Note that ActivityA was not destroyed. What is causing Activity to be stopped so late I do not know.*/


ActivityB finishes and we return to ActivityA

    ActivityA.onResume()

    /*  No ContentProvider.query() is executed because we have cursor has already consumed
        notification while ActivityB was visible and ActivityA was not yet stopped.
        Because there is no query() there is no onLoadFinished() execution and no data is updated in listview */

所以问题不在于ActivityA很快停止了,而是停止了很晚。数据已更新并通知 在创建ActivityB和停止ActivityA之间发送。 解决方案是强制ActivityA中的加载器在ActivityB启动之前停止加载。

ActivityA.getSupportLoaderManager().getLoader(LOADER_ID).stopLoading(); // <- THIS IS THE KEY
ActivityA.startActivity(intent)

这会停止加载程序并且(我再次猜测)阻止游标在活动处于上述状态时消耗通知。 现在的事件顺序是:

ActivityA is started

    ActivityA.onCreate()
        -> getSupportLoaderManager().initLoader(LOADER_ID, null, this);

    ContentProvider.query(uri)  // query is executes as it should

    ActivityA.onLoadFinished()  // in this event handler we change cursor in list view adapter and listview is populated


ActivityA starts ActivityB

    ActivityA.getSupportLoaderManager().getLoader(LOADER_ID).stopLoading();
    ActivityA.startActivity(intent)

    ActivityB.onCreate()
    -> ContentProvider.insert(uri)
        -> getContext().getContentResolver().notifyChange(uri, null);   // notify observers

    /*  No ContentProvider.query(uri) is executed, because we have stopped the loader in ActivityA. */

    ActivityA.onStop()
    /*  This event is still executed late. But we have stopped the loader so it didn't consume notification. */


ActivityB finishes and we return to ActivityA

    ActivityA.onResume()

    ContentProvider.query(uri)  // query is executes as it should

    ActivityA.onLoadFinished()  // in this event handler we change cursor in list view adapter and listview is populated

/* The listview is now populated with up to date data */

这是我能找到的最优雅的解决方案。无需重启加载器等。 但我仍然希望听到有更深入见解的人对该主题的评论。

答案 1 :(得分:2)

我在这里看不到任何特别错的东西。只要Cursor注册了URI,加载器就应该重新启动自己的新信息。我不认为这里的问题是您的代码有问题。我认为LoaderManager过早地从ContentResolver取消注册Cursor(实际上是在onStop()被调用的时候发生的。)

基本上没有任何关于注销的事情。但是,您可以通过调用LoaderManager#restartLoader(int, Bundle, LoaderCallbacks);强制重新启动加载程序。你可以在onStart()中调用它(这会使onCreate()中的initLoader调用无效)。更优化的方法是使用onActivityResult()。在这种情况下,您的活动结果无关紧要。您所说的只是您已从其他某些活动返回此活动,且数据可能会有所不同,因此您需要重新加载。

protected void onActivityResult(int requestCode, int resultCode,
             Intent data) {
   getSupportLoaderManager().restartLoader(LOADER_ID, null, this);
}

然后在打开新活动时调用Context#startActivityForResult()