Страницы

Поиск по вопросам

четверг, 25 октября 2018 г.

Как правильно пересоздать закэшированный Observable используемый вместе с Retrofit?

Дано:
API, возвращающее список с данными в формате JSON
Задача:
Получить эти данные силами Retrofit+RxJava
Проблема:
Необходимо сделать изначально один запрос и не дублировать его, если экран будет повёрнут до окончания задачи. Также нужно иметь возможность перезапустить задачу.
Что получилось:
Первое я решил, сделав Singlton и закешировав единственный экземпляр Observable с помощью cache().
Второе - полным пересозданием объекта Retrofit (1), экземпляра retrofit-интерфейса (2) и самого Observable(3). Если 1 и 2 не сделать - 3 - остаётся прежним и возвращает закэшированные данные, вместо новых.
Вопрос:
Использованный мной способ перезапуска задачи получения данных выглядит плохо. Как лучше/правильнее пересоздать Observable?

Синглтон для получения/пересоздания Observalbe
public class SingltonRetrofit { private static RxJavaCallAdapterFactory rxAdapter = RxJavaCallAdapterFactory.createWithScheduler(Schedulers.io());
private static Gson gson = new GsonBuilder().create();
private static Retrofit retrofit = new Retrofit.Builder() .baseUrl(Const.BASE_URL) .addConverterFactory(GsonConverterFactory.create(gson)) .addCallAdapterFactory(rxAdapter) .build();
private static GetModels apiService = retrofit.create(GetModels.class); private static Observable> observableModelsList;
public static void reset() { retrofit = new Retrofit.Builder() .baseUrl(Const.BASE_URL) .addConverterFactory(GsonConverterFactory.create(gson)) .addCallAdapterFactory(rxAdapter) .build(); apiService = retrofit.create(GetModels.class); observableModelsList = null; }
public static Observable> getModelsObservable() { if (observableModelsList == null) { observableModelsList = apiService.getModelsList().cache(); } return observableModelsList; } }
P.S.
Этот же вопрос на английском: How to recreate or reset cached Observable, used with Retrofit to get new data?


Ответ

В итоге сделал так:
Как верно написал @mit, метод cache() кэшировал запрос и пересоздание observable в итоге не перезапускало сетевой запрос. При этом в доках и в интернетах я нигде сему упоминания не находил. Видать это как-то связано с внутренней логикой связки Retrofit+OkHttp При этом как верно предложил @Yura Ivanov, потребовался BehaviorSubject На него подписывается фрагмент и его же можно пересоздать в случае нужды в свежих данных (без пересоздания будет отданы последние данные). При этом при каждом создании/пересоздании BehaviorSubject создаётся Subscriber для получения данных из сети через Observable, создаваемый Retrofit-ом. И он в onError и в onNext вызывает соответствующие методы у BehaviorSubject. При этом не транслируя onComplete, т.к. в этом случае может произойти ситуация, когда данные придут в процессе пересоздания фрагмента и фрагмент получит только последнее событие BehaviorSubject, т.е. событие завершения последовательности, вместо последних полученных данных. Т.е. соединять подпиской напрямую BehaviorSubject и Observable, получающий сетевые данные не стоит. Итого все требования соблюдены: При поворотах экрана будет запущена всего однажды задача на скачивание данных, фрагмент получит данные (или сообщение об ошибке) в любом случае и пересоздавать объекты Retrofita-а не нужно.
Итоговый синглтон:
public class SingltonRetrofitNew { private static RxJavaCallAdapterFactory rxAdapter = RxJavaCallAdapterFactory.createWithScheduler(Schedulers.io());
private static Gson gson = new GsonBuilder().create();
private static Retrofit retrofit = new Retrofit.Builder() .baseUrl(Const.BASE_URL) .addConverterFactory(GsonConverterFactory.create(gson)) .addCallAdapterFactory(rxAdapter) .build();
private static GetModels apiService = retrofit.create(GetModels.class);
private static BehaviorSubject> observableModelsList; private static Observable> observable = apiService.getModelsList(); private static Subscription subscription;
private SingltonRetrofitNew() { }
public static void resetObservable() { observableModelsList = BehaviorSubject.create();
if (subscription != null && !subscription.isUnsubscribed()) { subscription.unsubscribe(); } subscription = observable.subscribe(new Subscriber>() { @Override public void onCompleted() { //do nothing }
@Override public void onError(Throwable e) { observableModelsList.onError(e); }
@Override public void onNext(ArrayList hotels) { observableModelsList.onNext(hotels); } }); }
public static Observable> getModelsObservable() { if (observableModelsList == null) { resetObservable(); } return observableModelsList; } }
Сокращённый фрагмент:
public class FragmentsList extends Fragment { private static final String TAG = FragmentList.class.getSimpleName(); private Subscription subscription; private RecyclerView recyclerView; private SwipeRefreshLayout swipeRef; private ArrayList models = new ArrayList<>(); private boolean isLoading;
@Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment, container, false); //init views recyclerView = (RecyclerView) v.findViewById(R.id.recycler); swipeRef = (SwipeRefreshLayout) v.findViewById(R.id.swipe_ref); swipeRefreshLayout.setOnRefreshListener(new OnRefreshListener() { @Override public void onRefresh() { SingltonRetrofitNew.reset(); getModelsList(); } });
if (savedInstanceState != null) { models = savedInstanceState.getParcelableArrayList(Const.KEY_MODELS); isLoading = savedInstanceState.getBoolean(Const.KEY_IS_LOADING); }
if (models.size() == 0 || isLoading) { getModelsList(); } //TODO show saved data if is
return v; }
@Override public void onDestroy() { super.onDestroy(); if (subscription != null && !subscription.isUnsubscribed()) { subscription.unsubscribe(); } }
private void getModelsList() { isLoading = true; swipeRef.setRefreshing(true); if (subscription != null && !subscription.isUnsubscribed()) { subscription.unsubscribe(); } subscription = SingltonRetrofitNew.getModelsObservable(). subscribeOn(Schedulers.io()). observeOn(AndroidSchedulers.mainThread()). subscribe(new Subscriber>() { @Override public void onCompleted() { Log.d(TAG, "onCompleted"); }
@Override public void onError(Throwable e) { Log.d(TAG, "onError", e); isLoading = false; swipeRef.setRefreshing(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) { SingltonRetrofitNew.reset(); getModelsList(); } }) .show(); }
@Override public void onNext(ArrayList newModels) { isLoading = false; swipeRef.setRefreshing(false); models.clear(); models.addAll(newModels); //TODO show data } }); }
@Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelableArrayList(Const.KEY_MODELS, models); outState.putBoolean(Const.KEY_IS_LOADING, isLoading); } }
Всё вместе на gitHub: RxRetrofitAndScreenOrientation
Статья на ХабраХабр про решение: Используем RxJava и Retrofit на Android, учитывая поворот экрана

Комментариев нет:

Отправить комментарий