How to make notifyChange() work between two activities?
Asked Answered
B

2

2

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.

Boothman answered 23/9, 2015 at 13:56 Comment(31)
You can use onActivityResult for your instance.Hosier
onActivityResult is a method you use in an activity. If you want to open an activity and change some stuff, then go back to last activity opened to apply these stuff, then you should use onActivityResult. Please search for it on google. Hope that helps.Hosier
@HusseinElFeky No it does not, although I do appreciate your enthusiasm.Boothman
Of course that should help. First you start the new activity with startActivityForResult. If you finish the new activity you add an ok return code. Then in OnActivityResult you can call getContentResolver().notifyChange(MyContentProvider.CONTENT_URI, null, false);. |Please explain what you did and what exactly did not work.Dislodge
@Boothman Please check this link for a simple example: developer.android.com/training/basics/intents/result.htmlHosier
did you more or less follow official Loaders Guide?Lorolla
@Greenaps The whole point og ContentProviders and notifyChange() is that you broadcast notification from provider itself, where the change happened and not to take care for it via activityresult or anything else. And I think I have explained in great detail what I have done and what does not work.Boothman
@Lorolla Yes I did. But I did not implement my own loaders. I just used CursorLoader.Boothman
When are you calling initLoader?Steerage
add Log.d in LoaderManager.LoaderCallbacks<Cursor> methods and in onCreate where you call initLoaderLorolla
@Steerage I call initLoader() in ActivityA.onCreate() as I should.Boothman
did you try for testing use "system" ContentProviders? that way toy could find out whether your ContentProvider or Activity code is a culpritLorolla
@Lorolla And how does one go abot testing Contentproviders? In example I have called getContentResolver().notifyChange(ContentProviderNakupnik.CONTENT_URI, null, false) in ActivityA.onResume() and this does refresh the listview. But it is a dirty hack and I do not like it.Boothman
if you dont want to do anything you will have to deal with that hack...Lorolla
@Lorolla Don't wan't to do anything? Would you care to explain that?Boothman
Are you sure that both Uris (in notifyChange() and setNotificationUri() )are the same? It should also work if the notifyChange-Uri is a descendent of the Uri used in setNotificationUri(), but not the other way around. To check just set both Uris to explicitly to the same value (i.e. MyContentProvider.CONTENT_URI).Conley
@Herrmann Yes I am. I use predefined constants for uris. And, if I follow the things in debugger I see that each MycontentProvider.insert() called from activity B causes MyQueryProvider.query() to be executed. It is just that that the changes are not seen in ActivityA when I return. I have to manualyy restart loader.Boothman
Who does the requery? The CursorLoader or something else? Please add your onCreateLoader implementation and more details of your ContentProvider. Another issue: if you use CursorAdapter.swapCursor(cursor); you have to close the old one!Conley
@Hermann I have edited the question to include onCreateLoader() implementation. It is simple. What does the requery? I don't know. But the only "observer" with setNotificationUri() is the cursor that was created for ActivityA cursor loader. Are you proposing that in onLoadFinished() I should do: cursorAdapter.swap(newCursor).close()? Why?Boothman
ok, here you have the minimal working code: codeshare.io/z9PCS, run it and watch the logcat, then try to compare it with your codeLorolla
@Lorolla Thank you very much for your effort. I have looked at your code very thoroughly and in relevant points I find it identical to mine. And I see no reason why it should behave any differently. I have no doubt that upon changing data in ActivityB (ThirdActivity in your code) it will notify registered cursors. Yet somehow this is not visible in listview when you return to ActivityA (SecondActivity).Boothman
so it ts not working? what android version did you run it on? i tested on 4.4 and now run it on 5.0 and it works like a charmLorolla
@Device is version 2.3.5 (API 10) with support library import android.support.v4. It might come down to that, or the fact you are using array based content provider. I am thinking about giving up on this and just use restartLoader() on every activity resume.Boothman
ok run in again on 2.2 + com.android.support:support-v4:23.0.1, still, works like a charm, you just need to use getSupportLoaderManager instead of getLoaderManager and remove the last param of SimpleCursorAdapter ctor and of course use FragmentActivity as a baseLorolla
and? does it work with latest support-v4 library?Lorolla
@Lorolla I really do appreciate the effort. Yes I do use support-v4 library, Since you said your version worked I have replicated it and found that it worls witj list based content provider, but not with sql lite based content provider. I have prepared the complete simplified project that demonstrates this. You can find it at: codeshare.io/1eLmT 4 java classes and manifest. It is almost identical to yours, only some names are changed and I have added ContentProviderSQL and DBHelper class that creates simple database.Boothman
@Lorolla Wait a bit, some typos in code. Will post corrected.Boothman
works with this provider as well: codeshare.io/z9PCS, it uses "normal" SQLiteCursor not MatrixCursorLorolla
@Lorolla Got it. It was all a bit complicated and a mix multiple factors. I will post an answer.Boothman
@Lorolla I was too fast. It is still not ok. And I do not know why the sequence of things is different bettween simplified demonstrator and my app. I suspect that in my app ActivityA.onStop() happens between notification and cursor load. And I also do not understan why does CP.query() happens, while ActivityB is still open.Boothman
@Lorolla If you are still interested take a look at the update to my answer. It holds the explanation what was wrong and the workaround / fix.Boothman
B
4

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.

Boothman answered 24/9, 2015 at 21:48 Comment(2)
its not a problem that onStop is called late, see: codeshare.io/z9PCS again, note in ThirdActivity.onCreate i am calling onClick to insert new data as soon as possible and it is inserted before calling Activity (SecontActivity) onStop is called, yet it works, you see query and onLoadFinished calledLorolla
@Lorolla Yes. In about 75% of the cases this happened in my app also. But there is nothing to guarantee that activity stop will wait long enough. As you said in your demonstrator you have enought time to execute query AND onLoadFinished(). If onStop() (or better said stopping ActivityA happens between those two points in time onLoadFinished() is never called. It all boils down to a tricky sequence.Boothman
S
2

I don't see anything here particularly wrong with this. As long as the Cursor is registered with the URI, the loader should be restarting itself with new information. I don't think the issue here is anything wrong with your code. I think it's the LoaderManager is unregistering the Cursor from the ContentResolver too early (it actually happens by the time onStop() is called).

Basically there's nothing you can really do about it unregistering. You can however, force restart the loader by calling LoaderManager#restartLoader(int, Bundle, LoaderCallbacks);. You can call this in onStart() (which makes the initLoader call in onCreate() useless). A more optimized approach would be to use onActivityResult(). The result of your activity is irrelevant in this case. All you're saying is that you've returned to this activity from some other activity and the data may or may not be different, so you need to reload.

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

Then just call Context#startActivityForResult() when opening new Activities.

Steerage answered 23/9, 2015 at 14:54 Comment(12)
I know I can force reload when I return to ActivityA. I have stated that I could force refresh in onResume(). And this does work, but it is just a dirty hack which I try to avoid But this goes against whole concept of setNotificationUri() and notifyChange(). If they do not work then why do they exist. There must be something else wrong.Boothman
It exists if you have background processes working downloading data while the user has your activity open. Of which it actually works extremely well. It doesn't work so well when the activity is stopped. Force reloading in onActivityResult will at least ensure you're only reloading once when it needs to be (as opposed to onResume() which is actually called many times in the lifecycle of an Activity).Steerage
Yes I see the bebefit of forcing reload only on ActivityResult. But I cannot help myself feeling aggravated about that. Every time data is changed through content provider it notifies observers. And as I have observed with debugger this triggers ContentProvider.Query. Yet all this trips to database are then completely pointless. Just a waste of CPU. I would really like to find a way to make this work formally correctly.Boothman
If they didn't unregister the dataset observer in onStop(), then you'd be reloading data (perhaps hundreds of times) when the user can't even see it to begin with. so it's saving CPU cycles. One other option is to register your own DatasetObserver in onCreate() and unregister it in `onDestroy(). Then have a simple boolean value that indicates "datachanged", then only restart the loader when that is true.Steerage
I don't understand that. In contrary I think that now, as it is implemented content provider could reload data hundreds of times, on every change, and this would be not propagated. Because I see in debugger that every update with notofyChanege() causes a content resolver query.Boothman
@Steerage LoaderManager cannot unregister the Cursor from the ContentResolver, it is done either in AbstractCursor#setNotificationUri or in Cursor finalization phaseLorolla
@Steerage Could you please elaborate some on what you think with this cursor unregistering. Namely in my app the outcome depends on when ActiviyA.onStop() executes. In simplifed demonstrator app, the ActivityA.onStop() always executes data update and notification and therefore it works ok. What can I do about it?Boothman
@Boothman The LoaderManager stops any loaders that it contains during onStop() of an activity and a CursorLoader closes any cursors it contains when they're stopped (which unregisters from the uri). But I was thinking about it, a CursorLoader should be restarted with a new query, but for some reason it's returning the previous cursor. Either that, or it's doing a requerying with the same data which shouldn't be happening.Steerage
@Boothman In other words, I'm as stumped as you. From my experience with loaders, if you get old data, then that means the loader wasn't restarted. If the loader wasn't restarted, then that means it wasn't notified to be restarted. If it wasn't notified, than that means it's not registered to the right URI, or it's not registered to the URI anymore.Steerage
I think this is what happens when things go wrong. I start the ActivityB. In ActivityB sontent provider midifes data and via notifyChange() notifies observer. I think that in response to that cursorLoader from ActivityA starts a query in content provider. I can see that it does. Somewhere around that point ActivityA executes onStop(). CursorLoader than cancels any cursors being loaded and so the ActivityA.onLoadFinished() never executes. If I take a look at simplified demonstrator things go a bit different. AcivityA.onStop() ->ActivityB modifies data ->content provider executes notifyChange()Boothman
And then when I close ActivityB and return to ActivityA a content provider query is executed and cursor loaded. So the problem is no so much in my code. It is just that onStop() executes to late.Boothman
@Steerage Take a look at the update to my answer. Your sentence " I think it's the LoaderManager is unregistering the Cursor from the ContentResolver too early (it actually happens by the time onStop() is called)" was a nudge in right direction.Boothman

© 2022 - 2024 — McMap. All rights reserved.