Использование Rxjava И Retrofit На Android С Учетом Поворота Экрана

Однажды я решил попробовать ставший безумно популярным Rx. И в то же время модернизировать.

И посмотрите, как с их помощью реализовать стандартную задачу: получить набор данных с сервера, отобразить его и при этом ничего не потерять при повороте экрана и не делать лишних запросов.

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

После долгих мучений с новой технологией я выяснил, что во всем виноват кэш() (точнее, наверное, мое неправильное его понимание).

В итоге я сделал так: фрагмент запускает метод, подписывающий Подписчика синглтона на модификацию Observable, которая запускает onNext и onError BehaviorSubject, на который уже подписан Подписчик фрагмента.

Код на 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'

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

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

Версия 23.3.0 работает относительно стабильно, так что берем ее.

Перейдем к коду.

Вот структура проекта, которую я придумал:

Использование RxJava и Retrofit на Android с учетом поворота экрана

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

<Э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); } }

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

  • При запуске приложения должен начаться запрос к сети.

  • Ответом должны быть либо данные, либо ошибка.

  • Когда мы поворачиваем экран и воссоздаем активность/фрагмент, мы должны отображать уже загруженные данные, если они есть.

    Если их нет или запущен ранее неполный запрос новых данных, мы должны вывести индикатор загрузки и подписаться на получение данных.

  • Естественно, мы не хотим ни терять данные, ни повторно отправлять новый запрос в сеть.

  • Нам также нужна возможность принудительного обновления данных.

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

Сначала я не мог понять, что делать.

Повозившись с кодом так и сяк пару часов, я решил пойти на крайние меры — спросил вопрос по stackoverflow .

Прямо мне не ответили, но дали 2 подсказки - по поводу уже упомянутого поведения кэша() и по поводу того, что можно попробовать использовать BehaviorSubject, который может как получать, так и отправлять данные, а также хранит самые последние данные сами по себе.

С последним сразу возникла небольшая проблема — я не долго думая подписал BehaviorSubject на Observable-ретрофит, а фрагмент — на 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. Но с ним есть большие проблемы, а именно с установкой его статуса обновления, т.е.

показа вращающегося круга.

Иногда оно либо не появляется вообще, либо не исчезает, когда должно.

Также после появления координатораLayout в разных версиях библиотек поддержки этот виджет начинает некорректно работать с AppBarLayout (Pull-to-refresh срабатывает еще до полного раскрытия AppBarLayout и мешает его прокрутке вниз).

Более того, однажды Google исправил этот баг, а потом… вернул его обратно.

И опять же.

В общем, в нашем примере мы не будем трогать этот виджет, а создадим кнопку в меню и свой простой 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-код фрагмента немного сложнее, поэтому приведем именно его.

МоделиСписокФрагмент

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 загружаем разметку фрагмента, восстанавливаем состояние (т.е.

    список в данных и статус загрузки/не загрузки) и, проверив, что наш список с данными пуст или выводим индикатор в процессе загрузки и подписываемся на ПоведениеСубъект.

  • В методе 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 Теги: #Android #java #rxjava #rxandroid #Retrofit #github #java #Разработка мобильных приложений #Разработка Android

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.