Предположим, есть некоторые данные, которые можно получить по запросу из сети, попадая в сеть каждый раз, когда нам нужны эти данные, но кэширование данных на диске и в памяти будет гораздо более эффективным.
Более конкретно можно выделить это:
- Время от времени выполняем запросы из сети для подкачки свежих данных.
- Быстро извлекаем данные, при помощи сетевого кэширования.
Делать мы это будем при помощи RxJava.
Базовый шаблон
Для учета данных Observable <Data> для каждого источника (сеть, диск и память), мы можем построить простое решение с использованием всего двух операторов, concat() и first().
Метод concat() принимает несколько «наблюдаемых» данных и присоединяет их к последовательности. Метод first() выделяет только первый элемент из последовательности. Поэтому, если вы используете concat().first(), он извлекает первый элемент, получаемый из нескольких источников.
Давайте посмотрим его в действии:
// Our sources (left as an exercise for the reader) Observable<Data> memory = …; Observable<Data> disk = …; Observable<Data> network = …; // Retrieve the first source with data Observable<Data> source = Observable .concat(memory, disk, network) .first();
Ключом к этой модели является то, что concat() только подписывается на каждого потомка, когда нужно «наблюдение». Там нет ненужных запросов к более медленным источникам, если данные кэшируются, так как first() остановит последовательность. Другими словами, если память возвращает результат, то мы не будем беспокоиться, что нужно обращаться к диску или в сеть. И наоборот, если ни в памяти, ни на диске нет данные, сформируется новый запрос в сеть.
Обратите внимание, что порядок исходных «наблюдаемых» в concat() данных, имеют значения, и их можно проверить одно за другим.
Сохранение данных
Очевидно что, следующий шаг заключается в сохранении источников такими, как они приходят. Если вы не сохраните результаты запроса сети на диск или в дисковый кэш запросов в памяти, то вы никогда не увидите никаких сохраненных данных! И весь код выше, будет делать постоянные сетевые запросы.
Есть решение, чтобы каждый источник сохранял данные кэша такими, какими он изначально их получил:
Observable<Data> networkWithSave = network.doOnNext(data -> { saveToDisk(data); cacheInMemory(data); }); Observable<Data> diskWithCache = disk.doOnNext(data -> { cacheInMemory(data); });
Теперь, если вы используете networkWithSave и diskWithCache, данные будут автоматически сохранены, как только вы загрузите их.
(Преимуществом данной тактики является то, что networkWithSave / diskWithCache можно использовать в любом месте, а не только в многочисленных источников самого шаблона.)
Устаревание данных
К сожалению, теперь код для экономии данных работает немного даже слишком хорошо! Данные всегда возвращаются одни и те же, независимо от того, как устарели они или нет. Помните, что мы хотели бы иногда вернуться к серверу для получения свежих данных.
Решение в fisrt(), который также может выполнять фильтрацию. Просто установите его, чтобы отклонить данные, которые не нужны:
Observable<Data> source = Observable .concat(memory, diskWithCache, networkWithSave) .first(data -> data.isUpToDate());
Теперь мы будем извлекать только первый элемент, который квалифицируется как «до настоящего времени». Таким образом, если один из наших источников имеет устаревшие данные, мы переходим к следующему, пока мы не найдем свежие данные.
Метод first() против takeFirst()
В качестве альтернативы использованию first() для этого шаблона, можно также использовать takeFirst().
Разница между этими двумя вызовами является то, что first() будет пробрасывать NoSuchElementException, если ни один из источников не отдает достоверные данные, в то время как takeFirst() будет просто полным вызовом без исключений.
Какой вы будете использовать, зависит от того, требуется ли вам явно обрабатывать отсутствие данных или нет.
Примеры кода
Реализации приведенного выше кода вы можете найти здесь:
https://github.com/dlew/rxjava-multiple-sources-sample
Если вы хотите увидеть реальный пример использования RxJava, посмотрите приложение Gfycat, которое использует этот шаблон при получении метаданных Gfycat. Код не использует все возможности, описанные выше (так как он не нуждается в ней), но оно демонстрирует основные concat().first().