Puede actualizar con OKHttp usar datos de caché cuando no está conectado

Intento usar Retrofit y OKHttp para almacenar en caché las respuestas HTTP. Seguí esta esencia y terminé con este código:

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

Y este es MyApi con los encabezados de 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 callback ); 

Primero lo solicito en línea y verifico los archivos de caché. La respuesta JSON correcta y los encabezados están ahí. Pero cuando bash solicitar sin conexión, siempre obtengo RetrofitError UnknownHostException . ¿Hay algo más que deba hacer para que Retrofit lea la respuesta del caché?

EDITAR: desde OKHttp 2.0.x HttpResponseCache es Cache , setResponseCache está setCache

Editar para Retrofit 2.x:

OkHttp Interceptor es la forma correcta de acceder al caché cuando está fuera de línea:

1) Crear 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) Configurar el cliente:

 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) Agregar cliente para la actualización

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

También verifique la respuesta de @kosiara – Bartosz Kosarzycki . Es posible que deba eliminar algún encabezado de la respuesta.


OKHttp 2.0.x (Verifique la respuesta original):

Como OKHttp 2.0.x HttpResponseCache es Cache , setResponseCache es setCache . Entonces deberías setCache esta manera:

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

Respuesta Original:

Resulta que la respuesta del servidor debe tener Cache-Control: public para hacer que OkClient lea de la caché.

Además, si desea solicitar desde la red cuando esté disponible, debe agregar Cache-Control: max-age=0 encabezado de solicitud. Esta respuesta muestra cómo hacerlo parametrizado. Así es como lo usé:

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

Todos los indicadores anteriores no funcionaron para mí. Intenté implementar el caché sin conexión en la actualización 2.0.0-beta2 . okHttpClient.networkInterceptors() un interceptor usando el método okHttpClient.networkInterceptors() pero recibí java.net.UnknownHostException cuando traté de usar la memoria caché fuera de línea. Resultó que también tuve que agregar okHttpClient.interceptors() .

El problema era que el caché no se había escrito en el almacenamiento flash porque el servidor devolvía Pragma:no-cache que evita que OkHttp almacene la respuesta. La caché sin conexión no funcionó incluso después de modificar los valores del encabezado de la solicitud. Después de un poco de prueba y error, conseguí que la memoria caché funcionara sin modificar el lado del back-end al eliminar pragma de la respuesta en lugar de la solicitud – 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> getRestData(); } 

Mi solución:

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

Basándome en la respuesta de @kosiara-bartosz-kasarzycki, creé un proyecto de muestra que se carga correctamente desde la memoria-> disco-> red usando retrofit, okhttp, rxjava y guava. https://github.com/digitalbuddha/StoreDemo

La respuesta es SÍ, basada en las respuestas anteriores, comencé a escribir pruebas unitarias para verificar todos los casos de uso posibles:

  • Usar caché cuando no estés conectado
  • Utilice la respuesta en caché primero hasta que caduque, luego red
  • Primero use la red y la memoria caché para algunas solicitudes
  • No almacenar en caché para algunas respuestas

Creé una pequeña ayuda lib para configurar fácilmente el caché OKHttp, puede ver el examen de unidad relacionado aquí en Github: https://github.com/ncornette/OkCacheControl/blob/master/okcache-control/src/test/java/com/ ncornette / cache / OkCacheControlTest.java

Unittest que demuestra el uso de caché cuando no está conectado:

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

Como puede ver, la memoria caché se puede usar incluso si ha expirado. Espero que ayude

Caché con Retrofit2 y 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(); 

Método estático de 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(); } 

A continuación, solo agregue el cliente al generador de modificaciones:

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

Fuente original: https://newfivefour.com/android-retrofit2-okhttp3-cache-network-request-offline.html