Может дооснащаться OKHttp использовать данные кеша в автономном режиме

Я пытаюсь использовать Retrofit & OKHttp для кеширования HTTP-ответов. Я следил за этой сутью и получил этот код:

File httpCacheDirectory = new File(context.getCacheDir(), "responses");

HttpResponseCache httpResponseCache = null;
try {
     httpResponseCache = new HttpResponseCache(httpCacheDirectory, 10 * 1024 * 1024);
} catch (IOException e) {
     Log.e("Retrofit", "Could not create http cache", e);
}

OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setResponseCache(httpResponseCache);

api = new RestAdapter.Builder()
          .setEndpoint(API_URL)
          .setLogLevel(RestAdapter.LogLevel.FULL)
          .setClient(new OkClient(okHttpClient))
          .build()
          .create(MyApi.class);

А это MyApi с заголовками Cache-Control

public interface MyApi {
   @Headers("Cache-Control: public, max-age=640000, s-maxage=640000 , max-stale=2419200")
   @GET("/api/v1/person/1/")
   void requestPerson(
           Callback<Person> callback
   );

Сначала я запрашиваю онлайн и проверяю файлы кеша. Правильный ответ JSON и заголовки есть. Но когда я пытаюсь запросить офлайн, всегда получаю RetrofitError UnknownHostException. Что еще мне нужно сделать, чтобы Retrofit прочитал ответ из кеша?

РЕДАКТИРОВАТЬ: Поскольку OKHttp 2.0.x HttpResponseCache равно Cache, setResponseCache равно setCache


person osrl    schedule 02.05.2014    source источник
comment
Отвечает ли вызываемый вами сервер соответствующим заголовком Cache-Control?   -  person Hassan Ibraheem    schedule 03.05.2014
comment
он возвращает это Cache-Control: s-maxage=1209600, max-age=1209600 Я не знаю, достаточно ли этого.   -  person osrl    schedule 04.05.2014
comment
Похоже, что ключевое слово public было необходимо в заголовке ответа, чтобы он работал в автономном режиме. Но эти заголовки не позволяют OkClient использовать сеть, когда она доступна. Есть ли способ установить политику / стратегию кеширования для использования сети, когда она доступна?   -  person osrl    schedule 06.05.2014
comment
Я не уверен, сможете ли вы сделать это в том же запросе. Вы можете проверить соответствующий CacheControl и Cache-Control заголовки. Если такого поведения нет, я бы, вероятно, выбрал два запроса: только кешированный (только если-кешированный), а затем сетевой (max-age = 0) один.   -  person Hassan Ibraheem    schedule 06.05.2014
comment
это было первое, что пришло мне в голову. Я провел несколько дней в этом CacheControl и CacheStrategy. Но идея двух запросов не имела особого смысла. Если max-stale + max-age передан, он делает запрос из сети. Но я хочу установить max-stale в неделю. Это заставляет его читать ответ из кеша, даже если сеть доступна.   -  person osrl    schedule 06.05.2014
comment
Разве s-maxage не только для сервера, а не для клиентов?   -  person McNinja    schedule 16.10.2014
comment
Также проверьте заголовки с сервера, как здесь: stackoverflow.com/questions/31321963/   -  person Dawid Drozd    schedule 24.07.2015
comment
rtm: https://square.github.io/okhttp/4.x/okhttp/okhttp3/-cache/#force-a-cache-response   -  person ccpizza    schedule 22.07.2020


Ответы (6)


Изменить для Retrofit 2.x:

OkHttp Interceptor - это правильный способ доступа к кешу в автономном режиме:

1) Создать перехватчик:

private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
    @Override public Response intercept(Chain chain) throws IOException {
        Response originalResponse = chain.proceed(chain.request());
        if (Utils.isNetworkAvailable(context)) {
            int maxAge = 60; // read from cache for 1 minute
            return originalResponse.newBuilder()
                    .header("Cache-Control", "public, max-age=" + maxAge)
                    .build();
        } else {
            int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
            return originalResponse.newBuilder()
                    .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                    .build();
        }
    }

2) Настроить клиент:

OkHttpClient client = new OkHttpClient();
client.networkInterceptors().add(REWRITE_CACHE_CONTROL_INTERCEPTOR);

//setup cache
File httpCacheDirectory = new File(context.getCacheDir(), "responses");
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(httpCacheDirectory, cacheSize);

//add cache to the client
client.setCache(cache);

3) Добавить клиента для модернизации

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(client)
        .addConverterFactory(GsonConverterFactory.create())
        .build();

Также проверьте ответ @kosiara - Bartosz Kosarzycki. Возможно, вам придется удалить заголовок из ответа.


OKHttp 2.0.x (проверьте исходный ответ):

Поскольку OKHttp 2.0.x HttpResponseCache это Cache, setResponseCache это setCache. Итак, вы должны setCache вот так:

        File httpCacheDirectory = new File(context.getCacheDir(), "responses");

        Cache cache = null;
        try {
            cache = new Cache(httpCacheDirectory, 10 * 1024 * 1024);
        } catch (IOException e) {
            Log.e("OKHttp", "Could not create http cache", e);
        }

        OkHttpClient okHttpClient = new OkHttpClient();
        if (cache != null) {
            okHttpClient.setCache(cache);
        }
        String hostURL = context.getString(R.string.host_url);

        api = new RestAdapter.Builder()
                .setEndpoint(hostURL)
                .setClient(new OkClient(okHttpClient))
                .setRequestInterceptor(/*rest of the answer here */)
                .build()
                .create(MyApi.class);

Исходный ответ:

Оказывается, ответ сервера должен иметь Cache-Control: public, чтобы OkClient читал из кеша.

Также, если вы хотите запросить из сети, когда она доступна, вы должны добавить заголовок запроса Cache-Control: max-age=0. В этом ответе показано, как сделать это параметризованным. Вот как я это использовал:

RestAdapter.Builder builder= new RestAdapter.Builder()
   .setRequestInterceptor(new RequestInterceptor() {
        @Override
        public void intercept(RequestFacade request) {
            request.addHeader("Accept", "application/json;versions=1");
            if (MyApplicationUtils.isNetworkAvailable(context)) {
                int maxAge = 60; // read from cache for 1 minute
                request.addHeader("Cache-Control", "public, max-age=" + maxAge);
            } else {
                int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
                request.addHeader("Cache-Control", 
                    "public, only-if-cached, max-stale=" + maxStale);
            }
        }
});
person osrl    schedule 06.05.2014
comment
(Мне было интересно, почему это не сработало; оказалось, что я забыл установить фактический кеш для использования OkHttpClient. См. Код в вопросе или в этот ответ.) - person Jonik; 10.03.2015
comment
Небольшой совет: HttpResponseCache has been renamed to Cache.** Install it with OkHttpClient.setCache(...) instead of OkHttpClient.setResponseCache(...). - person Henrique de Sousa; 28.05.2015
comment
Да вы правы, я уже добавил это к вопросу. Я тоже должен добавить это к ответу - person osrl; 29.05.2015
comment
Я не получаю вызов перехватчика, когда сеть недоступна. Я не уверен, как может сработать условие, когда сеть недоступна. Я что-то упустил? - person Androidme; 12.04.2016
comment
Зачем нужно явно указывать директиву public? По умолчанию он приватный? - person toobsco42; 16.06.2016
comment
@ toobsco42 какой? Заголовок? - person osrl; 16.06.2016
comment
@osrl Я за заголовок Cache-Control. - person toobsco42; 16.06.2016
comment
Я не очень разбираюсь в заголовках. Для меня это был метод проб и ошибок. Я сказал в комментарии: похоже, что ключевое слово public нужно было в заголовке ответа, чтобы он работал в автономном режиме. Так что по умолчанию он был приватным. Было бы здорово, если бы кто-нибудь это объяснил. - person osrl; 16.06.2016
comment
if (Utils.isNetworkAvailable(context)) правильный или он должен быть перевернут, т.е. if (!Utils.isNetworkAvailable(context))? - person ericn; 22.07.2016
comment
Я использую Retrofit 2.1.0, и когда телефон не в сети, public okhttp3.Response intercept(Chain chain) throws IOException никогда не звонит, он звонит только тогда, когда я в сети - person ericn; 22.07.2016
comment
У меня есть api rails, и он уже устанавливает заголовки управления кешем для ответа. По-прежнему не работает. Я использую дооснащение 2.1.0 - person lightsaber; 30.07.2016
comment
Извините, это работает. Разрешение на хранение было отключено. - person lightsaber; 30.07.2016
comment
Данные доступны в автономном режиме здесь только на время «max-age», через 60 секунд они не могут быть загружены. @StarWars есть идеи? - person Boy; 11.08.2016
comment
@ Boy На самом деле все еще не работает. Он работал на one +, но не на nexus 5. Я думаю, это проблема с устройствами nexus. - person lightsaber; 11.08.2016
comment
@StarWars Я сдался и вместо этого начал кэшировать сами данные - person Boy; 15.08.2016
comment
Я потратил много времени, потому что отправлял дату в качестве параметра, и из-за этого запрос не мог быть кэширован. Хотя это может показаться глупым, я подумал, что было бы полезно, если бы я написал это в ответе, который работает, так что вот он. - person Oriol; 08.11.2016
comment
@osrl Почему мы проверяем интернет-соединение при перехвате ответа? Спасибо, хотя это решение работает для меня, но я не понимаю. - person Rohit Bandil; 20.01.2017
comment
@RohitBandil Это хороший вопрос. Возможно, вы правы, перехватывая запрос вместо ответа. Я добавил заголовки к запросу в исходном ответе. Я действительно не помню, зачем я это сделал. - person osrl; 23.01.2017
comment
@osrl, какова цель добавления заголовка в запрос, когда Интернет недоступен в вашем исходном ответе. В любом случае это вызовет исключение сети недоступно. - person Rohit Bandil; 01.02.2017
comment
У меня такая же проблема, как описано в @ericn. Использование приведенного выше кода для перехватчика в Retrofit 2.3.0 дает мне java.net.ConnectException, когда я не в сети. Исключение уже выброшено в строке Response originalResponse = chain.proceed(chain.request());. Следовательно, я даже не могу проверить, подключено ли устройство к сети или нет. Кто-нибудь знает, как это исправить? - person user2350644; 22.01.2019
comment
Обновление моего комментария. Используя библиотеку, опубликованную в анкете ниже @Nicolas Cornette: github.com/ncornette/OkCacheControl, он работает, и кеш используется, когда я не в сети. Спасибо за это. Но все же мне было бы интересно, почему вышеупомянутый перехватчик у меня не работает? - person user2350644; 22.01.2019

Все вышеперечисленные ответы у меня не работали. Я попытался реализовать автономный кеш в retrofit 2.0.0-beta2. Я добавил перехватчик, используя метод okHttpClient.networkInterceptors(), но получил java.net.UnknownHostException, когда попытался использовать кеш в автономном режиме. Оказалось, что пришлось добавить и okHttpClient.interceptors().

Проблема заключалась в том, что кеш не был записан во флеш-хранилище, потому что сервер вернул Pragma:no-cache, что не позволяет OkHttp сохранить ответ. Автономный кеш не работал даже после изменения значений заголовка запроса. После некоторых проб и ошибок я заставил кеш работать без изменения серверной части, удалив прагму из ответа вместо запроса - response.newBuilder().removeHeader("Pragma");

Модернизация: 2.0.0-beta2; OkHttp: 2.5.0

OkHttpClient okHttpClient = createCachedClient(context);
Retrofit retrofit = new Retrofit.Builder()
        .client(okHttpClient)
        .baseUrl(API_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build();
service = retrofit.create(RestDataResource.class);

...

private OkHttpClient createCachedClient(final Context context) {
    File httpCacheDirectory = new File(context.getCacheDir(), "cache_file");

    Cache cache = new Cache(httpCacheDirectory, 20 * 1024 * 1024);
    OkHttpClient okHttpClient = new OkHttpClient();
    okHttpClient.setCache(cache);
    okHttpClient.interceptors().add(
            new Interceptor() {
                @Override
                public Response intercept(Chain chain) throws IOException {
                    Request originalRequest = chain.request();
                    String cacheHeaderValue = isOnline(context) 
                        ? "public, max-age=2419200" 
                        : "public, only-if-cached, max-stale=2419200" ;
                    Request request = originalRequest.newBuilder().build();
                    Response response = chain.proceed(request);
                    return response.newBuilder()
                        .removeHeader("Pragma")
                        .removeHeader("Cache-Control")
                        .header("Cache-Control", cacheHeaderValue)
                        .build();
                }
            }
    );
    okHttpClient.networkInterceptors().add(
            new Interceptor() {
                @Override
                public Response intercept(Chain chain) throws IOException {
                    Request originalRequest = chain.request();
                    String cacheHeaderValue = isOnline(context) 
                        ? "public, max-age=2419200" 
                        : "public, only-if-cached, max-stale=2419200" ;
                    Request request = originalRequest.newBuilder().build();
                    Response response = chain.proceed(request);
                    return response.newBuilder()
                        .removeHeader("Pragma")
                        .removeHeader("Cache-Control")
                        .header("Cache-Control", cacheHeaderValue)
                        .build();
                }
            }
    );
    return okHttpClient;
}

...

public interface RestDataResource {

    @GET("rest-data") 
    Call<List<RestItem>> getRestData();

}
person kosiara - Bartosz Kosarzycki    schedule 14.12.2015
comment
Похоже, ваши interceptors () и networkInterceptors () идентичны. Зачем вы это продублировали? - person toobsco42; 16.06.2016
comment
разные типы перехватчиков читайте здесь. https://github.com/square/okhttp/wiki/Interceptors - person Rohit Bandil; 20.01.2017
comment
да, но они оба делают одно и то же, поэтому я почти уверен, что одного перехватчика должно хватить, верно? - person Ovidiu Latcu; 13.02.2018
comment
есть ли конкретная причина не использовать один и тот же экземпляр перехватчика как для .networkInterceptors().add(), так и для interceptors().add()? - person ccpizza; 22.07.2020

Мое решение:

private BackendService() {

    httpCacheDirectory = new File(context.getCacheDir(),  "responses");
    int cacheSize = 10 * 1024 * 1024; // 10 MiB
    Cache cache = new Cache(httpCacheDirectory, cacheSize);

    httpClient = new OkHttpClient.Builder()
            .addNetworkInterceptor(REWRITE_RESPONSE_INTERCEPTOR)
            .addInterceptor(OFFLINE_INTERCEPTOR)
            .cache(cache)
            .build();

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl("https://api.backend.com")
            .client(httpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .build();

    backendApi = retrofit.create(BackendApi.class);
}

private static final Interceptor REWRITE_RESPONSE_INTERCEPTOR = chain -> {
    Response originalResponse = chain.proceed(chain.request());
    String cacheControl = originalResponse.header("Cache-Control");

    if (cacheControl == null || cacheControl.contains("no-store") || cacheControl.contains("no-cache") ||
            cacheControl.contains("must-revalidate") || cacheControl.contains("max-age=0")) {
        return originalResponse.newBuilder()
                .header("Cache-Control", "public, max-age=" + 10)
                .build();
    } else {
        return originalResponse;
    }
};

private static final Interceptor OFFLINE_INTERCEPTOR = chain -> {
    Request request = chain.request();

    if (!isOnline()) {
        Log.d(TAG, "rewriting request");

        int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
        request = request.newBuilder()
                .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                .build();
    }

    return chain.proceed(request);
};

public static boolean isOnline() {
    ConnectivityManager cm = (ConnectivityManager) MyApplication.getApplication().getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo netInfo = cm.getActiveNetworkInfo();
    return netInfo != null && netInfo.isConnectedOrConnecting();
}
person Arkadiusz Konior    schedule 22.04.2016
comment
у меня не работает ... Получение неудовлетворительного запроса 504 (только в кешировании) - person zacharia; 14.12.2016
comment
Мне помогает только твое решение, спасибо большое. Потратьте 2 дня на прокрутку вниз - person Ярослав Мовчан; 05.03.2017
comment
ага, единственное рабочее решение в моем случае. (Дооснащение 1.9.x + ok http3) - person Hoang Nguyen Huu; 20.05.2017
comment
Работает с дооснащением RETROFIT_VERSION = 2.2.0 OKHTTP_VERSION = 3.6.0 - person Tadas Valaitis; 21.07.2017
comment
как добавить builder.addheader () в этот метод для доступа к api с авторизацией? - person Abhilash; 26.10.2017

Ответ ДА, основываясь на приведенных выше ответах, я начал писать модульные тесты, чтобы проверить все возможные варианты использования:

  • Использовать кеш в автономном режиме
  • Сначала используйте кешированный ответ, пока не истечет срок его действия, затем сеть
  • Сначала используйте сеть, а затем кешируйте некоторые запросы
  • Не хранить в кеше некоторые ответы

Я создал небольшую вспомогательную библиотеку для легкой настройки кеша OKHttp, вы можете увидеть соответствующий unittest здесь, на Github: https://github.com/ncornette/OkCacheControl/blob/master/okcache-control/src/test/java/com/ncornette/cache/OkCacheControlTest.java

Unittest, демонстрирующий использование кеша в автономном режиме:

@Test
public void test_USE_CACHE_WHEN_OFFLINE() throws Exception {
    //given
    givenResponseInCache("Expired Response in cache", -5, MINUTES);
    given(networkMonitor.isOnline()).willReturn(false);

    //when
    //This response is only used to not block when test fails
    mockWebServer.enqueue(new MockResponse().setResponseCode(404));
    Response response = getResponse();

    //then
    then(response.body().string()).isEqualTo("Expired Response in cache");
    then(cache.hitCount()).isEqualTo(1);
}

Как видите, кеш можно использовать, даже если срок его действия истек. Надеюсь, это поможет.

person Nicolas Cornette    schedule 22.06.2016
comment
Ваша библиотека великолепна! Спасибо за вашу тяжелую работу. Библиотека: github.com/ncornette/OkCacheControl - person Hoang Nguyen Huu; 22.05.2017

основываясь на ответе @ kosiara-bartosz-kasarzycki, я создал образец проекта, который правильно загружается из памяти-> диска-> сети используя retrofit, okhttp, rxjava и guava. https://github.com/digitalbuddha/StoreDemo

person FriendlyMikhail    schedule 21.12.2015

Кэш с Retrofit2 и OkHTTP3:

OkHttpClient client = new OkHttpClient
  .Builder()
  .cache(new Cache(App.sApp.getCacheDir(), 10 * 1024 * 1024)) // 10 MB
  .addInterceptor(new Interceptor() {
    @Override public Response intercept(Chain chain) throws IOException {
      Request request = chain.request();
      if (NetworkUtils.isNetworkAvailable()) {
        request = request.newBuilder().header("Cache-Control", "public, max-age=" + 60).build();
      } else {
        request = request.newBuilder().header("Cache-Control", "public, only-if-cached, max-stale=" + 60 * 60 * 24 * 7).build();
      }
      return chain.proceed(request);
    }
  })
  .build();

Статический метод NetworkUtils.isNetworkAvailable ():

public static boolean isNetworkAvailable(Context context) {
        ConnectivityManager cm =
                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
        return activeNetwork != null &&
                activeNetwork.isConnectedOrConnecting();
    }

Затем просто добавьте клиента в конструктор модернизации:

Retrofit retrofit = new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .client(client)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();

Исходный источник: https://newfivefour.com/android-retrofit2-okhttp3-cache-network-request-offline.html

person Владимир Широков    schedule 29.05.2016
comment
при первой загрузке в автономном режиме происходит сбой! в противном случае он работает правильно - person Zafer Celaloglu; 15.06.2016
comment
это не работает для меня. Я скопировал его и попробовал после того, как попытался интегрировать его принцип, но у меня получилось заставить его работать. - person Boy; 11.08.2016
comment
App.sApp.getCacheDir () что это делает? - person Huzaifa Asif; 03.05.2020