RxJava2 в CursorLoaders onLoadFinished callback

Чтобы получить данные из базы данных, я использую CursorLoader в приложении. После того как метод обратного вызова onLoadFinished() вызывает логику приложения, он преобразует объект Cursor в объект List объектов в рамках требований бизнес-модели. Это преобразование (тяжелая операция) занимает некоторое время, если имеется много данных. Это замедляет поток пользовательского интерфейса. Я попытался запустить преобразование в не-UI Thread с помощью RxJava2 передачи Cursor объекта, но получил Exception:

Caused by: android.database.StaleDataException: Attempting to access a closed CursorWindow.Most probable cause: cursor is deactivated prior to calling this method.

Вот часть кода Fragment:

@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;
        }

Цель использования CursorLoader здесь - получить уведомления от БД, если обновление данных обновлено.

Обновление Как предположил Тин Тран, я удалил CursorUtil.closeSafely(cursor);, и теперь я получаю другое исключение:

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() метод 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;
    }

Ответ 1

Как вы можете видеть из моего комментария на ваш вопрос, меня интересовало, обновляются ли данные, пока getFieldsObservable() еще не возвращен. Я получил интересующую вас информацию в вашем комментарии.

Как я могу судить, вот что происходит в вашем случае:

  • onLoadFinished() вызывается с помощью курсора-1
  • Метод RxJava выполняется в другом потоке с Cursor-1 (еще не закончен, здесь используется Cursor-1)
  • onLoadFinished() вызывается с помощью Cursor-2, API LoaderManager заботится о закрытии Cursor-1, который по-прежнему запрашивается RxJava on другой поток

Таким образом, возникает исключение.

Итак, вам лучше придерживаться создания AsyncTaskLoader (который CursorLoader). Этот AsyncTaskLoader будет содержать все логики, которые CursorLoader имеет (в основном один-к-одному экземпляр), но возвратит уже отсортированный/фильтрующий объект в onLoadFinished(YourCustomObject). Таким образом, операция, которую вы хотите выполнить с помощью RxJava, будет фактически выполняться вашим загрузчиком в нем loadInBackground().

Здесь снимок изменений, которые MyCustomLoader будет иметь в методе loadInBackground():

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 {
      ...
    }
  }
  ...
}

Где PojoWrapper:

public class PojoWrapper {
  Cursor cursor;
  List<Pojo> list;

  public PojoWrapper(Cursor cursor, List<Pojo> list) {
    this.cursor = cursor;
    this.list = list;
  }
}

Таким образом, в onLoadFinished() вам не нужно заботиться о делегировании задания другому потоку, потому что вы уже сделали это в Loader:

@Override public void onLoadFinished(Loader<PojoWrapper> loader, PojoWrapper data) {
      List<Pojo> alreadySortedList = data.list;
}

Вот весь код MyCustomLoader.

Ответ 2

Загрузчик выдает данные после того, как он узнает, что приложение больше не использует его. Например, если данные являются курсором из CursorLoader, вы не должны называть его close(). От: https://developer.android.com/guide/components/loaders.html

Вы не должны закрывать курсор самостоятельно, что я думаю CursorUtil.closeSafely(cursor).

Вы можете использовать оператор switchMap для его реализации. Он делает именно то, что мы хотим

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)
}

Теперь вам нужно изменить showFields to и getFieldsObservable на учетную запись пустым Cursor