Java 离线时可以使用 OKHttp 进行 Retrofit 使用缓存数据

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/23429046/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-13 22:57:54  来源:igfitidea点击:

Can Retrofit with OKHttp use cache data when offline

javacachingretrofitokhttpoffline-caching

提问by osrl

I'm trying to use Retrofit & OKHttp to cache HTTP responses. I followed this gistand, ended up with this code:

我正在尝试使用 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);

And this is MyApi with the Cache-Control headers

这是带有 Cache-Control 标头的 MyApi

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

First I request online and check the cache files. The correct JSON response and headers are there. But when I try to request offline, I always get RetrofitError UnknownHostException. Is there anything else I should do to make Retrofit read the response from cache?

首先我在线请求并检查缓存文件。那里有正确的 JSON 响应和标头。但是当我尝试离线请求时,我总是得到RetrofitError UnknownHostException. 还有什么我应该做的让 Retrofit 从缓存中读取响应吗?

EDIT:Since OKHttp 2.0.x HttpResponseCacheis Cache, setResponseCacheis setCache

编辑:由于 OKHttp 2.0.xHttpResponseCacheCachesetResponseCachesetCache

采纳答案by osrl

Edit for Retrofit 2.x:

针对 Retrofit 2.x 进行编辑:

OkHttp Interceptor is the right way to access cache when offline:

OkHttp Interceptor 是离线访问缓存的正确方式:

1) Create 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) Setup client:

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) Add client to retrofit

3) 添加客户端进行改造

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

Also check @kosiara - Bartosz Kosarzycki's answer. You may need to remove some header from the response.

另请查看@kosiara-Bartosz Kosarzycki回答。您可能需要从响应中删除一些标头。



OKHttp 2.0.x (Check the original answer):

OKHttp 2.0.x(查看原答案):

Since OKHttp 2.0.x HttpResponseCacheis Cache, setResponseCacheis setCache. So you should setCachelike this:

由于 OKHttp 2.0.xHttpResponseCacheCachesetResponseCachesetCache。所以你应该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);


Original Answer:

原答案:

It turns out that server response must have Cache-Control: publicto make OkClientto read from cache.

事实证明,必须Cache-Control: public做出服务器响应才能OkClient从缓存中读取。

Also If you want to request from network when available, you should add Cache-Control: max-age=0request header. This answershows how to do it parameterized. This is how I used it:

另外如果你想在可用时从网络请求,你应该添加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);
            }
        }
});

回答by kosiara - Bartosz Kosarzycki

All of the anwsers above did not work for me. I tried to implement offline cache in retrofit 2.0.0-beta2. I added an interceptor using okHttpClient.networkInterceptors()method but received java.net.UnknownHostExceptionwhen I tried to use the cache offline. It turned out that I had to add okHttpClient.interceptors()as well.

上面的所有答案都不适合我。我尝试在改造 2.0.0-beta2 中实现离线缓存。我添加了一个使用okHttpClient.networkInterceptors()方法的拦截器,但java.net.UnknownHostException在我尝试离线使用缓存时收到。事实证明,我也必须添加okHttpClient.interceptors()

The problem was that cache wasn't written to flash storage because the server returned Pragma:no-cachewhich prevents OkHttp from storing the response. Offline cache didn't work even after modifying request header values. After some trial-and-error I got the cache to work without modifying the backend side by removing pragma from reponse instead of the request - response.newBuilder().removeHeader("Pragma");

问题是缓存没有写入闪存,因为服务器返回Pragma:no-cache阻止 OkHttp 存储响应。即使修改请求标头值后,离线缓存也不起作用。经过一些反复试验后,我通过从响应中删除 pragma 而不是请求,在不修改后端的情况下使缓存工作 -response.newBuilder().removeHeader("Pragma");

Retrofit: 2.0.0-beta2; OkHttp: 2.5.0

改造: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();

}

回答by FriendlyMikhail

building on @kosiara-bartosz-kasarzycki's answer, I created a sample project that properly loads from memory->disk->network using retrofit, okhttp, rxjava and guava. https://github.com/digitalbuddha/StoreDemo

基于@kosiara-bartosz-kasarzycki 的回答,我创建了一个示例项目,该项目使用改造、okhttp、rxjava 和番石榴从内存->磁盘->网络正确加载。 https://github.com/digitalbuddha/StoreDemo

回答by Arkadiusz Konior

My solution:

我的解决方案:

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

回答by Владимир Широков

Cache with Retrofit2 and OkHTTP3:

使用 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() static method:

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

Then just add client to the retrofit builder:

然后只需将客户端添加到改造构建器:

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

Original source: https://newfivefour.com/android-retrofit2-okhttp3-cache-network-request-offline.html

原始来源:https: //newfivefour.com/android-retrofit2-okhttp3-cache-network-request-offline.html

回答by Nicolas Cornette

The answer is YES, based on the above answers, I started writing unit tests to verify all possible use cases :

答案是肯定的,基于上述答案,我开始编写单元测试来验证所有可能的用例:

  • Use cache when offline
  • Use cached response first until expired, then network
  • Use network first then cache for some requests
  • Do not store in cache for some responses
  • 离线时使用缓存
  • 首先使用缓存响应直到过期,然后网络
  • 首先使用网络然后缓存一些请求
  • 不要将某些响应存储在缓存中

I built a small helper lib to configure OKHttp cache easily, you can see the related unittest here on Github : https://github.com/ncornette/OkCacheControl/blob/master/okcache-control/src/test/java/com/ncornette/cache/OkCacheControlTest.java

我构建了一个小助手库来轻松配置 OKHttp 缓存,您可以在 Github 上看到相关的单元测试:https: //github.com/ncornette/OkCacheControl/blob/master/okcache-control/src/test/java/com/ ncornette/缓存/OkCacheControlTest.java

Unittest that demonstrates the use of cache when offline :

离线时演示缓存使用的单元测试:

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

As you can see, cache can be used even if it has expired. Hope it will help.

如您所见,即使缓存已过期,也可以使用它。希望它会有所帮助。