To get data from database I use CursorLoader
in the app. Once onLoadFinished()
callback method calls the logic of app converts Cursor
object to List
of objects within business model requirements. That conversion (heavy operation) takes some time if there is a lot of data. That slows UI thread. I tried to start conversion in non-UI Thread
using RxJava2
passing Cursor
object, but got Exception
:
Caused by: android.database.StaleDataException: Attempting to access a closed CursorWindow.Most probable cause: cursor is deactivated prior to calling this method.
Here is the part of Fragment
's code:
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
QueryBuilder builder;
switch (id) {
case Constants.FIELDS_QUERY_TOKEN:
builder = QueryBuilderFacade.getFieldsQB(activity);
return new QueryCursorLoader(activity, builder);
default:
return null;
}
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (cursor.getCount() > 0) {
getFieldsObservable(cursor)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::showFields);
} else {
showNoData();
}
}
private static Observable<List<Field>> getFieldsObservable(Cursor cursor) {
return Observable.defer(() -> Observable.just(getFields(cursor))); <-- Exception raised at this line
}
private static List<Field> getFields(Cursor cursor) {
List<Field> farmList = CursorUtil.cursorToList(cursor, Field.class);
CursorUtil.closeSafely(cursor);
return farmList;
}
The purpose of using CursorLoader
here is to get notifications from DB if there is data store updated.
Update
As Tin Tran suggested, I removed CursorUtil.closeSafely(cursor);
and now I get another exception:
Caused by: java.lang.IllegalStateException: attempt to re-open an already-closed object: /data/user/0/com.my.project/databases/db_file
at android.database.sqlite.SQLiteClosable.acquireReference(SQLiteClosable.java:55)
at android.database.CursorWindow.getNumRows(CursorWindow.java:225)
at android.database.sqlite.SQLiteCursor.onMove(SQLiteCursor.java:121)
at android.database.AbstractCursor.moveToPosition(AbstractCursor.java:236)
at android.database.AbstractCursor.moveToNext(AbstractCursor.java:274)
at android.database.CursorWrapper.moveToNext(CursorWrapper.java:202)
at com.db.util.CursorUtil.cursorToList(CursorUtil.java:44)
at com.my.project.MyFragment.getFields(MyFragment.java:230)
cursorToList()
method of CursorUtil
public static <T> ArrayList<T> cursorToList(Cursor cursor, Class<T> modelClass) {
ArrayList<T> items = new ArrayList<T>();
if (!isCursorEmpty(cursor)) {
while (cursor.moveToNext()) { <-- at this line (44) of the method raised that issue
final T model = buildModel(modelClass, cursor);
items.add(model);
}
}
return items;
}
As you can see from my comment to your question, I was interested whether the data is being updated while getFieldsObservable()
hasn't been yet returned. I received the info I was interested in your comment.
As I can judge, here's what happens in your case:
onLoadFinished()
is called with Cursor-1onLoadFinished()
is called with Cursor-2, LoaderManager API takes care of closing Cursor-1
, which is still being queried by RxJava on another threadThus, an exception results.
So, you'd better stick with creating your custom AsyncTaskLoader
(which CursorLoader
extends from). This AsyncTaskLoader
will incorporate all the logics that CursorLoader
has (basically one-to-one copy), but would return already sorted/filter object in onLoadFinished(YourCustomObject)
. Thus, the operation, that you wished to perform using RxJava would actually be done by your loader in it's loadInBackground()
method.
Here's the snapshot of the changes that MyCustomLoader
will have in loadInBackground()
method:
public class MyCustomLoader extends AsyncTaskLoader<PojoWrapper> {
...
/* Runs on a worker thread */
@Override
public PojoWrapper loadInBackground() {
...
try {
Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection,
mSelectionArgs, mSortOrder, mCancellationSignal);
...
// `CursorLoader` performs following:
// return cursor;
// We perform some operation here with `cursor`
// and return PojoWrapper, that consists of `cursor` and `List<Pojo>`
List<Pojo> list = CursorUtil.cursorToList(cursor, Field.class);
return new PojoWrapper(cursor, list);
} finally {
...
}
}
...
}
Where PojoWrapper
is:
public class PojoWrapper {
Cursor cursor;
List<Pojo> list;
public PojoWrapper(Cursor cursor, List<Pojo> list) {
this.cursor = cursor;
this.list = list;
}
}
Thus, in onLoadFinished()
you do not have to take care of delegating the job to another thread, because you already have done it in your Loader
implementation:
@Override public void onLoadFinished(Loader<PojoWrapper> loader, PojoWrapper data) {
List<Pojo> alreadySortedList = data.list;
}
Here's the entire code of MyCustomLoader
.
The loader will release the data once it knows the application is no longer using it. For example, if the data is a cursor from a CursorLoader, you should not call close() on it yourself. From: https://developer.android.com/guide/components/loaders.html
You should not close the cursor yourself which I think CursorUtil.closeSafely(cursor)
does.
You can use switchMap
operator to implement that. It does exactly what we want
private PublishSubject<Cursor> cursorSubject = PublishSubject.create()
public void onCreate(Bundle savedInstanceState) {
cursorSubject
.switchMap(new Func1<Cursor, Observable<List<Field>>>() {
@Override public Observable<List<Field>> call(Cursor cursor) {
return getFieldsObservable(cursor);
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::showFields);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
cursorSubject.onNext(cursor)
}
You now need to modify showFields
to and getFieldsObservable
to account for empty Cursor
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With