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