RxJava и кэшированные данные

Я все еще довольно новичок в RxJava, и я использую его в приложении для Android. Я прочитал метрическую тонну по этому вопросу, но все равно чувствую, что чего-то не хватает.

У меня есть следующий сценарий:

У меня есть данные, хранящиеся в системе, доступ к которой осуществляется через различные соединения службы (AIDL), и мне нужно получить данные из этой системы (может произойти 1-n количество асинхронных вызовов). Rx помог мне тонну в упрощении этого кода. Однако весь этот процесс имеет тенденцию потратьте несколько секунд (до 5 секунд+) поэтому мне нужно кэшировать эти данные, чтобы ускорить родное приложение.

требования на данный момент являются следующими:

  1. начальная подписка, кэш будет пустым, поэтому мы должны ждать необходимое время для загрузки. Ничего страшного. После этого данные должны быть кэшированы.

  2. последующие нагрузки должны вытягивать данные из кэша, но затем данные должны быть перезагружены, и дисковый кэш должен быть за кулисами.

проблема: у меня есть два наблюдаемых - A и B. A содержит вложенные наблюдаемые, которые извлекают данные из локальных служб (тонны здесь). B намного проще. Б просто содержит код для извлечения данных из дискового кэша.

нужно решить: a) возвращает кэшированный элемент (если кэширован) и продолжает повторную загрузку кэша диска. b) кэш пуст, загрузите данные из системы, кэшируйте их и верните. Последующие вызовы возвращаются в "a".

У меня было несколько людей, рекомендующих несколько операций, таких как flatmap, merge и даже предметы, но по какой-то причине у меня возникли проблемы с соединением точек.

Как я могу это сделать?

3 ответов


вот несколько вариантов того, как это сделать. Я постараюсь объяснить их как можно лучше. Это код салфетки, и я использую синтаксис лямбда-кода Java8, потому что я ленивый, и он красивее. :)

  1. тему, как AsyncSubject, было бы идеально, если бы вы могли сохранить их как состояния экземпляра в памяти, хотя похоже, что вам нужно сохранить их на диск. Тем не менее, я думаю, что этот подход стоит упомянуть на всякий случай. Кроме того, это просто изящная техника, чтобы знать. AsyncSubject является наблюдаемым, который испускает только последнее значение, опубликованное ему (субъект является одновременно наблюдателем и наблюдаемым), и начнет испускать только после onCompleted была вызвана. Таким образом, все, что подписывается после этого завершения, получит следующее значение.

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

    public class MyApplication extends Application {    
        private final AsyncSubject<Foo> foo = AsyncSubject.create();
    
        /** Asynchronously gets foo and stores it in the subject. */
        public void fetchFooAsync() {
            // Gets the observable that does all the heavy lifting.
            // It should emit one item and then complete.
            FooHelper.getTheFooObservable().subscribe(foo);
        }
    
        /** Provides the foo for any consumers who need a foo. */
        public Observable<Foo> getFoo() {
            return foo;
        }
    
    }
    
  2. откладывание наблюдаемого. Заметный.defer позволяет вам ждать создания наблюдаемого, пока он не будет подписан. Вы можете использовать это, чтобы позволить выборке кэша диска работать в фоновом режиме, а затем вернуть кэшированную версию или, если не в кэше, сделать реальную сделку.

    эта версия предполагает, что ваш код геттера, как Cache fetch, так и non - catch creation, блокируют вызовы, а не наблюдаемые, и отсрочка работает в фоновом режиме. Например:

    public Observable<Foo> getFoo() {
        Observable.defer(() -> {
            if (FooHelper.isFooCached()) {
                return Observable.just(FooHelper.getFooFromCacheBlocking());
            }
            return Observable.just(FooHelper.createNewFooBlocking());
        }).subscribeOn(Schedulers.io());
    }
    
  3. использовать concatWith и take. Здесь мы предполагаем, что наш метод для получения Foo из дискового кэша либо выдает один элемент и завершает, либо просто завершает без испускания, если пусто.

    public Observable<Foo> getFoo() {
        return FooHelper.getCachedFooObservable()
                .concatWith(FooHelper.getRealFooObservable())
                .take(1);
    }
    

    этот метод должен пытаться получить реальную сделку, только если кэшированный наблюдаемый закончен пустым.

  4. использовать amb или ambWith. Это, вероятно, одно из самых сумасшедших решений, но интересно отметить. amb в основном занимает пару (или больше с перегрузками) наблюдаемый и ждет, пока один из них не испустит предмет, затем он полностью отбрасывает другой наблюдаемый и просто берет тот, который выиграл гонку. Единственный способ, которым это было бы полезно, - это если шаг вычисления создания нового Foo будет быстрее, чем его извлечение с диска. В этом случае вы можете сделать что-то вроде этого:

    public Observable<Foo> getFoo() {
        return Observable.amb(
                FooHelper.getCachedFooObservable(),
                FooHelper.getRealFooObservable());
    }
    

я предпочитаю вариант 3. Что касается фактического кэширования, вы можете иметь что-то подобное в одной из записей точки (предпочтительно, прежде чем нам понадобится Foo, так как, как вы сказали, это длительная операция), более поздние потребители должны получить кэшированную версию, пока она закончила писать. С помощью AsyncSubject здесь также может помочь, чтобы убедиться, что мы не запускаем работу несколько раз, ожидая ее написания. Потребители получат только завершенный результат, но опять же, это работает только в том случае, если его можно разумно хранить в памяти.

if (!FooHelper.isFooCached()) {
    getFoo()
        .subscribeOn(Schedulers.io())
        .subscribe((foo) -> FooHelper.cacheTheFoo(foo));
}

обратите внимание, что вы должны либо держите вокруг планировщика одного потока, предназначенного для записи на диск (и чтения), и используйте .observeOn(foo) после .subscribeOn(...) или иным образом синхронизировать доступ к дисковому кэшу для предотвращения проблем параллелизма.


недавно я опубликовал библиотеку на Github для Android и Java под названием RxCache, который отвечает вашим потребностям в кэшировании данных с помощью наблюдаемых.

RxCache реализует два слоя кэширования-память и диск, и он подсчитывает с несколькими аннотациями для настройки поведения каждого поставщика.

рекомендуется использовать с модернизация для данных, полученных из http-вызовов. Используя лямбда-выражение, можно сформулировать выражение следующее:

    rxCache.getUser(retrofit.getUser(id), () -> true).flatmap(user -> user);

надеюсь, вам будет интересно:)


взгляните на проект ниже. Это мой личный взгляд на вещи, и я использовал этот шаблон в ряде приложений.

https://github.com/zsiegel/rxandroid-architecture-sample

взгляните на PersistenceService. Вместо того, чтобы попасть в базу данных (или MockService в примере проекта), вы можете просто иметь локальный список пользователей, которые обновляются с помощью метода save() и просто возвращают его в get().

Дайте мне знать, если у вас есть вопросы.