Решил я однажды таки попробовать дико популярный нынче 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 работает относительно стабильно, засим берём её.
Переходим к коду. Вот какая структура проекта у меня получилась:
Разметка активити будет простой, вот она:
<?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-код же фрагмента сложнее немного, так что его точно приведём.
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)
Beanut
12.07.2016 14:49+3Мне одному режет глаз «Singlton»?
kuchanov
12.07.2016 17:49There are only two hard things in Computer Science: cache invalidation and naming things.
— Phil Karlton
Готов выслушать ваши предложения о именовании класса)Bringoff
12.07.2016 18:46Меня убивает именование в стиле ActivityMain. Не говоря уже о том, что так никто не пишет в коде и это непривычно, это еще и неправильно с точки зрения английского языка. А у вас все классы так обозваны.
kuchanov
12.07.2016 19:05-1Ну, тут, наверное, вопрос во вкусовых предпочтениях. Лично я не видел, вроде, конвенции прямо запрещающей именовать так как у меня. Лично я выбрал такой способ исключительно для упрощения автоподстановки — часто я не помню как у меня к-л фрагмент/активити точно называется, но точно знаю, что начинается название класса с Activity/Fragment. Вполне готов признать что это может быть неверный подход. Если у вас есть ссылка на более правильный принцип именования, то я бы с удовольствием её бы почитал)
Bringoff
12.07.2016 19:10AndroidStudio понимает автокомплит и с середины.
Ого, у вас еще и скобки с новой строки… Печаль-беда. Это уже точно конвенции не приветствуют.
Вот андроидовые конвенции неплохо описанные. https://github.com/ribot/android-guidelines/blob/master/project_and_code_guidelines.mdBringoff
12.07.2016 19:13Там сразу же:
the name of the class should end with the name of the component
kuchanov
12.07.2016 19:28-1Да, с середины понимает, но мне кажется удобным когда с начала) Ссылку посмотрел, спасибо, буду иметь в виду теперь. Правда странно, почему в случае с xml файлами рекомендуют начинать с fragment/activity, а вот в классах — наоборот…
А скобки… Мне просто удобнее так, тем более, что сменить все скобки можно одним shortCut-ом)meandnano
13.07.2016 09:08+1Классы вы можете организовывать в пакеты, а все XML с разметкой лежат в одной директории. Отсюда и разница в конвенции. XML файлы имеют префикс для большего порядка в res/layout.
kuchanov
13.07.2016 13:08Вы меня убедили) Исправил в статье и в репозитории именование классов/ресурсов и скобки
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 для большого количества входных данных давать не нужно):
DbListpublic 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(); } }
kuchanov
13.07.2016 13:27Спасибо за полезную информацию. Отвечаю по пунктам:
- MVP я ещё не пробовал, хотя много слышал. Судя по реакции на эту статью надо изучить. Просто изначально целью статьи было поделиться своим первым опытом с Rx и Retrofit в максимально простом и абстрактном примере.
- В БД ничего не сохранял, т.к. изначально (хоть это и не помянуто в статье по ряду причин) задача стояла в отображении данных с коротким сроком жизни, устаревающих в течении пары часов. Засим желательно при каждом запуске приложения получать данные именно из сети, а не сохранённые ранее, ибо они уже будут нерелевантны. А так — да, в одном из своих приложений так и делал — сначала в БД данные шли, потом уже из неё во View. Но это от конкретной задачи зависит.
- Code style и имена поправил и тут и в репозитории. Судя по реакции в комментариях всё было плохо, так что исправлюсь)
- Про преимущества Cursor над ArrayList бы послушал — могу предположить что он просто быстрее работает?
- Ещё раз спасибо за код в комментарии. Возможно, я попробую переделать в другой ветке проект в MVP стиле и напишу ещё одну статью)
withoutuniverse
13.07.2016 16:16При большом количестве входных данных есть большой шанс словить OutOfMemory, используя ArrayList, так как все данные хранятся в памяти.
Cursor лишен этого недостатка, так как подгрузка с носителя данных таблицы в память происходит по мере надобности, как и выгрузка.andreich
18.07.2016 09:29+1Это ж сколько данных должно быть, чтобы OOM схватить !?, это если исключит изображения(они вообще отдельная тему)
mefi100fell
15.07.2016 11:44+1Спасибо за обертку над Cursor.
А в каком месте закрывать Cursor? Наверное, нужно добавить метод close к DbList?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()) ...
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
mefi100fell
15.07.2016 20:01Понял, спасибо.
У меня как раз сегодня появилось место для использования этого wrapper-а.
Jukobob
14.07.2016 18:33-2Как же режет глаза Java после Kotlin. Тем более на таких тестовых примерах. Kotlin — лямбды из коробки, и это не единственный плюс.
andreich
18.07.2016 09:26Все уже написано до нас :) У меня еще с февраля в черновиках лежит статья с подобной темой, жаль, что руки не дошли ее дописать.
В общем странно, что вы не наткнулись на RxLoader, который в полное мере решает описанную задачу, Так же на том же SO я уже отвечал на подобный вопрос .kuchanov
18.07.2016 20:32Должен признаться, что я специально и не искал других решений этой задачи :) Мне просто захотелось взять Rx, Retrofit, их соединить и посмотреть что получится) А ссылка интересная, спасибо. Правда уж очень давно, похоже, не обновляли библиотеку, что некоторые сомнения вызывает...
Lucky_spirit
А чем не угодили Лоадеры в связке с Лоадер Менеджером? Я имею ввиду, Ретрофит запрос можно было бы делать в loadInBackground() и кэшировать его в самом Лоадере, а при повороте доставлять уже «кэшированный» результат. Есть очень хорошая статья у Ian Lake насчёт того, как кэшировать данные в Лоадере. Думаю, в данном случае она бы пригодилась. Да, и внутри самого Лоадера можно было бы подписаться на Rx.
kuchanov
Loader-ы я пробовал пару раз… Наверное я что-то в них не понял, но у меня так и не получилось в своё время добиться от них такой гибкости и лаконичности как с Rx сейчас. Ну и как я и писал в статье — Rx модный, захотелось его попробовать, тем более, что он не такой узкоспециализированный как Loader-ы и сейчас часто вакансии с его упоминанием встречаю.
9arret
До потому что лоадеры используют колбэки которые скажем так явно не улучшают ваш код. Rx же позволяет написать понятнее и временами лаконичнее. Да и с потоками проще. Непонятно только почему возникло желание писать свой велосипед, есть же, например, RxLifecycle
kuchanov
Слышал о RxLifecycle. Ещё раз её бегло посмотрел. Насколько я понял её использование в моём случае даст лишь возможность не отписываться вручную, самостоятельно вызывая unsubscribe(), но потребует лишнюю зависимость и нужду в расширении от RxFragment. Возможно я чего-то не понял, но, кмк, эта либа не будет серебряной пулей) Всё же статья не о избегании утечек из-за неотписанных подписчиков, а о получении данных вне зависимости от поворотов экрана.