Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RxJava2 in CursorLoader’s onLoadFinished callback

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;
    }
like image 877
devger Avatar asked Jun 06 '17 12:06

devger


2 Answers

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-1
  • RxJava's method is being executed on another thread with Cursor-1 (hasn't yet been finished, here Cursor-1 is being used)
  • onLoadFinished() is called with Cursor-2, LoaderManager API takes care of closing Cursor-1, which is still being queried by RxJava on another thread

Thus, 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.

like image 102
azizbekian Avatar answered Oct 21 '22 13:10

azizbekian


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

like image 22
Tin Tran Avatar answered Oct 21 '22 13:10

Tin Tran