How to make notifyChange () work between two actions?

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

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 I know, this should be enough. But that does not work. Upon returning to ActivityA, the list does not change .

I tracked things with a debugger, and this happens.

First run ActivityA, methods that call in this order

 MyContentProvider.query() ActivityA.onLoadFinished() 

The list 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 read everything I could and carefully considered many questions about the stack, but all these questions / answers revolve around setNotificationUri () and notifyChangeCombo () that I implemented. Why does this not work in different directions?

If, for example, force update in ActivityA.onResume () with

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

then it updates the list. But this will force every resume to be updated regardless of whether the data has been changed or not.

+2
android android-contentprovider
Sep 23 '15 at 13:56
source share
2 answers

After two long two days of scratching my head and altruistic engagement from Psklin, I painted myself a picture of what was wrong. My activity A is actually much more complicated. It uses a ViewPager with a PagerAdapter with a list of instances. First, I created these components in the onCreate () method 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 the wrong order of creation. Why this did not cause an error because PagerAdapter.instantiateItem () is called aftter onCreate (). I do not know why and how this caused the initial problem. Something may not have connected correctly to lists, adapters, and content watchers. I did not delve into this.

I reordered:

 @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 magic made it work in about 75% of cases. When I examined the output of CatLog, I noticed that ActivityA (). OnStop () is called at different times. When it works, it is called late, and I see in logcat that onLoadFinished () is executing. Sometimes ActivityA.onStop () is executed immediately after the request, and then onLoadFinished () is not called at all. This leads me to posting in my DeeV jas answer that cursors are not registered with ContentResolver. It may be so. What made things somehow reveal was the fact that a simple pskink demonstrator insisted on working, and my application was not there, although they were the same in key points. This caught my attention on asynchronous things and my onCreate () method. Actually my ActivityB is complicated, so it gives enough time to stop ActivityA. What I also noticed (and it made it harder to sort) was that if I ran my 75% version in debug mode (without breakpoints), then the success rate would drop to 0. ActivityA will stop until the download is complete cursor, so my onLoadFinished () is never called, and the lists are not updated.

Two key points:

  • Somehow the order of creating odPager, CursorAdapter and CursorLoader is important.
  • ActivityA can be stopped (s) before the cursor is loaded.

But even that is not so. If I look at the simplified sequence, then I see that ActivityA.onStop () is executed before the content provider inserts the record. I do not see the request while ActivityB is active. But when I get back to ActivityA, you will get execeuted laodFinished () and the listview will update. Not so in my application. It always executes the request while still in ActivityB, why ??? This destroys my theory that onStop () is the culprit.

(Many thanks to pskink and DeeV)

UPDATE

After a lot of time on this problem, 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. 

Problem:

After manipulating the data in ActivityB, the changes are not displayed in list mode in ActivityA. List view is not updated.

After I studied a lot and studied the traces of logcat, I saw that everything happens in the 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 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 */ 

Thus, the problem is not that ActivityA is stopped to the end, but that it is stopped until late. The data is updated and notified somewhere between the creation of the ActivityB and the stop of the ActivityA. The solution is to force the bootloader in ActivityA to stop loading immediately before starting ActivityB.

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

This stops the bootloader, and (I think again) prevents the cursor from consuming notification while the activity is in the above uncertainty state. Now the sequence of events:

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

It was the most elegant solution I could find. No need to reboot bootloaders, etc. But still, I would like to hear a comment on this issue from someone with a deeper understanding.

+4
Sep 24 '15 at 21:48
source share

I do not see anything special about this. While Cursor is registered in the URI, the bootloader must restart itself with the new information. I don't think the problem is in the code here. I think LoaderManager is registering Cursor from ContentResolver too early (this happens when onStop() ) is called).

Basically, you can’t do anything about it to unregister. However, you can force the bootloader to reboot by calling LoaderManager#restartLoader(int, Bundle, LoaderCallbacks); . You can call this in onStart() (which makes calling initLoader in onCreate() useless). A more streamlined approach is to use onActivityResult() . In this case, the result of your activity does not matter. All you say is that you returned to this action 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 actions.

+2
Sep 23 '15 at 2:54
source share



All Articles