I've just implemented a CursorLoader and it works great! In fact, I didn't believe that my ListView would automatically update when the underlying data changed until I tested it. This apparently is the magic of setNotificationUri.
My question is, how does it know when the data in the cursor has changed? Say I quietly insert an additional row somewhere. Does the underlying mechanism constantly query the database and compare it with the past data? Won't that be horribly inefficient if the datasets are large?
Before I used cursorloaders, I would manually refresh when necessary. It's great that I don't have to do this anymore, but is it efficient to let the CursorLoader to this in the background?
Please, correct me if I'm wrong somewhere.
ContentProvider calls something like this in query(…) method:
// Tell the cursor what uri to watch, so it knows when its source data changes
cursor.setNotificationUri(getContext().getContentResolver(), uri);
CursorLoader get cursor back and registers an observer.
/* Runs on a worker thread */
#Override
public Cursor loadInBackground() {
Cursor cursor = getContext().getContentResolver().query(mUri, mProjection,
mSelection, mSelectionArgs, mSortOrder);
if (cursor != null) {
// Ensure the cursor window is filled
cursor.getCount();
registerContentObserver(cursor, mObserver);
}
return cursor;
}
/**
* Registers an observer to get notifications from the content provider
* when the cursor needs to be refreshed.
*/
void registerContentObserver(Cursor cursor, ContentObserver observer) {
cursor.registerContentObserver(mObserver);
}
When someone modifies data, ContentProvider notifies ContentResolver about changes:
getContext().getContentResolver().notifyChange(uri, null);
ContentResolver in its turn notifies all registered observers.
Observer, registered by CursorLoader, forces it to load new data.
Related
Below is a code snippet from an Android tutorial book I was following. loadInBackground gets a cursor and then does cursor.getCount() to "ensure that the content window is filled". What does this mean? The docs on getCount just say "returns number of rows in the cursor". I've Googled for this "ensure that the content window is filled" and there are quite a few snippets that do this, all with the same comment, but no explanation of why this is needed/how it works.
import android.content.AsyncTaskLoader;
import android.content.Context;
import android.database.Cursor;
public abstract class SQLiteCursorLoader extends AsyncTaskLoader<Cursor> {
private Cursor cursor;
public SQLiteCursorLoader(Context context) {
super(context);
}
protected abstract Cursor loadCursor();
#Override
public Cursor loadInBackground() {
Cursor cursor = loadCursor();
if (cursor != null) {
cursor.getCount(); // ensure that the content window is filled
}
return cursor;
}
}
As you know, Database returns a Cursor after a query. However, Cursor is only effectively filled with data when you try to read some information like: cursor.getCount() or cursor.moveToFirst() etc...
This is more evident during large queries.
For example, imagine that query below would return thousand of results:
Cursor cursor = db.rawQuery("select * from TABLE", null);
That statement however, does not take too much time to run...
However, when you finally call cursor.getCount() or cursor.moveToFirst(), for the first time, you may see some "lag" since the cursor is being effectively being filled with the data from database.
If you do that in Main UI Thread, you app may freeze for some seconds. Specially on low tier devices.
So, by calling that method, I believe that author is trying to ensure that data was fully loaded during loadInBackground(). This way, he ensure that data is loaded in Background an not in any other future method. This way, any future call to getCount() or moveToFirst() will be executed very quickly since the data was already loaded.
Anyway, it is not mandatory..
I'm developing an app based on Google IO presentation architecture using the first approach. Basically I have a Service, ContentProvider backed by SQLite DB and I also use Loaders.
I need a way to update UI when changes to my database occur. For instance a user might want to add an item into his basket. After I insert the item id into the basket table I want to update the UI. What approach should I use? I've seen very little information on ContentObserver so far. Is it the way to go?
In the query method of your ContentProvider attach a listener to the returned cursor:
Cursor cursor = queryBuilder.query(dbConnection, projection, selection, selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
Then in your insert/update/delete methods use code like this:
final long objectId = dbConnection.insertOrThrow(ObjectTable.TABLE_NAME, null, values);
final Uri newObjectUri = ContentUris.withAppendedId(OBJECT_CONTENT_URI, objectId );
getContext().getContentResolver().notifyChange(newObjectUri , null);
Your CursorLoader will be notified and the OnLoadFinished(Loader, Cursor) will be called again.
If you're not using a Loader, the ContentObserver is the way to go, with a few lines of code you are notified on db changes (but you will need to requery manually).
private ContentObserver objectObserver = new ContentObserver(new Handler()) {
#Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
restartObjectLoader();
}
};
Remember to call in onResume():
getContentResolver().registerContentObserver(ObjectProvider.OBJECT_CONTENT_URI, false, objectObserver);
and in onPause():
getContentResolver().unregisterContentObserver(objectObserver);
Update: UI Changes
This is a larger topic because it depends on the Adapter you use to fill the ListView or RecyclerView.
CursorAdapter
In onLoadFinished(Loader loader, Cursor data)
mAdapter.swapCursor(data);
ArrayAdapter
In onLoadFinished(Loader loader, Cursor data)
Object[] objects = transformCursorToArray(data); //you need to write this method
mAdapter.setObjects(objects); //You need to wrie this method in your implementation on the adapter
mAdapter.notifyDataSetChange();
RecyclerView.Adapter
In onLoadFinished(Loader loader, Cursor data)
Object[] objects = transformCursorToArray(data); //you need to write this method
//Here you have more mAdapter.notify....()
Read from here for different way to notify the RecyclerView.Adapter.
If you are using a list, you can fill adapter again and set it to your list. Or try to inform data set change.
Suppose you have a CONTENT_URI inside your ContentProvider in which you want to do some complex stuff and return a combination of Cursors (MergeCursor) instead of a simple, single Cursor.
It so happens that if you set the notification URI on the MergeCursor instead of a cursor from within that MergeCursor, the notification is not going to work.
Initial code:
Cursor[] cursors = { extraCursorBefore, usersCursor, extraCursorAfter };
Cursor extendedCursor = new MergeCursor(cursors);
// Make sure that potential listeners are getting notified
extendedCursor.setNotificationUri(getContext().getContentResolver(), CONTENT_URI_PEOPLE);
return extendedCursor;
PS: If by any means, somebody has another idea, or figures out why the hell this didn't work on the original MergeCursor, then please, share your knowledge.
So you need to set the notification URI on a Cursor from within the resulting MergeCursor.
Code that actually works:
Cursor[] cursors = { extraCursorBefore, usersCursor, extraCursorAfter };
Cursor extendedCursor = new MergeCursor(cursors);
// Make sure that potential listeners are getting notified
usersCursor.setNotificationUri(getContext().getContentResolver(), CONTENT_URI_PEOPLE);
return extendedCursor;
I have been working on a small To-Do list app. I used CursorLoader to update the ToDolistview from a content provider. I have a written a function onNewItemAdded(), which is called when user enters a new item in the text view and clicks enter. Refer below:
public void onNewItemAdded(String newItem) {
ContentResolver cr = getContentResolver();
ContentValues values = new ContentValues();
values.put(ToDoContentProvider.KEY_TASK, newItem);
cr.insert(ToDoContentProvider.CONTENT_URI, values);
// getLoaderManager().restartLoader(0, null, this); // commented for the sake of testing
}
#Override
protected void onResume() {
super.onResume();
//getLoaderManager().restartLoader(0, null, this); // commented for the sake of testing
}
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
CursorLoader loader = new CursorLoader(this,
ToDoContentProvider.CONTENT_URI, null, null, null, null);
Log.e("GOPAL", "In the onCreateLoader");
return loader;
}
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
int keyTaskIndex = cursor.getColumnIndexOrThrow(ToDoContentProvider.KEY_TASK);
Log.e("GOPAL", "In the onLoadFinished");
todoItems.clear();
if (cursor.moveToNext() == false) Log.e("GOPAL", "Empty Cursor");
else {
while (cursor.moveToNext()) {
ToDoItem newItem = new ToDoItem(cursor.getString(keyTaskIndex));
todoItems.add(newItem);
}
aa.notifyDataSetChanged(); // aa is arrayadapter used for the listview
}
}
I have read, CursorLoader automatically updates the view, whenever there is a data change in the content provider db. That means I suppose, getLoaderManager().restartLoader(0, null, this) has to be called implicitly whenever there is a change in data, right?
But that is not happening. Whenever I add a new item (the item is added to the db from onNewItemAdded, but restartLoader is not explicitly called), pause this activity and resume it back. I don't see any implicit call to restartLoader(even though db is changed) and the listview also is not updated with new item added. Why is that? How does a CursorLoader automatically updates the view even if app is not active???
Thanks :)
EDIT: I have also used getContext().getContentResolver().notifyChange(insertedId, null) in insert of my content provider.
I found the answer for my question. In general, CursorLoader doesn't automatically detect data changes and load them to view. We need to track URI for changes. This can be done by following steps:
Registering an Observer in content resolver through cursor using: (Done in the query method of ContentProvider)
cursor.setNotificationUri(getContext().getContentResolver(), uri);
Now when there is any change in URI underlying data using insert()/delete()/update(), we notify the ContentResolver about the change using:
getContext().getContentResolver().notifyChange(insertedId, null);
This is received by the observer, we registered in step-1 and this calls to ContentResolver.query(), which inturn calls ContentProvider's query() method to return a fresh cursor to LoaderManager. LoaderManager calls onLoadFinished() passing this cursor, along with the CursorLoader where we update the View (using Adapter.swapCursor()) with fresh data.
For Custom AsyncTaskLoaders:
At times we need our custom loader instead of CursorLoader. Here we can use someother object other than cursor to point to the loaded data (like list etc). In this we won't be having previlige to notify ContentResolver through cursor. The application may also not have a content Provider, to track URI changes. In this scenario we use BroadcastReceiver or explicit ContentObserver to achieve automatic view updation. This is as follows:
We need to define our custom loader which extends AsyncTaskLoader and implements all its abstract methods. Unlike CursorLoader, our Custom Loader may or may not use a content Provider and it's constructor may not call to ContentResolver.query(), when this loader is instatiated. So we use a broadcast receiver to serve the purpose.
We need to instantiate a BroadCastReceiver or ContentObserver in OnStartLoading() method of abstract AsyncTaskLoader class.
This BroadCast receiver should be defined to receive data-changing broadcasts from content provider or any system events(Like new app installed) and it has to call loader's onContentChanged() method, to notify the loader about the data change. Loader automatically does the rest to load the updated data and call onLoadFinished() to update the view.
For more details refer this: http://developer.android.com/reference/android/content/AsyncTaskLoader.html
I found this very useful for clear explanation : http://www.androiddesignpatterns.com/2012/08/implementing-loaders.html
Well, I think you can restart the loader on certain events. E.g. in my case I have an activity of TODOs. On clicking 'add' option, it launches new activity which has view to feed new TODO.
I am using following code in parent activity's onActivityResult()
getLoaderManager().restartLoader(0, null, this);
It works fine for me. Please share if there is any better approach.
get a reference to your loader while initializing as follows
Loader dayWeatherLoader = getLoaderManager().initLoader(LOADER_DAY_WEATHER, null, this);
then create a class that extends ContentObserver as follows
class DataObserver extends ContentObserver {
public DataObserver(Handler handler) {
super(handler);
}
#Override
public void onChange(boolean selfChange, Uri uri) {
dayWeatherLoader.forceLoad();
}
}
Then register content observer inside onResume lifecycle method as follows
#Override
public void onResume() {
super.onResume();
getContext().getContentResolver().registerContentObserver(CONTENTPROVIDERURI,true,new DayWeatherDataObserver(new Handler()));
}
Whenever there is a change in the underlying data of content provider, the onChange method of contentobserver will be called where you can ask loader to load the data again
I have an Android ListActivity that is backed by a database Cursor through a SimpleCursorAdapter.
When the items are clicked, a flag field in the coresponding row in the database is toggled and the view in the list needs to be updated.
The problem is, when the view that's updated goes off screen and is recycled, the old value is displayed on the view when it returns into view. The same thing happens whenever thr list is redrawb (orientation changes, etc).
I use notifydatasetchanged() to refresh the cursor adapter but it seems ineffective.
How should I be updating the database so the cursor is updated as well?
Call requery() on the Cursor when you change data in the database that you want reflected in that Cursor (or things the Cursor populates, like a ListView via a CursorAdapter).
A Cursor is akin to an ODBC client-side cursor -- it holds all of the data represented by the query result. Hence, just because you change the data in the database, the Cursor will not know about those changes unless you refresh it via requery().
UPDATE: This whole question and set of answers should be deleted due to old age, but that's apparently impossible. Anyone seeking Android answers should bear in mind that the Android is a swiftly-moving target, and answers from 2009 are typically worse than are newer answers.
The current solution is to obtain a fresh Cursor and use either changeCursor() or swapCursor() on the CursorAdapter to affect a data change.
requery is now deprecated. from the documentation:
This method is deprecated.
Don't use this. Just request a new cursor, so you can do this asynchronously and update your list view once the new cursor comes back.
after obtaining a new cursor one can use theadapter.changeCursor(cursor). this should update the view.
In case of using loader and automagically generated cursor you can call:
getLoaderManager().restartLoader(0, null, this);
in your activity, just after changing something on a DB, to regenerate new cursor.
Don't forget to also have event handlers defined:
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
CursorLoader cursorLoader =
new CursorLoader(this,
YOUR_URI,
YOUR_PROJECTION, null, null, null);
return cursorLoader;
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
adapter.swapCursor(data);
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
adapter.swapCursor(null);
}
I am not clear if you set the autoRequery property of CursorAdapter to true.
The adapter will check the autoRequery property; if it is false, then the cursor will not be changed.
requery() is already deprecated, just implement the simple updateUI() method like this in your CursorAdapter's child class and call it after data updates:
private void updateUI(){
swapCursor(dbHelper.getCursor());
notifyDataSetChanged();
}
It's easy.
private Db mDbAdapter;
private Cursor mCursor;
private SimpleCursorAdapter mCursorAd;
.....................................
//After removing the item from the DB, use this
.....................................
mCursor = mDbAdapter.getAllItems();
mCursorAd.swapCursor(mCursor);
Or use CursorLoader...