Решил я однажды таки попробовать дико популярный нынче Rx. А заодно и Retrofit. И посмотреть, как с их помощью реализовать стандартную задачу: получить с сервера набор данных, отобразить их и при этом ничего не терять при поворотах экрана и не делать лишних запросов. Первый вариант у меня получился сразу почти — просто взял и вызвал cache() на Observable, получаемый из синглтона, но он меня не устраивал — для принудительного обновления приходилось, по какой-то причине, пересоздавать экземпляры классов Retrofit и его же реализации моего интерфейса для API. Пересоздание же самого Observable эффекта не давало — всегда возвращались старые данные вместо запуска нового сетевого запроса и получения новых данных.


После долгих мучений с новой для себя технологией выяснил, что во всём был виновен cache() (точнее, наверное, моё неправильное оного понимание). В итоге сделал так: фрагмент запускает метод, подписывающий Subscriber синглтона на Observable retrofit-a, коий запускает onNext и onError BehaviorSubject-a, на который подписывается уже Subscriber фрагмента. Код на GitHub тут, подробности — под катом.


Итак, приступим. Сначала напишем простейший php код, коий будет отдавать JSON. Чтобы успевать повернуть экран сделаем так чтобы перед отдачей данных была задержка секунд в 5.


<?php
$string = '[
  {
    "title": "Some awesome title 1",
    "text": "Lorem ipsum dolor sit amet..."
  },
  {
    "title": "Some awesome title 2",
    "text": "Lorem ipsum dolor sit amet..."
  }
]';

$seconds = 5;
sleep($seconds);

$json = json_decode($string);
print json_encode($json, JSON_PRETTY_PRINT);

Теперь зависимости в gradle:


compile 'com.android.support:appcompat-v7:23.3.0'
compile 'com.android.support:design:23.3.0'
compile 'com.android.support:cardview-v7:23.3.0'
compile 'com.android.support:recyclerview-v7:23.3.0'

compile 'io.reactivex:rxjava:1.1.3'
compile 'io.reactivex:rxandroid:1.1.0'

compile 'com.squareup.retrofit2:adapter-rxjava:2.0.2'
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'

compile 'com.google.code.gson:gson:2.6.2'

Использовать более свежие версии либ от гугла не будем — уж столько раз обжигался на их бездумном обновлении у себя в проектах… То атрибуты какие-нибудь в стилях виджетов поменяют, то баг, уже однажды поправленный вернут, то новый придумают. Версия 23.3.0 работает относительно стабильно, засим берём её.


Переходим к коду. Вот какая структура проекта у меня получилась:


image


Разметка активити будет простой, вот она:


<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    android:id="@+id/root"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar_layout"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:minHeight="?attr/actionBarSize">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:layout_scrollFlags="scroll|enterAlways"/>
    </android.support.design.widget.AppBarLayout>

    <FrameLayout
        android:id="@+id/container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:paddingEnd="@dimen/activity_horizontal_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingStart="@dimen/activity_horizontal_margin"/>
</android.support.design.widget.CoordinatorLayout>

Код в активити не менее лаконичен:


public class MainActivity extends AppCompatActivity {
    private Toolbar toolbar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initViews();

        setSupportActionBar(toolbar);

        Fragment fragmentHotelsList = getSupportFragmentManager().findFragmentById(R.id.container);
        if (fragmentHotelsList == null) {
            fragmentHotelsList = new ModelsListFragment();
            getSupportFragmentManager().
                    beginTransaction().add(R.id.container, fragmentHotelsList)
                    .commit();
        }
    }

    private void initViews() {
        toolbar = (Toolbar) findViewById(R.id.toolbar);
    }
}

Основа готова, теперь о том, как приложение должно себя вести:


  • При запуске приложения должен стартовать запрос в сеть.
  • Ответом должны служить либо данные, либо ошибка.
  • При повороте экрана и пересоздании активити/фрагмента мы должны отобразить уже загруженные данные, если они есть. Если же их нет или был запущен до этого ещё не завершённый запрос о новых данных, мы должны отобразить индикатор загрузки и подписаться на получение данных.
  • Естественно, мы не хотим ни терять данные, ни повторно запускать новый запрос в сеть.
  • Также нам нужна возможность принудительного обновления данных.

Как упоминалось в начале, я возлагал большие надежды на cache(), но, насколько я понял, он кэширует сам запрос в сеть и даже пересоздание Observable не позволяет делать новый запрос в сеть без пересоздания ещё и объектов Retrofita, что, очевидно, неправильный путь. Поначалу я никак не мог сообразить как же мне поступить. Поковыряв код и так и сяк пару часов решился на крайние меры — задал вопрос на stackoverflow. Там мне не ответили прямо, но дали 2 подсказки — про уже помянутое поведение cache() и про то, что можно попробовать использовать BehaviorSubject, который может как получать, так и отправлять данные, да ещё и хранящий последние данные в себе.


С последним возникла сразу небольшая проблема — не долго думая я подписал BehaviorSubject на Observable retrofit-a, а фрагмент на BehaviorSubject. Вроде всё верно, вот только если во время поворота экрана задача будет завершена, то фрагмент в качестве последних данных получит… правильно — событие onComplete, а не сами данные. Тут я ненадолго завис, пытаясь загуглить как помешать Observable излучать событие окончания работы или как его игнорировать у подписчиков. Гугл молчал и всячески этим намекал, что я не в ту сторону капаю. И да — подобная идея могла придти в голову только новичку в технологии) Решение оказалось простым — вместо попыток изменить поведение Observable я просто не стал подписывать на него BehaviorSubject, а просто в колбэках первого (onNext и onError) вызвал соответствующие методы второго. А onComplete — проигнорировал.


В итоге вот такой получился синглтон:


public class RetrofitSingleton {
    private static final String TAG = RetrofitSingleton.class.getSimpleName();

    private static Observable<ArrayList<Model>> observableRetrofit;
    private static BehaviorSubject<ArrayList<Model>> observableModelsList;
    private static Subscription subscription;

    private RetrofitSingleton() {
    }

    public static void init() {
        Log.d(TAG, "init");

        RxJavaCallAdapterFactory rxAdapter = RxJavaCallAdapterFactory.createWithScheduler(Schedulers.io());

        Gson gson = new GsonBuilder().create();

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(Const.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .addCallAdapterFactory(rxAdapter)
                .build();

        GetModels apiService = retrofit.create(GetModels.class);

        observableRetrofit = apiService.getModelsList();
    }

    public static void resetModelsObservable() {
        observableModelsList = BehaviorSubject.create();

        if (subscription != null && !subscription.isUnsubscribed()) {
            subscription.unsubscribe();
        }
        subscription = observableRetrofit.subscribe(new Subscriber<ArrayList<Model>>() {
            @Override
            public void onCompleted() {
                //do nothing
            }

            @Override
            public void onError(Throwable e) {
                observableModelsList.onError(e);
            }

            @Override
            public void onNext(ArrayList<Model> models) {
                observableModelsList.onNext(models);
            }
        });
    }

    public static Observable<ArrayList<Model>> getModelsObservable() {
        if (observableModelsList == null) {
            resetModelsObservable();
        }
        return observableModelsList;
    }
}

Теперь собственно фрагмент. Т.к. нам нужен способ принудительного обновления и индикатор загрузки, то, казалось бы, самым очевидным решение будет использование SwipeRefreshLayout. Но с ним большие проблемы, а именно — в установке ему статуса refreshing, т.е. показа крутящегося кружочка. Он порой либо не показывается вовсе, либо не исчезает, когда должен. Также, после появления CoordinatorLayout в разных версиях библиотек поддержки этот виджет начинает неправильно работать с AppBarLayout (Потяни-чтоб-обновить срабатывает ещё до полного раскрытия AppBarLayout и мешает его скроллу вниз). При чём однажды в гугле этот баг поправили, а потом… вернули обратно. А потом опять… В общем, в нашем примере мы не будем трогать этот виджет, а сделаем кнопку в меню и свою простой ImageView с анимацией вращения, коий будем в нужные моменты скрывать/показывать. Просто и никаких проблем со SwipeRefreshLayout.


Вот разметка фрагмента:


<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <ImageView
        android:id="@+id/loading_indicator"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_gravity="center"
        android:contentDescription="@string/app_name"
        android:src="@drawable/ic_autorenew_indigo_500_48dp"
        android:visibility="gone"/>
</FrameLayout>

Так просто, что можно и не приводить. Java-код же фрагмента сложнее немного, так что его точно приведём.


ModelsListFragment
public class ModelsListFragment extends Fragment {
    private static final String TAG = ModelsListFragment.class.getSimpleName();
    private Subscription subscription;
    private ImageView loadingIndicator;
    private RecyclerView recyclerView;
    private ArrayList<Model> models = new ArrayList<>();
    private boolean isLoading;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        inflater.inflate(R.menu.menu_models_list, menu);
        super.onCreateOptionsMenu(menu, inflater);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        switch (id) {
            case R.id.refresh:
                Log.d(TAG, "refresh clicked");
                RetrofitSingleton.resetModelsObservable();
                showLoadingIndicator(true);
                getModelsList();
                return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.fragment_models_list, container, false);

        if (savedInstanceState != null) {
            models = savedInstanceState.getParcelableArrayList(Const.KEY_MODELS);
            isLoading = savedInstanceState.getBoolean(Const.KEY_IS_LOADING);
        }

        recyclerView = (RecyclerView) v.findViewById(R.id.recycler);
        loadingIndicator = (ImageView) v.findViewById(R.id.loading_indicator);

        recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        recyclerView.setAdapter(new ModelsListRecyclerAdapter(models));

        if (models.size() == 0 || isLoading) {
            showLoadingIndicator(true);
            getModelsList();
        }

        return v;
    }

    private void showLoadingIndicator(boolean show) {
        isLoading = show;
        if (isLoading) {
            loadingIndicator.setVisibility(View.VISIBLE);
            loadingIndicator.animate().setInterpolator(new AccelerateDecelerateInterpolator()).rotationBy(360).setDuration(500).setListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    loadingIndicator.animate().setInterpolator(new AccelerateDecelerateInterpolator()).rotationBy(360).setDuration(500).setListener(this);
                }
            });
        }
        else {
            loadingIndicator.animate().cancel();
            loadingIndicator.setVisibility(View.GONE);
        }
    }

    private void getModelsList() {
        if (subscription != null && !subscription.isUnsubscribed()) {
            subscription.unsubscribe();
        }
        subscription = RetrofitSingleton.getModelsObservable().
                subscribeOn(Schedulers.io()).
                observeOn(AndroidSchedulers.mainThread()).
                subscribe(new Subscriber<ArrayList<Model>>() {
                    @Override
                    public void onCompleted() {
                        Log.d(TAG, "onCompleted");
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.d(TAG, "onError", e);
                        isLoading = false;
                        if (isAdded()) {
                            showLoadingIndicator(false);
                            Snackbar.make(recyclerView, R.string.connection_error, Snackbar.LENGTH_SHORT)
                                    .setAction(R.string.try_again, new View.OnClickListener() {
                                        @Override
                                        public void onClick(View v) {
                                            RetrofitSingleton.resetModelsObservable();
                                            showLoadingIndicator(true);
                                            getModelsList();
                                        }
                                    })
                                    .show();
                        }
                    }

                    @Override
                    public void onNext(ArrayList<Model> newModels) {
                        Log.d(TAG, "onNext: " + newModels.size());
                        int prevSize = models.size();
                        isLoading = false;
                        if (isAdded()) {
                            recyclerView.getAdapter().notifyItemRangeRemoved(0, prevSize);
                        }
                        models.clear();
                        models.addAll(newModels);
                        if (isAdded()) {
                            recyclerView.getAdapter().notifyItemRangeInserted(0, models.size());
                            showLoadingIndicator(false);
                        }
                    }
                });
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putParcelableArrayList(Const.KEY_MODELS, models);
        outState.putBoolean(Const.KEY_IS_LOADING, isLoading);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (subscription != null && !subscription.isUnsubscribed()) {
            subscription.unsubscribe();
        }
    }
}

Вот что в нём есть, по пунктам:


  • При создании говорим, что в нём есть свои элементы в меню.
  • Добавляем в меню активити новые элементы меню.
  • Переопределяем метод нажатий на меню и в нём запускаем принудительное обновление данных (вызываем метод синглтона, запускаем анимацию индикатора загрузки, переподписываемся на BehaviorSubject)
  • В onCreateView загружаем разметку фрагмента, восстанавливаем состояние (т.е. список в данными и статус загружаю/не загружаю) и, проверив, что у нас список с данными пуст или мы в процессе загрузки отображаем индикатор и подписываемся на BehaviorSubject.
  • В методе getModelsList() мы сначала отписываемся от BehaviorSubject, если подписаны и подписывемся на него же. В onNext и onError соответствующе реагируем: показываем SnackBar с текстом ошибки и кнопкой "повторить"; обновляем данные в списке данных фрагмента, уведомляем об этом адаптер. В обоих случаях останавливаем индикатор загрузки (если фрагмент добавлен (isAdded())) и обновляем статус загружаем/не загружаем.
  • В onSaveInstanceState сохраняем состояние
  • В onDestroy отписываемся от BehaviorSubject

На счёт того, когда надо подписываться и отписываться я не уверен. В Интернете видел советы делать это в onResume/onPause и думал сделать так же… Но мне слишком понравилось то, что если отписываться в onDestroy, то даже после сворачивания приложения до прихода данных данные в итоге во фрагмент поступят и после переключения обратно на приложения они отобразятся. Да, если сделать иначе, то при разворачивании приложения вызовется onResume, мы заново подпишемся на BehaviorSubject и данные никуда не денутся и придут… Но и мой способ работает — если у вас есть возражения и/или какие-то мысли на сей счёт — напишите в комментах.


Ну и на последок — модель данных. Надо было, пожалуй, ближе к началу её поместить, но так всё так просто, что я решил поместить её в конце. Единственное, на что там стоит обратить внимание — это на реализацию классом интерфейса Parcelable, позволяющего записыывать модель в Bundle для восстановления после поворотов экрана. Ну и помянуть, что для правильной работы парсинга JSON-строки из API в модель надо чтобы для полей класса присутствовали как сеттеры, так и геттеры. Ну и чтобы в аннотациях к полям были верные значения.


public class Model implements Parcelable {
    /**
     * Parcel implementation
     */
    public static final Parcelable.Creator<Model> CREATOR = new Parcelable.Creator<Model>() {
        @Override
        public Model createFromParcel(Parcel source) {
            return new Model(source);
        }

        @Override
        public Model[] newArray(int size) {
            return new Model[size];
        }
    };

    @SerializedName("title")
    private String title;

    @SerializedName("text")
    private String text;

    /**
     * Parcel implementation
     */
    private Model(Parcel in) {
        this.title = in.readString();
        this.text = in.readString();
    }

    /**
     * Parcel implementation
     */
    @Override
    public int describeContents() {
        return 0;
    }

    /**
     * Parcel implementation
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(title);
        dest.writeString(text);
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }
}

Вот и всё. Мы попробовали в бою Retrofit + RxJava/RxAndroid и получили рабочий прототип приложения, кое не ест лишний трафик, не падает при повороте экрана и имеет модные библиотеки в зависимостях. Спасибо, что дочитали до конца!


P.S. Ещё раз ссылки:
Вопрос на stackoverflow: http://ru.stackoverflow.com/q/541099/17609
Репозиторий на GitHub: https://github.com/mohaxspb/RxRetrofitAndScreenOrientation

Поделиться с друзьями
-->

Комментарии (34)


  1. Lucky_spirit
    12.07.2016 11:44

    А чем не угодили Лоадеры в связке с Лоадер Менеджером? Я имею ввиду, Ретрофит запрос можно было бы делать в loadInBackground() и кэшировать его в самом Лоадере, а при повороте доставлять уже «кэшированный» результат. Есть очень хорошая статья у Ian Lake насчёт того, как кэшировать данные в Лоадере. Думаю, в данном случае она бы пригодилась. Да, и внутри самого Лоадера можно было бы подписаться на Rx.


    1. kuchanov
      12.07.2016 11:50
      -1

      Loader-ы я пробовал пару раз… Наверное я что-то в них не понял, но у меня так и не получилось в своё время добиться от них такой гибкости и лаконичности как с Rx сейчас. Ну и как я и писал в статье — Rx модный, захотелось его попробовать, тем более, что он не такой узкоспециализированный как Loader-ы и сейчас часто вакансии с его упоминанием встречаю.


    1. 9arret
      12.07.2016 14:06

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


      1. kuchanov
        12.07.2016 14:37
        -1

        Слышал о RxLifecycle. Ещё раз её бегло посмотрел. Насколько я понял её использование в моём случае даст лишь возможность не отписываться вручную, самостоятельно вызывая unsubscribe(), но потребует лишнюю зависимость и нужду в расширении от RxFragment. Возможно я чего-то не понял, но, кмк, эта либа не будет серебряной пулей) Всё же статья не о избегании утечек из-за неотписанных подписчиков, а о получении данных вне зависимости от поворотов экрана.


  1. Beanut
    12.07.2016 14:49
    +3

    Мне одному режет глаз «Singlton»?


    1. kuchanov
      12.07.2016 17:49

      There are only two hard things in Computer Science: cache invalidation and naming things.
      — Phil Karlton


      Готов выслушать ваши предложения о именовании класса)


      1. Beanut
        12.07.2016 17:52

        Вы когда в google в поиск вводите «singlton», он вам что подсказывает?


        1. kuchanov
          12.07.2016 18:07

          Вы имеете в виду, что я не создаю экземпляра класса-синглтона тогда как в википедии написано, что он должен быть и доступ к нему должен быть через getInstance?


          1. Beanut
            12.07.2016 18:13
            +1

            >>Вы когда в google в поиск вводите «singlton», он вам что подсказывает?
            Просто ответьте на вопрос.


            1. kuchanov
              12.07.2016 18:23
              +1

              А-а-а! Опечатка, понял) Исправлю, спасибо.


      1. Bringoff
        12.07.2016 18:46

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


        1. kuchanov
          12.07.2016 19:05
          -1

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


          1. Bringoff
            12.07.2016 19:10

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


            1. Bringoff
              12.07.2016 19:13

              Там сразу же:


              the name of the class should end with the name of the component


            1. kuchanov
              12.07.2016 19:28
              -1

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

              А скобки… Мне просто удобнее так, тем более, что сменить все скобки можно одним shortCut-ом)


              1. meandnano
                13.07.2016 09:08
                +1

                Классы вы можете организовывать в пакеты, а все XML с разметкой лежат в одной директории. Отсюда и разница в конвенции. XML файлы имеют префикс для большего порядка в res/layout.


                1. kuchanov
                  13.07.2016 13:08

                  Вы меня убедили) Исправил в статье и в репозитории именование классов/ресурсов и скобки


  1. Tiberal
    12.07.2016 18:59

    Форматирование классное.


  1. withoutuniverse
    12.07.2016 22:40

    Сейчас удобно использовать 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();
      }
    
    }
    
    


    1. kuchanov
      13.07.2016 13:27

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


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


      1. withoutuniverse
        13.07.2016 16:16

        При большом количестве входных данных есть большой шанс словить OutOfMemory, используя ArrayList, так как все данные хранятся в памяти.
        Cursor лишен этого недостатка, так как подгрузка с носителя данных таблицы в память происходит по мере надобности, как и выгрузка.


        1. kuchanov
          13.07.2016 18:13

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


        1. andreich
          18.07.2016 09:29
          +1

          Это ж сколько данных должно быть, чтобы OOM схватить !?, это если исключит изображения(они вообще отдельная тему)


    1. mefi100fell
      15.07.2016 11:44
      +1

      Спасибо за обертку над Cursor.
      А в каком месте закрывать Cursor? Наверное, нужно добавить метод close к DbList?


      1. withoutuniverse
        15.07.2016 17:57

        Идея в том, что объекты, которые используют данный 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())
        ...
        


        1. withoutuniverse
          15.07.2016 18:45

          посмотрел последние коммиты — немного изменился класс

          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
          


          1. mefi100fell
            15.07.2016 20:01

            Понял, спасибо.
            У меня как раз сегодня появилось место для использования этого wrapper-а.


        1. Bringoff
          16.07.2016 09:46

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


  1. Nagg
    13.07.2016 11:08

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


    1. kuchanov
      13.07.2016 13:12
      -1

      Согласен, просто я не хотел перегружать список зависимостей для добавления библиотеки RetroLambda. Возможно, я её позже добавлю и обновлю код в статье и репозитории.


  1. Jukobob
    14.07.2016 18:33
    -2

    Как же режет глаза Java после Kotlin. Тем более на таких тестовых примерах. Kotlin — лямбды из коробки, и это не единственный плюс.


  1. andreich
    18.07.2016 09:26

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


    1. kuchanov
      18.07.2016 20:32

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


      1. andreich
        19.07.2016 07:59

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