Страницы

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

воскресенье, 15 декабря 2019 г.

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

#java #android #retrofit #rxjava #rxandroid


Дано:

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?
    


Ответы

Ответ 1



Не претендую на лучшую реализацию, но... Лично я у себя во избежания повторных запросов держу данные отдельно от любой активити, дабы не захламлять код. Есть некий класс, который всегда создается в памяти при старте приложения к примеру пусть будет Model public class Model { public final MyData mayData; public Model(RestApi mRestApi) { //мне удобно передовать сразу интерфейс апи mayData = new MyData(mRestApi); } } Старт его происходит в классе наследованного от Application. Класс MyData - обычный класс в котором описаны запросы для конкретного экрана или типа данных. public class MyData { public MainPageInfo(RestApi retrofit) { super(retrofit); } public void getData() { mRestApi.getData().enqueue(new Callback>() { @Override public void onResponse(Call> call, Response> response) { } @Override public void onFailure(Call> call, Throwable t) { setError(t); } }); } } Для удобства доступа к модели я храню на нее ссылки в классах унаследованных от фрагмента или активити, в свою очередь их ставлю в наследники для нужных мне вью. Как итог - во время переворота дестроятся сама вьюшка, но не данные и их легко попросить у класса MyData с просто проверкой на null (делать запрос если данных нет). Конечно реализация не совершенна, как и здесь я описал не весь код (кроме всего есть модель слушателей)). Но я почему то думаю для вас это не проблема и вы сами решите подойдет вам этот метод или нет. Единственный недостаток - если дестроется все приложение, то и данные тоже. Зато можно обратиться к данным из любого места =), Если что пишите на почту ) расскажу подробней )

Ответ 2



В итоге сделал так: Как верно написал @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, учитывая поворот экрана

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

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