可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I\'m trying to use Retrofit & OKHttp to cache HTTP responses. I followed this gist and, ended up with this code:
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
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?
EDIT:
Since OKHttp 2.0.x HttpResponseCache
is Cache
, setResponseCache
is setCache
回答1:
Edit for Retrofit 2.x:
OkHttp Interceptor is the right way to access cache when offline:
1) Create Interceptor:
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:
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
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.
OKHttp 2.0.x (Check the original answer):
Since OKHttp 2.0.x HttpResponseCache
is Cache
, setResponseCache
is setCache
. So you should setCache
like this:
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: public
to make OkClient
to read from cache.
Also If you want to request from network when available, you should add Cache-Control: max-age=0
request header. This answer shows how to do it parameterized. This is how I used it:
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);
}
}
});
回答2:
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.UnknownHostException
when I tried to use the cache offline. It turned out that I had to add okHttpClient.interceptors()
as well.
The problem was that cache wasn\'t written to flash storage because the server returned Pragma:no-cache
which 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\");
Retrofit: 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();
}
回答3:
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();
}
回答4:
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
回答5:
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
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.
回答6:
Cache with Retrofit2 and 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:
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