Say I've got a Cursor just returned from a database call:
Cursor myCursor = db.rawQuery(someQuery, null);
According to the docs, the Cursor isn't actually populated before any calls are made to it, like getCount() for example. So my question is, does the following code actually query my database twice?
if(myCursor.getCount() > 1)
{
// Do something
}
else if(myCursor.getCount() == 1)
{
// Do something else
}
Or does Android cache the Cursor object after the first 'if' statement, making the 'else if' statement access the cached object instead?
No, it doesn't. The cursor object actually contains it's own count. You can see the source code for the SQLiteCursor object here.Take a look at the getCount() method:
#Override
public int getCount() {
if (mCount == NO_COUNT) {
fillWindow(0);
}
return mCount;
}
It will only check the first time. mCount is only updated when the Cursor is requeried (see QueryThread.run() on the same page).
Related
I'm using custom RecyclerView.Adapter to show the list items. I'm using LoaderManager.LoaderCallbacks<Cursor> to fetch the data from the database and in onLoadFinished() I use custom method to extract the data from the cursor and store it in the custom ArrayList one by one using while(cursor.moveToNext()). The list is loaded successfully. But upon screen rotation the list goes off.
When I debugged the code I got to know that the onLoadFinished() is called on screen rotation but the while loop is not working even if the cursor is not null and cursor count is greater than 0.
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
loadAndroidFlavours(cursor);
mCursor = cursor;
invalidateOptionsMenu();
}
private void loadAndroidFlavours(Cursor cursor) {
if (cursor.getCount() != 0) {
while (cursor.moveToNext()) {
int columnIndexID = cursor.getColumnIndex(AndroidFlavourEntry._ID);
int columnIndexFlavour = cursor.getColumnIndex(AndroidFlavourEntry.COLUMN_FLAVOUR);
int columnIndexVersion = cursor.getColumnIndex(AndroidFlavourEntry.COLUMN_VERSION);
int ID = cursor.getInt(columnIndexID);
String flavour = cursor.getString(columnIndexFlavour);
String version = cursor.getString(columnIndexVersion);
mFavoritesList.add(new AndroidFlavour(ID, flavour, version));
}
loadAndroidFlavours();
} else {
hideList();
}
}
I'm stuck. Please help with explanation. Regards!
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 got this exception when i run the project,
here is part of main activity
private void fillData() {
// Get all of the notes from the database and create the item list
Cursor notesCursor = mDbHelper.fetchAllNotes();
if (notesCursor != null) {
notesCursor.moveToFirst();
do {
messagesList.add(notesCursor.getString(notesCursor
.getColumnIndex(DbAdapter.KEY_FIELD2)));
Log.e("Test", notesCursor.getString(notesCursor
.getColumnIndex(DbAdapter.KEY_FIELD2)));
} while (notesCursor.moveToNext());
}
customAdapter.notifyDataSetChanged();
// setListAdapter(notes);
}
First of all, you should test notesCursor != null AND notesCursor.moveToNext() (which returns a boolean). Having a cursor not null doesn't mean it has results to give. And moreover, you use the do/while loop, which means you suppose you'll always have as leats one result...
I would say you should use this instead:
private void fillData() {
// Get all of the notes from the database and create the item list
Cursor notesCursor = mDbHelper.fetchAllNotes();
if (notesCursor != null) {
while(notesCursor.moveToNext())
{
messagesList.add(notesCursor.getString(notesCursor
.getColumnIndex(DbAdapter.KEY_FIELD2)));
Log.e("Test", notesCursor.getString(notesCursor
.getColumnIndex(DbAdapter.KEY_FIELD2)));
}
customAdapter.notifyDataSetChanged();
}
}
And I also think you should only call notifyDataSetChanged() if some modifications were done: so move it INSIDE your if clause: You don't have to ask your system to re-calculate your ListView if nothing as changed...
Typically, your error may occur when calling moveToNext: Your cursor doesn't have any result but you ask it to move to next... Next of what? If no result, no way to move to next...
SQliteDatabase.query() returns a Cursor object, which is positioned before the first entry.
Cursor is never null but it can be empty. Your query() will either return a Cursor object or it will throw an exception. It will never return null.
boolean moveToFirst():
returns false if cursor is empty otherwise it will move cursor to the first row of the result and returns true.
You can check something like this:
notesCursor.moveToFirst();
while(notesCursor.moveToNext())
{
messagesList.add(notesCursor.getString(notesCursor
.getColumnIndex(DbAdapter.KEY_FIELD2)));
Log.e("Test", notesCursor.getString(notesCursor
.getColumnIndex(DbAdapter.KEY_FIELD2)));
}
customAdapter.notifyDataSetChanged();
}
I get ACRA exception reports from some users that the cursor which supplies data to my appwidget (RemoteViewService) is deactivated/closed. It never happens to me in person, but it happens enough where it's a bit of an issue.
Here's the code to my RemoteViewService:
public static class ListItemService extends RemoteViewsService {
public RemoteViewsFactory onGetViewFactory(final Intent intent) {
return new RemoteViewsFactory() {
private MyCursor cursor;
public void onCreate() {
// Nothing
}
public synchronized void onDestroy() {
if (this.cursor != null)
this.cursor.close();
}
public synchronized RemoteViews getViewAt(int position) {
// Here I read from the cursor and it crashes with
// the stack trace below
}
public int getCount() {
return ((this.cursor != null) ? this.cursor.getCount() : 0);
}
public int getViewTypeCount() {
return 1;
}
public boolean hasStableIds() {
return true;
}
public long getItemId(int position) {
return position;
}
public RemoteViews getLoadingView() {
return null;
}
public synchronized void onDataSetChanged()
{
if (this.cursor != null)
this.cursor.close();
this.cursor = getApplicationCntext().getContentResolver().query(myUri, null, null, null, null);
}
};
The stack trace varies from platform version to platform version. For example, I get the following on 4.0.3:
android.database.StaleDataException: Attempting to access a closed CursorWindow.Most probable cause: cursor is deactivated prior to calling this method.
On 2.3, I get a:
java.lang.IllegalStateException: attempt to re-open an already-closed object: android.database.sqlite.SQLiteQuery
I cannot, for the life of me, figure out who or what closes the cursor on me, other than from onDestroy() and onDataSetChanged(). Some users had reported that they weren't actively working with app when the crash happened.
I suspected maybe multiple calls to the ContentProvider return the same cursor and when I display my UI which uses the same query, they step on each other. But this doesn't appear to be the case as the cursor objects are different.
Any ideas?
Looks to me like a sync issue between multiple threads, one closing the previous cursor and one immediately accessing it thereafter.
You may want to consider closing the cursor right when you first can, such as a onPause event.
Also - you can add a safety precaution as checking cursor.isClosed() before you access it again. You can also add some synchronisation to your code.
A helper method that fetches the new cursor with a local var and only once it's done, replaces the previous one and closes it may be a quicker solution in the meantime.
From AOSP doc;
This interface provides random read-write access to the result set
returned by a database query. Cursor implementations are not required
to be synchronized so code using a Cursor from multiple threads should
perform its own synchronization when using the Cursor.
From Content provider basics,
The ContentResolver.query() client method always returns a Cursor containing the columns specified by the query's projection for the rows that match the query's selection criteria. A Cursor object provides random read access to the rows and columns it contains. Using Cursor methods, you can iterate over the rows in the results, determine the data type of each column, get the data out of a column, and examine other properties of the results. Some Cursor implementations automatically update the object when the provider's data changes, or trigger methods in an observer object when the Cursor changes, or both.
Try to add this.cursor = null on onDestory() method
public synchronized void onDestroy() {
if (this.cursor != null){
this.cursor.close();
this.cursor = null
}
}
boolean android.database.Cursor.moveToNext() documentation says:
http://developer.android.com/reference/android/database/Cursor.html#moveToNext%28%29
Move the cursor to the next row.
This method will return false if the cursor is already past the last entry in the result set.
However, my book says to do the following to extract data from a cursor:
Cursor myCursor = myDatabase.query(...);
if (myCursor.moveToFirst()) {
do {
int value = myCursor.getInt(VALUE_COL);
// use value
} while (myCursor.moveToNext());
}
Who's right? These both can't be true. If you can't see the contradiction, imagine myCursor has 1 row returned from the query. The first call to getInt() will work, but then moveToNext() will return true because it is not "already" past the last entry in the result set. So now the cursor will be past the last entry and the second call to getInt() will do something undefined.
I suspect the documentation is wrong and should instead read:
This method will return false if the cursor is "already at" the last entry in the result set.
Must the cursor be already PAST (not AT) the last entry before the moveToNext() method returns false?
No Snark Please
A simpler idiom is:
Cursor cursor = db.query(...);
while (cursor.moveToNext()) {
// use cursor
}
This works because the initial cursor position is -1, see the Cursor.getPosition() docs.
You can also find usages of cursors in the Android source code itself with this Google Code Search query. Cursor semantics are the same in SQLite database and content providers.
References: this question.
Verbatim from the API:
Returns:
whether the move succeeded.
So, it means that:
Cursor in first row -> moveToNext() -> cursor in second row -> there's no second row -> return false
If you want the details, go to the source: http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.3.3_r1/android/database/AbstractCursor.java#AbstractCursor.moveToNext%28%29
public final boolean moveToNext() {
return moveToPosition(mPos + 1);
}
public final boolean moveToPosition(int position) {
// Make sure position isn't past the end of the cursor
final int count = getCount();
if (position >= count) {
mPos = count;
return false;
}
I think I tend to stay away from solutions that are based on a hidden assumption like: I hope that sqlite never changes it api and that a cursor will always start at the first item.
Also, I have almost always been able to replace a while statement with a for statement. So my solution, shows what I expect the cursor to start at, and avoids using a while statement:
for( boolean haveRow = c.moveToFirst(); haveRow; haveRow = c.moveToNext() ) {
...
}
why is showing that a cursor needs to start at the first row, well 6 months down the line you might be debugging your own code, and will wonder why you didn't make that explicit so you could easily debug it.
It appears to be down to the Android implementation of AbstractCursor and it remains broken in Jellybean.
I implemented the following unit test to demonstrate the problem to myself using a MatrixCursor:
#Test
public void testCursor() {
MatrixCursor cursor = new MatrixCursor(new String[] { "id" });
for (String s : new String[] { "1", "2", "3" }) {
cursor.addRow(new String[] { s });
}
cursor.moveToPosition(0);
assertThat(cursor.moveToPrevious(), is(true));
cursor.moveToPosition(cursor.getCount()-1);
assertThat(cursor.moveToNext(), is(true));
assertThat(cursor.moveToPosition(c.getCount()), is(true));
assertThat(cursor.moveToPosition(-1), is(true));
}
All assertions fail, contrary to the documentation for moveToNext, moveToPrevious and moveToPosition.
Reading the code at API 16 for AbstractCursor.moveToPosition(int position) it appears to be intentional behaviour, ie the methods explicitly return false in these cases, contrary to the documentation.
As a side note, since the Android code sat on existing devices in the wild cannot be changed, I have taken the approach of writing my code to match the behaviour of the existing Android implementation, not the documentation. ie. When implementing my own Cursors / CursorWrappers, I override the methods and write my own javadoc describing the departure from the existing documentation. This way, my Cursors / CursorWrappers remain interchangeable with existing Android cursors without breaking run-time behaviour.
Cursor.moveToNext() returning a boolean is only useful if it will not move the cursor past the last entry in the data set. Thus, I have submitted a bug report on the documentation's issue tracker.
https://issuetracker.google.com/issues/69259484
It reccomends the following sentence:
"This method will return false if the current (at time of execution) entry is the last entry in the set, and there is no next entry to be had."