Pull to refresh

Comments 35

А чем не угодили Лоадеры в связке с Лоадер Менеджером? Я имею ввиду, Ретрофит запрос можно было бы делать в loadInBackground() и кэшировать его в самом Лоадере, а при повороте доставлять уже «кэшированный» результат. Есть очень хорошая статья у Ian Lake насчёт того, как кэшировать данные в Лоадере. Думаю, в данном случае она бы пригодилась. Да, и внутри самого Лоадера можно было бы подписаться на Rx.
Loader-ы я пробовал пару раз… Наверное я что-то в них не понял, но у меня так и не получилось в своё время добиться от них такой гибкости и лаконичности как с Rx сейчас. Ну и как я и писал в статье — Rx модный, захотелось его попробовать, тем более, что он не такой узкоспециализированный как Loader-ы и сейчас часто вакансии с его упоминанием встречаю.
До потому что лоадеры используют колбэки которые скажем так явно не улучшают ваш код. Rx же позволяет написать понятнее и временами лаконичнее. Да и с потоками проще. Непонятно только почему возникло желание писать свой велосипед, есть же, например, RxLifecycle
Слышал о RxLifecycle. Ещё раз её бегло посмотрел. Насколько я понял её использование в моём случае даст лишь возможность не отписываться вручную, самостоятельно вызывая unsubscribe(), но потребует лишнюю зависимость и нужду в расширении от RxFragment. Возможно я чего-то не понял, но, кмк, эта либа не будет серебряной пулей) Всё же статья не о избегании утечек из-за неотписанных подписчиков, а о получении данных вне зависимости от поворотов экрана.
There are only two hard things in Computer Science: cache invalidation and naming things.
— Phil Karlton


Готов выслушать ваши предложения о именовании класса)
Вы когда в google в поиск вводите «singlton», он вам что подсказывает?
Вы имеете в виду, что я не создаю экземпляра класса-синглтона тогда как в википедии написано, что он должен быть и доступ к нему должен быть через getInstance?
>>Вы когда в google в поиск вводите «singlton», он вам что подсказывает?
Просто ответьте на вопрос.
А-а-а! Опечатка, понял) Исправлю, спасибо.

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

Ну, тут, наверное, вопрос во вкусовых предпочтениях. Лично я не видел, вроде, конвенции прямо запрещающей именовать так как у меня. Лично я выбрал такой способ исключительно для упрощения автоподстановки — часто я не помню как у меня к-л фрагмент/активити точно называется, но точно знаю, что начинается название класса с Activity/Fragment. Вполне готов признать что это может быть неверный подход. Если у вас есть ссылка на более правильный принцип именования, то я бы с удовольствием её бы почитал)

AndroidStudio понимает автокомплит и с середины.
Ого, у вас еще и скобки с новой строки… Печаль-беда. Это уже точно конвенции не приветствуют.
Вот андроидовые конвенции неплохо описанные. https://github.com/ribot/android-guidelines/blob/master/project_and_code_guidelines.md

Там сразу же:


the name of the class should end with the name of the component
Да, с середины понимает, но мне кажется удобным когда с начала) Ссылку посмотрел, спасибо, буду иметь в виду теперь. Правда странно, почему в случае с xml файлами рекомендуют начинать с fragment/activity, а вот в классах — наоборот…

А скобки… Мне просто удобнее так, тем более, что сменить все скобки можно одним shortCut-ом)
Классы вы можете организовывать в пакеты, а все XML с разметкой лежат в одной директории. Отсюда и разница в конвенции. XML файлы имеют префикс для большего порядка в res/layout.
Вы меня убедили) Исправил в статье и в репозитории именование классов/ресурсов и скобки
Сейчас удобно использовать MVP и непонятно, что мешало ввести кэширование на уровне слоя данных.
Уровень View просто не должен знать о кэшировании.
Также непонятна причина неиспользования базы данных для хранения результатов запроса.
Получили данные, записали результат в базу, вернули List (который враппер над Cursor).
При повороте или восстановлении данных с экрана смотрим, есть ли закэшированный запрос.

У вас же много ненужного и непонятного кода, про code-style и naming я вообще молчу.
Есть сообщество андроид-разработчиков, которое использует общепринятые стандарты, которые тут игнорируются, потому что «я так привык». Это неуважение.

Извините, но код уровня junior developer, статью я бы советовал только примером — «как усложнить себе жизнь и делать не нужно».

Вот вам некая абстракция, которая куда привлекательнее, уровень View — внутри Fragment:

model
  .compose(bindToLifecycle()) // rxLifecycle
  .getRecords()
  .subscribeOn(Schedulers.io())
  .observeOn(AndroidSchedulers.mainThread())
  .subscribe(
    records -> {
      // TODO show result here
    },
    throwable -> {
      // TODO show error here
    });


И вот вам абстракция, уровень Model:

class Model {
  ...

  private BehaviorSubject<List<Record>> recordsSubject = BehaviorSubject.create();

  public Observable<List<Record>> getRecords() {
    return Observable.<List<Record>>create(subscriber -> {
      // some synchronization here - begin
      if (cacheIsEmpty()) {
        List<Record> records = loadRecordsFromWeb();
        saveInDatabase(records);
      }
      // some synchronization here - end

      List<Record> cachedRecords = getListFromDatabase();

      subscriber.onNext(cachedRecords);
      subscriber.onCompleted();
    })
    .flatMap(records -> {
      recordsSubject.onNext(records);
      return recordsSubject.asObservable();
    });
  }

  ...
}


Бонусом класс — wrapper над объектом Cursor (надеюсь объяснение, почему лучше использовать курсор вместо ArrayList для большого количества входных данных давать не нужно):
DbList
public class DbList<T> extends AbstractList<T> {

  public interface Factory<T> {
    T get(Cursor cursor);
  }

  public static <T> List<T> create(Cursor cursor, Factory<T> factory) {
    return new DbList<>(cursor, factory);
  }

  private final Cursor cursor;
  private final Factory<T> factory;

  private DbList(Cursor cursor, Factory<T> factory) {
    this.cursor = cursor;
    this.factory = factory;
  }

  @Override
  @Nullable
  public T get(int location) {
    if (cursor != null && cursor.moveToPosition(location)) {
      return factory.get(cursor);
    }
    return null;
  }

  @Override
  public int size() {
    return cursor == null ? 0 : cursor.getCount();
  }

}


Спасибо за полезную информацию. Отвечаю по пунктам:


  • MVP я ещё не пробовал, хотя много слышал. Судя по реакции на эту статью надо изучить. Просто изначально целью статьи было поделиться своим первым опытом с Rx и Retrofit в максимально простом и абстрактном примере.
  • В БД ничего не сохранял, т.к. изначально (хоть это и не помянуто в статье по ряду причин) задача стояла в отображении данных с коротким сроком жизни, устаревающих в течении пары часов. Засим желательно при каждом запуске приложения получать данные именно из сети, а не сохранённые ранее, ибо они уже будут нерелевантны. А так — да, в одном из своих приложений так и делал — сначала в БД данные шли, потом уже из неё во View. Но это от конкретной задачи зависит.
  • Code style и имена поправил и тут и в репозитории. Судя по реакции в комментариях всё было плохо, так что исправлюсь)
  • Про преимущества Cursor над ArrayList бы послушал — могу предположить что он просто быстрее работает?
  • Ещё раз спасибо за код в комментарии. Возможно, я попробую переделать в другой ветке проект в MVP стиле и напишу ещё одну статью)
При большом количестве входных данных есть большой шанс словить OutOfMemory, используя ArrayList, так как все данные хранятся в памяти.
Cursor лишен этого недостатка, так как подгрузка с носителя данных таблицы в память происходит по мере надобности, как и выгрузка.

Спасибо за полезную информацию)

Это ж сколько данных должно быть, чтобы OOM схватить !?, это если исключит изображения(они вообще отдельная тему)
поиграйтесь с жадными алгоритмами
Спасибо за обертку над Cursor.
А в каком месте закрывать Cursor? Наверное, нужно добавить метод close к DbList?
Идея в том, что объекты, которые используют данный List не должны знать о том, что по факту это Cursor.

В большинстве мест в коде используется вот такая конструкция:
Ссылка для понимания, что именно я использую — LINK

Собственно код:
//composer
class CursorAutoCloser implements Observable.Transformer<Cursor, Cursor> {

  private Cursor cursor;

  @Override
  public Observable<Cursor> call(Observable<Cursor> observable) {
    return observable
      .map(cursor -> this.cursor = cursor)
      .doOnUnsubscribe(() -> CursorUtil.close(this.cursor));
  }
}

//part of CursorUtil
class CursorUtil {
  ...
  public static void close(Cursor cursor) {
    if (cursor != null && !cursor.isClosed()) {
      cursor.close();
    }
  }
  ...
}

//usage example
cursorObservable
.compose(new CursorAutoCloser())
...
посмотрел последние коммиты — немного изменился класс

CursorUiTransformer
//composer
class CursorUiTransformer implements Observable.Transformer<Cursor, Cursor> {

  private Cursor cursor;

  @Override
  public Observable<Cursor> call(Observable<Cursor> observable) {
    return observable
      .subscribeOn(RxExecutors.io())
      .observeOn(RxExecutors.ui())
      .unsubscribeOn(RxExecutors.ui())
      .doOnNext(cursor -> {
        closeCursor();
        this.cursor = cursor;
      })
      .doOnUnsubscribe(this::closeCursor);
  }

  private void closeCursor() {
    CursorUtil.close(this.cursor);
  }
}

//usage example
cursorObservable
...
.compose(new CursorAutoCloser()) // apply at the end


Так делается для того, чтобы старый Cursor закрывался при множественных onNext (обновления данных) и в ui потоке — в таком случае нету конфликтов с адаптером.
Понял, спасибо.
У меня как раз сегодня появилось место для использования этого wrapper-а.

Ну а если Rx не используется? Как тогда закрывать без нарушения абстракции?

Ох, как же JavaRX мне режет глаза без лямбд.

Согласен, просто я не хотел перегружать список зависимостей для добавления библиотеки RetroLambda. Возможно, я её позже добавлю и обновлю код в статье и репозитории.
Как же режет глаза Java после Kotlin. Тем более на таких тестовых примерах. Kotlin — лямбды из коробки, и это не единственный плюс.
Все уже написано до нас :) У меня еще с февраля в черновиках лежит статья с подобной темой, жаль, что руки не дошли ее дописать.
В общем странно, что вы не наткнулись на RxLoader, который в полное мере решает описанную задачу, Так же на том же SO я уже отвечал на подобный вопрос .

Должен признаться, что я специально и не искал других решений этой задачи :) Мне просто захотелось взять Rx, Retrofit, их соединить и посмотреть что получится) А ссылка интересная, спасибо. Правда уж очень давно, похоже, не обновляли библиотеку, что некоторые сомнения вызывает...

RxLoader да, давненько не обновляли. Но мой вариант относительно свежий :)
Sign up to leave a comment.

Articles