Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
531 views
in Technique[技术] by (71.8m points)

android - How to make notifyChange() work between two activities?

I have an activity ActitvityA that holds a listview populated by a CursorLoader. I want to switch to ActivityB and change some data and see those changes reflected in listview in 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.");
        }
    }
        .
        .
        .
}

From ActivityA I switch to ActivityB where I change the underlying data.

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

The details of 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;
    }
}

As far as my knowledge goes this should be sufficient. But it does not work. Upon returning to ActivityA the listview is unchanged.

I have followed things with debugger and this is what happens.

First visit ActivityA, the methods that are called in that order

MyContentProvider.query()    
ActivityA.onLoadFinished()

The listview displays the correct values. And now I switch to activityB and change the data

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 ?

Return to ActivityA

!!! ActivityA.onLoadFinished() is not called    

I have read anything I could about this, took a close look at a lot of stackoverflow questions yet all those question/answers revolve around setNotificationUri() and notifyChangeCombo() which I implemented. Why does this not work across activities?

If for example force refresh in ActivityA.onResume() with

getContentResolver().notifyChange(MyContentProvider.CONTENT_URI, null, false);

then it refreshes the list view. But that would force refresh on every resume regardless if data was changed or not.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

After a two long two days of scratching my head and altruistic engagement from pskink I painted myself a picture of what was wrong. My ActivityA is in a reality a lot more complicated. It uses ViewPager with PagerAdapter with instantiates listviews. At first I created those components in onCreate() method something like this:

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

Somewhere along the line I noticed that this is wrong order of creating. Why it didn't produce some error is because PagerAdapter.instantiateItem() is called aftter onCreate() finishes. I dont know why or how this caused the original problem. Maybe something did not wire correctly with listviews, adapters and content observers. I didn't dig into that.

I changed the order to:

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

This magically made it work in about 75% of the cases. When I studied CatLog output I noticed that ActivityA().onStop() is called at different times. When it works it is called late and I can see in logcat that onLoadFinished() executes. Sometimes ActivityA.onStop() executes right after query and then onLoadFinished() is not called at all. This brings me to what DeeV jas posted in his answer about cursors being unregistered from ContentResolver. This just might be the case. What made things to somehow came to light was the fact that simple demonstrator pskink insisted on did work and my app didn't although they were identical in key points. This brought my attention to asynchronous things and my onCreate() method. In reality my ActivityB is complicated so it gives enough time for ActivityA to stop. What I noticed also (and this did make things more difficult to sort) was that if I run my 75% version in debug mode (with no breakpoints) then the success rate falls to 0. ActivityA is stopped before cursor load finishes so my onLoadFinished() is never called and listviews are never updated.

Two key points:

  • Somehow the order of creation od ViewPager, CursorAdapter and CursorLoader is important
  • ActivityA may be (and is) stopped before cursor is loaded.

But even this is not. If I take a look at a sequence of simplified then I see that ActivityA.onStop() is executed before content provider inserts a record. I see no query while ActivityB is active. But when i return to ActivityA a query is execeuted laodFinished() follows and listview is refreshed. Not so in my app. It always executes a query while still in ActivityB, why??? This destroy my theory about onStop() being the culprit.

(Big thanks to pskink and DeeV)

UPDATE

After a lot of waisted time on this issue I finally nailed the cause of the problem.

Short description:

I have the following classes:

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.

The problem:

After data manipulation in ActivityB the changes are not shown in list view in ActivityA. List view is not refreshed.

After I lot of eyeballing and studying logcat traces I have seen that things proceed in following sequence:

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 */

So the problem is not that ActivityA is stopped to soon but that it is stopped to late. The data is updated and notification sent somewhere between creation of ActivityB and stopping of ActivityA. The solution is to force loader in ActivityA to stop loading just before ActivityB is started.

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

This stops the loader and (I guess again) prevents cursor to consume notification while activity is in the above described limbo state. The sequence of events now is:

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 */

This was the most elegant solution I could find. No need to restart loaders and such. But still I would like to hear a comment on that subject from someone with a deeper insight.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...