Realm, RxJava, asObservable() и doOnUnsubscribe()

В моих проектах Android я использую realm в качестве механизма хранения данных. Я люблю это!
Я также использую RxJava, потому что он делает "threading" намного проще, и мне очень нравится весь "реактивный образ мышления". Я люблю это!

Я использую шаблон MVP + некоторые идеи "чистой архитектуры" для создания моих приложений.

Мои Interactors являются единственными, кто знает о Realm. Я выставляю данные с помощью Observable, например:

@Override
public Observable<City> getHomeTown() {
    final Realm realm = Realm.getDefaultInstance();
    return realm.where(City.class).equalTo("name", "Cluj-Napoca").findAllAsync().asObservable()
            .doOnUnsubscribe(new Action0() {
                @Override
                public void call() {
                    realm.close();
                }
            })
            .compose(new NullIfNoRealmObject<City>());
}

Проблема заключается в том, что мой побочный эффект doOnUnsubscribe вызывается до того, как Realm может выполнять свою работу, обрабатывая видимые видимые объекты:

Caused by: java.lang.IllegalStateException: This Realm instance has already been closed, making it unusable.
at io.realm.BaseRealm.checkIfValid(BaseRealm.java:344)
at io.realm.RealmResults.removeChangeListener(RealmResults.java:818)
at io.realm.rx.RealmObservableFactory$3$2.call(RealmObservableFactory.java:137)
at rx.subscriptions.BooleanSubscription.unsubscribe(BooleanSubscription.java:71)
at rx.internal.util.SubscriptionList.unsubscribeFromAll(SubscriptionList.java:124)
at rx.internal.util.SubscriptionList.unsubscribe(SubscriptionList.java:113)
at rx.Subscriber.unsubscribe(Subscriber.java:98)
at rx.internal.util.SubscriptionList.unsubscribeFromAll(SubscriptionList.java:124)
at rx.internal.util.SubscriptionList.unsubscribe(SubscriptionList.java:113)
at rx.Subscriber.unsubscribe(Subscriber.java:98)
at rx.subscriptions.CompositeSubscription.unsubscribeFromAll(CompositeSubscription.java:150)
at rx.subscriptions.CompositeSubscription.unsubscribe(CompositeSubscription.java:139)
at ro.tudorluca.realm.sandbox.city.CityPresenter.onDestroy(CityPresenter.java:62)
at ro.tudorluca.realm.sandbox.city.CityActivity.onDestroy(CityActivity.java:35)

Я создал проект sandbox для этого варианта использования.

Мне действительно нравится использовать Realm + RxJava, но я не могу найти чистого решения для close экземпляра Realm, когда я unsubscribe (обычно я отказываюсь от подписки, когда действие уничтожается). Любые идеи?

Изменить 1: https://github.com/realm/realm-java/issues/2357
Изменить 2: благодаря очень активной команде realm, для исправления этой проблемы уже существует запрос на перенос.

Ответ 1

21 час спустя, и вот что я придумал:

@Override
public Observable<City> getHomeTown() {
    return getManagedRealm()
            .concatMap(new Func1<Realm, Observable<City>>() {
                @Override
                public Observable<City> call(Realm realm) {
                    return realm.where(City.class).equalTo("name", "Cluj-Napoca").findAllAsync().asObservable()
                            .compose(new NullIfNoRealmObject<City>());
                }
            });
}

private static Observable<Realm> getManagedRealm() {
    return Observable.create(new Observable.OnSubscribe<Realm>() {
        @Override
        public void call(final Subscriber<? super Realm> subscriber) {
            final Realm realm = Realm.getDefaultInstance();
            subscriber.add(Subscriptions.create(new Action0() {
                @Override
                public void call() {
                    realm.close();
                }
            }));
            subscriber.onNext(realm);
        }
    });
}

Я попробовал что-то вроде этого, прежде чем публиковать вопрос о stackoverflow, но моя ошибка заключалась в использовании flatMap() вместо concatMap().

В отличие от flatMap(), concatMap() будет поддерживать порядок выбросов, что в моем случае означает, что мой Action0 -> realm.close() будет последним действием, вызываемым после отмены подписки из потока, после Realm Action0 -> results.removeChangeListener(listener), который был вызывая проблему.

Полный пример можно найти на github.

Изменить: благодаря очень активной команде realm, для исправления этой проблемы уже существует pull request.

Ответ 2

Поскольку вы сказали, что только Interactor "знает" об инфраструктуре Realm, я бы сказал, что даже не вернул управляемый объект Realm, вместо этого вернет неуправляемую копию результатов, используя copyFromRealm. Таким образом, вам не нужно заботиться о том, чтобы экземпляр Realm был открыт или закрыт в Presenter.

В то же время я бы позволил Presenter выбрать, что вызов должен выполняться асинхронно или нет, поскольку RxJava делает это довольно круто и легко, и у вас не будет проблем, вызывающих метод загрузки Interactor в другом потоке (который может избегайте использования Loopers, но почему это затрудняет ситуацию, если вы можете сделать это проще: P).

Итак, я бы пошел за:

Override
public Observable<City> getHomeTown() {
    final Realm realm = Realm.getDefaultInstance();
    City city = realm.where(City.class).equalTo("name", "Cluj-Napoca").findFirst();

    // make sure we don't send back Realm stuff, this is a deep copy that will copy all referenced objects (as the method doc says)
    City cityUnmanaged = realm.copyFromRealm(city);

    // safe to close the realm instance now
    realm.close();

    return Observable.just(cityUnmanaged);
}

Мне интересно узнать больше вариантов:).

Ответ 3

Как мне кажется, одна из главных вещей, которые нужно учесть в хорошей архитектуре, - это модульность. Все основные модули (или библиотеки) должны быть изолированы от остальной части кода. Поскольку Realm, RealmObject или RealmResult не могут быть переданы по потокам, еще важнее сделать операции, связанные с Realm и Realm, изолированными от остальной части кода.

Помня эту философию, я придумал следующий подход.

Для каждого класса jsonModel мы создаем класс realmModel и класс DAO (Data Access Object). Идея здесь заключается в том, что кроме класса DAO ни один из классов не должен знать или получать доступ к объектам realmModel или Realm. Класс DAO принимает jsonModel, преобразует его в realmModel, выполняет операции чтения/записи/редактирования/удаления, а для операций чтения DAO преобразует приведенные realmModel в jsonModel и возвращает их.

Таким образом, легко поддерживать Realm, избегать всех проблем, связанных с потоком, легко тестировать и отлаживать.

Вот статья о лучших практиках Realm с хорошей архитектурой https://medium.com/@Viraj.Tank/realm-integration-in-android-best-practices-449919d25f2f

Также показан пример проекта, демонстрирующий интеграцию Realm на Android с MVP (Presentation Viewer), RxJava, Retrofit, Dagger, Annotations and Testing. https://github.com/viraj49/Realm_android-injection-rx-test