I'm developing an application which relies on database (local) - and this database updates frequently.
What I'm trying to achieve is:
Suppose the data is shown in a listview. I want the listview to update the dataset as soon as any change in the database happens (or a specific table to be precise).
So far I've thought of these options:
SQLiteOpenHelper class: whenever an update/insert is done it'll notify the activity to update listview via BroadcastReceiver.
ContentProvider with CursorLoader (haven't used it before so a little skeptical)
Something else? Please suggest.
Which is the best way to achieve consistent and immediate updates without blocking the UI (performance)?
As suggested by #Karakuri created a custom CursorLoader by extending CursorLoader class without ContentProvider.
Here's the solution:
CustomCursorLoader.class
public class CustomCursorLoader extends CursorLoader {
private final ForceLoadContentObserver forceLoadContentObserver = new ForceLoadContentObserver();
public CustomCursorLoader(Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
super(context, uri, projection, selection, selectionArgs, sortOrder);
}
#Override
public Cursor loadInBackground() {
Cursor cursor = /* get cursor from DBHandler class */;
cursor.setNotificationUri(getContext().getContentResolver(), CONTENT_URI);
if (cursor != null) {
cursor.getCount();
cursor.registerContentObserver(forceLoadContentObserver);
}
return cursor;
}
}
Every time you make a change to DB, do:
getContentResolver().notifyChange(CONTENT_URI, null);
In Activity class:
implement interface LoaderManager.LoaderCallbacks<Cursor>
initiate loader getLoaderManager().initLoader(0, null, this);
and override these methods:
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CustomCursorLoader(this, CONTENT_URI, null, null, null, null);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
customCursorLoaderAdapter.swapCursor(data);
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
customCursorLoaderAdapter.swapCursor(null);
}
extend CursorAdapter class to create listview adapter and you're done.
If performance is crucial for your app you should take a look on Realm for Android database. It provides better efficiency than SQLite and you can use RealmChangeListener to listen for changes in the database.
Related
Having fragment like below, in normal flow the onCreateLoader and onLoadFinished is called in pair.
But when the datasource (the database) content is changed, and since the loader is monitoring the data change the loader will issue another call to onLoadFinished() with new data filled in cursor.
But In my case it does not want to change the current cursor in use, so don't want the loader deliver the updated cursor vis another onLoadFinished call, or disable the loader's monitoring part.
Is there a way to do it?
AFragment extends Fragment implements LoaderCallbacks<Cursor> {
protected void startSupportLoaderManager() {
getActivity().getSupportLoaderManager()
.initLoader(LOADER_ID, null, this);
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return createLoader(getActivity(), id, args, null);
}
#Override
public void onLoaderReset(Loader<Cursor> arg0) {
if (mAdapter != null) {
mAdapter.resetCursor();
}
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
...
}
}
EDIT:
Kinda of knowing this may work, but feel still missing some dot. Here is what the thought:
In the implementation of ContentProvider, for insert(), update(), delete() we do
getContext().getContentResolver().notifyChange(uri, null);
And in the CursorLoader it did cursor.registerContentObserver(mObserver); in
public Cursor loadInBackground() {
Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection,
mSelectionArgs, mSortOrder);
if (cursor != null) {
// Ensure the cursor window is filled
cursor.getCount();
cursor.registerContentObserver(mObserver);
}
return cursor;
}
So it in some way is monitoring the Uri data source change, same as we do on cursor
cursor.setNotificationUri(getContext().getContentResolver(), uri);
So if we could provide a Uri, which could used for insert/update/delete, but is different than the Uri we give to the Loader, that would still do the inert/update/delet operation but the loader will not be notified because of the Uri is different.
So basically, the loader will use different Uri than the Uri the other data operation using.
Not sure If understanding how the Loader's monitoring with the content Uri is correct. Maybe there is better way of doing it?
Any suggestion is appreciated.
If you want to stop the loader, you can use:
getLoaderManager().destroyLoader(LOADER_ID);
I have a series of ListView objects in Fragments that are being populated by a CursorAdapter which gets a Cursor from the LoaderManager for the activity. As I understand it, all database and Cursor close actions are completely handled by the LoaderManager and the ContentProvider, so at no point in any of the code am I calling .close() on anything.
Sometimes, however, I get this exception:
02-19 11:07:12.308 E/AndroidRuntime(18777): java.lang.IllegalStateException: attempt to re-open an already-closed object: android.database.sqlite.SQLiteQuery (mSql = SELECT * FROM privileges WHERE uuid!=?)
02-19 11:07:12.308 E/AndroidRuntime(18777): at android.database.sqlite.SQLiteClosable.acquireReference(SQLiteClosable.java:33)
02-19 11:07:12.308 E/AndroidRuntime(18777): at android.database.sqlite.SQLiteQuery.fillWindow(SQLiteQuery.java:82)
02-19 11:07:12.308 E/AndroidRuntime(18777): at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:164)
02-19 11:07:12.308 E/AndroidRuntime(18777): at android.database.sqlite.SQLiteCursor.onMove(SQLiteCursor.java:147)
02-19 11:07:12.308 E/AndroidRuntime(18777): at android.database.AbstractCursor.moveToPosition(AbstractCursor.java:178)
02-19 11:07:12.308 E/AndroidRuntime(18777): at android.database.CursorWrapper.moveToPosition(CursorWrapper.java:162)
02-19 11:07:12.308 E/AndroidRuntime(18777): at android.widget.CursorAdapter.getView(CursorAdapter.java:241)
I put some log code into my CursorAdapter that tells me when getView(...), getItem(...) or getItemId(...) are being called and it seems as though this is happening on the first getView(...) for a given adapter after a lot of getView(...)s for another adapter. It also happens after a user has navigated around the app a lot.
This makes me wonder if the Cursor for an adapter is being retained in the CursorAdapter, but being closed in error by the ContentProvider or the Loader. Is this possible? Should I be doing any housekeeping on the CursorAdapter based on app/activity/fragment lifecycle events?
ContentProvider query method:
class MyContentProvider extends ContentProvider {
//...
#Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
Cursor query = db.query(getTableName(uri), projection, selection, selectionArgs, null, null, sortOrder);
query.setNotificationUri(getContext().getContentResolver(), uri);
return query;
}
//...
}
Typical LoaderCallbacks:
LoaderCallbacks<Cursor> mCallbacks = new LoaderCallbacks<Cursor>() {
#Override
public void onLoaderReset(Loader<Cursor> loader) {
mArticleAdapter.swapCursor(null);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if(cursor.isClosed()) {
Log.d(TAG, "CURSOR RETURNED CLOSED");
Activity activity = getActivity();
if(activity!=null) {
activity.getLoaderManager().restartLoader(mFragmentId, null, mCallbacks);
}
return;
}
mArticleAdapter.swapCursor(cursor);
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
triggerArticleFeed();
CursorLoader cursorLoader = null;
if(id == mFragmentId) {
cursorLoader = new CursorLoader(getActivity(),
MyContentProvider.ARTICLES_URI,
null,
ArticlesContentHelper.ARTICLES_WHERE,
ArticlesContentHelper.ARTICLES_WHEREARGS,
null);
}
return(cursorLoader);
}
};
CursorAdapter constructor:
public ArticlesCursorAdapter(Context context, Cursor c) {
super(context, c, 0);
mImageloader = new ImageLoader(context);
}
I have read this question but unfortunately it hasn't got the answer to my problem as it simply suggests using a ContentProvider, which I am.
IllegalStateException: attempt to re-open an already-closed object. SimpleCursorAdapter problems
IMPORTANT NEW INFORMATION THAT HAS JUST COME TO LIGHT
I discovered some other code elsewhere in the project that was NOT using Loaders and NOT managing its Cursors properly. I've just switched this code over to use the same pattern as above; however, if this fixes things, it would suggest that an unmanaged Cursor in one part of a project can kill a properly managed one elsewhere.
Stick around.
OUTCOME OF NEW INFORMATION
That did not fix it.
NEW IDEA
#Override
onDestroyView() {
getActivity().getLoaderManager().destroyLoader(mFragmentId);
//any other destroy-time code
super.onDestroyView()
}
ie possibly yes, I should be doing housekeeping on the CursorAdapter (or rather the CursorLoader in line with lifecycle events).
OUTCOME OF NEW IDEA
Nope.
PREVIOUS IDEA
Turned out to work once I added in a minor tweak! However it's so complex that I should probably rewrite the entire question.
Have you updated your data set? It could be the case that the cursor has been re-loaded due notifying a change in the content resolver:
getContentResolver().notifyChange(URI, null);
If you have set a notification URI, this would trigger your current cursor to close and a new cursor to be returned by the cursor loader. You can then grab the new cursor if you have registered a onLoadCompleteListener:
mCursorLoader.registerListener(0, new OnLoadCompleteListener<Cursor>() {
#Override
public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) {
// Set your listview's CursorAdapter
}
});
You can try to path null instead cursor into adapter constructor. Then owerride SwapCursor(Cursor c) in adapter, move initialization of cursor data there and call it in OnLoadFinished(Loader loader, Cursor data) method of your data loader.
enter code here
#Override
public void onActivityCreated(Bundle savedInstanceState) {
// ... building your query here
mSimpleCursorAdapter = new mSimpleCursorAdapter(getActivity().getApplicationContext(),
layout, null, from, to, flags);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
contentAdapter.swapCursor(data);
}
I continue to struggle with getting a query to work with a CursorLoader in a ListFragment. I suspect part of my problem is that I'm unsure about certain details. I have an xml file, myfragment.xml, which defines the two fragments in my app. The first fragment, my ListFragment, is identified by:
android:id="#+id/frag_mylist"
When I call SimpleCursorAdapter in my ListFragment class, I believe I should do this:
String[] dataColumns = { "fieldname", "_id" };
int[] viewIDs = { R.id.frag_mylist };
mAdapter = new SimpleCursorAdapter(getActivity(), R.layout.myfragment, null, dataColumns, viewIDs, 0);
setListAdapter(mAdapter);
getLoaderManager().initLoader(0, info, (LoaderCallbacks<Cursor>) this);
where info is a Bundle that I've passed from a previous activity. Is that right? Also, I've seen some examples with 0 as the last parameter for SimpleCursorAdapter, others with CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER. What's the difference? Finally, this page may indicate that I have to retrieve a LoaderManager in my code like so:
private LoaderManager mLoaderManager;
public void onCreate(savedInstanceState) {
super.onCreate(savedInstanceState);
mLoaderManager = this.getSupportLoaderManager();
}
but this is the only place I've seen this. Is this necessary? I'm hoping that getting answers to these questions will help me dig down to why my query is returning no results. I'm fairly confident that my database is being created and populated at this point. Thanks much!
As requested below, here are the three methods of my LoaderManager.LoaderCallbacks interface:
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
String selection = "level='" + args.getString("Level") + "'";
return (Loader<Cursor>) new CursorLoader(getActivity(), MY_URI,
PROJECTION, selection, null, null);
}
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
switch (loader.getId()) {
case LOADER_ID:
mAdapter.swapCursor((android.database.Cursor) cursor);
break;
}
}
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.swapCursor(null);
}
Let me add that I've verified through the debugger that args.GetString("Level") in the onCreateLoader method is "Beginning", which is what it should be.
Add this line within your onLoadFinished after you swap the cursor
mAdapter.notifyDataSetChanged()
I have a simple contentProvider, a layout with a ListView and a button for adding Items in content Provider and a CursorLoader. The android.content.Loader, D reference states that
The Loader will monitor for changes to the data, and report them to
you through new calls here. You should not monitor the data yourself.
For example, if the data is a Cursor and you place it in a
CursorAdapter, use the CursorAdapter(android.content.Context,
android.database.Cursor, int) constructor without passing in either
FLAG_AUTO_REQUERY or FLAG_REGISTER_CONTENT_OBSERVER (that is, use 0
for the flags argument). This prevents the CursorAdapter from doing
its own observing of the Cursor, which is not needed since when a
change happens you will get a new Cursor throw another call here.
But the Log.info line in the onLoadFinished method was not executed and listView didn't refreshed. Here is my (simple) code:
public class HomeActivity extends FragmentActivity implements LoaderManager.LoaderCallbacks<Cursor>{
static final String TAG = "HomeActivity";
SimpleCursorAdapter adapter;
ListView listAnnunciVicini;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.home);
listAnnunciVicini = (ListView) findViewById(R.id.lista_annunci_vicini);
adapter = new SimpleCursorAdapter(this, R.layout.list_item, null,
new String[] {
ContentDescriptor.Annunci.Cols.ID,
ContentDescriptor.Annunci.Cols.TITOLO,
ContentDescriptor.Annunci.Cols.DESCRIZIONE
}, new int[] {
R.id.list_annunci_item_id_annuncio,
R.id.list_annunci_item_titolo_annuncio,
R.id.list_annunci_item_descrizione_annuncio
}, 0);
listAnnunciVicini.setAdapter(adapter);
// Prepare the loader. Either re-connect with an existing one,
// or start a new one.
getSupportLoaderManager().initLoader(0, null, this).forceLoad();
}
public void addRandomItem(View sender) {
ContentValues dataToAdd = new ContentValues();
dataToAdd.put(ContentDescriptor.Annunci.Cols.TITOLO, "Titolo");
dataToAdd.put(ContentDescriptor.Annunci.Cols.DESCRIZIONE, "Lorem ipsum dolor sit amet.");
this.getContentResolver().insert(ContentDescriptor.Annunci.CONTENT_URI, dataToAdd);
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// creating a Cursor for the data being displayed.
String[] proiezione = new String[] {ContentDescriptor.Annunci.Cols.ID, ContentDescriptor.Annunci.Cols.TITOLO, ContentDescriptor.Annunci.Cols.DESCRIZIONE };
CursorLoader cl = new CursorLoader(this, ContentDescriptor.Annunci.CONTENT_URI, proiezione, null, null, null);
return cl;
}
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
adapter.swapCursor(data);
Log.i(TAG, "I dati sono stati ricaricati");
}
public void onLoaderReset(Loader<Cursor> loader) {
// This is called when the last Cursor provided to onLoadFinished()
// above is about to be closed. We need to make sure we are no
// longer using it.
adapter.swapCursor(null);
}
}
Any suggestion?
AFAIK, you need to implement notification yourself in ContentProvider. For this, add something like getContext().getContentResolver().notifyChange(uri, null); to insert,update and delete method of ContentProvider and invoke.setNotificationUri(getContext().getContentResolver(), uri) on your Cursor in queryas described in official documentation. Here you can find the whole picture.
Note: You should not close the cursor (cursor.close()) in order to get
notifications about the changes.
I need to make a ListAdapter that presents data from multiple ContentProviders. The ContentProviders themselves represent one table each from relational database.
I want to use the CursorLoader system to retrieve aggregate data into ListView. Is this possible to do with 1 loader or do I need to use multiple loaders? I'd prefer to use one.
I'm not sure how I can have 2 ContentProviders interact with each other beyond doing the join manually in code which doesn't seem like a great option either.
You will have to write a Custom Loader class. For example:
public class FooLoader extends AsyncTaskLoader {
Context context;
public FooLoader(Context context) {
super(context);
this.context = context;
}
#Override
public Cursor loadInBackground() {
Log.d(TAG, "loadInBackground");
YourDatabase dbHelper = new YourDataBase(context);
SQLiteDatabase db= dbHelper.getReadableDatabase();
/*** create a custom cursor whether it is join of multiple tables or complex query**/
Cursor cursor = db.query(<TableName>, null,null, null, null, null, null, null);
return cursor;
}
}
In the calling activity or fragments onCreate() method, you would need to call the custom loader class:
public class MyFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate():" + mContent);
Loader loader = getLoaderManager().initLoader(0, null, this);
loader.forceLoad();
}
#Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Log.d(TAG, "onCreateLoader()") ;
return new FooLoader(getActivity());
}
#Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
Log.d(TAG, "onLoadFinished");
}
#Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
}
}
You might want to take a look at CursorJoiner.
I'm new to ContentLoaders myself, but I haven't yet seen a way that you could use one ContentLoader to handle multiple ContentProviders.
Are the tables you're querying in separate databases? It isn't clear from your question. If the tables are all in the same database, one alternative might be to instead use one ContentProvider for the separate tables. The data can be joined and returned to one cursor, which means you could use one CursorLoader. The SQLiteQueryBuilder.setTables() method has some information on this:
http://developer.android.com/reference/android/database/sqlite/SQLiteQueryBuilder.html#setTables%28java.lang.String%29
and you can see it in action here:
http://code.google.com/p/openintents/source/browse/trunk/shoppinglist/ShoppingList/src/org/openintents/shopping/provider/ShoppingProvider.java
this might also be helpful:
https://stackoverflow.com/a/3196484/399105