【问题标题】:How to make notifyChange() work between two activities?如何使 notifyChange() 在两个活动之间工作?
【发布时间】:2015-09-23 13:56:45
【问题描述】:

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

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

然后刷新列表视图。但这会强制刷新每个简历,无论数据是否更改。

【问题讨论】:

  • 您可以将 onActivityResult 用于您的实例。
  • onActivityResult 是您在活动中使用的方法。如果你想打开一个活动并改变一些东西,然后回到上一个打开的活动来应用这些东西,那么你应该使用 onActivityResult。请在谷歌上搜索。希望对您有所帮助。
  • @HusseinElFeky 不,它没有,虽然我很欣赏你的热情。
  • 当然应该有帮助。首先,您使用 startActivityForResult 启动新活动。如果您完成新活动,则添加 ok 返回码。然后在 OnActivityResult 你可以调用 getContentResolver().notifyChange(MyContentProvider.CONTENT_URI, null, false);。 |请解释一下你做了什么,到底什么没用。
  • @f470071 请查看此链接以获取简单示例:developer.android.com/training/basics/intents/result.html

标签: android android-contentprovider


【解决方案1】:

经过两天的挠头和 pskink 的无私参与后,我给自己描绘了一幅错误的图景。 我的 ActivityA 实际上要复杂得多。它使用 ViewPager 和 PagerAdapter 并实例化列表视图。 起初我在 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() 在 onCreate() 完成后被调用。我不知道为什么或如何导致最初的问题。也许有些东西没有与列表视图、适配器和内容观察器正确连接。我没有深入研究。

我把顺序改为:

@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() 并刷新列表视图。在我的应用程序中并非如此。它总是在 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中的loader在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 */

这是我能找到的最优雅的解决方案。无需重新启动加载程序等。 但我仍然想听听有更深入见解的人对此主题的评论。

【讨论】:

  • onStop被调用晚不是问题,见:codeshare.io/z9PCS再次,注意ThirdActivity.onCreate我正在调用onClick尽快插入新数据并插入在调用 Activity (SecontActivity) onStop 之前调用,但它可以工作,你会看到 queryonLoadFinished 调用
  • @pskink 是的。在大约 75% 的情况下,这也发生在我的应用程序中。但是没有什么可以保证活动停止会等待足够长的时间。正如您在演示器中所说,您有足够的时间来执行查询和 onLoadFinished()。如果 onStop()(或者更好的说法是停止 ActivityA 发生在这两个时间点之间,则永远不会调用 onLoadFinished()。这一切都归结为一个棘手的序列。
【解决方案2】:

我看不出这有什么特别不对劲的地方。只要使用 URI 注册了 Cursor,加载程序就应该使用新信息重新启动自身。我认为这里的问题与您的代码没有任何问题。我认为这是 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()

【讨论】:

  • 我知道我可以在返回 ActivityA 时强制重新加载。我已经说过我可以在 onResume() 中强制刷新。这确实有效,但这只是我试图避免的一种肮脏的黑客行为,但这违背了 setNotificationUri() 和 notifyChange() 的整个概念。如果它们不起作用,那么它们为什么存在。肯定有其他问题。
  • 如果您有后台进程正在下载数据而用户打开您的活动,则存在。其中它实际上工作得非常好。活动停止时效果不佳。在 onActivityResult 中强制重新加载至少可以确保您只在需要时重新加载一次(而不是 onResume(),它实际上在 Activity 的生命周期中被多次调用)。
  • 是的,我看到仅在 ActivityResult 上强制重新加载的好处。但我无法控制自己对此感到恼火。每次通过内容提供者更改数据时,它都会通知观察者。正如我在调试器中观察到的那样,这会触发 ContentProvider.Query。然而,所有这些对数据库的访问都是完全没有意义的。只是浪费CPU。我真的很想找到一种方法使这项工作正式正确。
  • 如果他们没有在 onStop() 中取消注册数据集观察者,那么当用户甚至看不到它开始时,您将重新加载数据(可能数百次)。所以它可以节省 CPU 周期。另一种选择是在 onCreate() 中注册您自己的 DatasetObserver 并在 `onDestroy() 中取消注册。然后有一个简单的布尔值表示“datachanged”,然后只有当它为真时才重新启动加载器。
  • 我不明白。相反,我认为现在,内容提供商可以在每次更改时重新加载数据数百次,而这不会传播。因为我在调试器中看到每次使用 notofyChanege() 更新都会导致内容解析器查询。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多