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

148

Estoy tratando de 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 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
   );

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

EDITAR: ya que OKHttp 2.0.x HttpResponseCachees Cache, setResponseCacheessetCache

osrl
fuente
1
¿El servidor al que llama responde con un encabezado de Cache-Control apropiado?
Hassan Ibraheem
devuelve esto Cache-Control: s-maxage=1209600, max-age=1209600, no sé si es suficiente.
osrl
Parece que la publicpalabra clave era necesaria para estar en el encabezado de respuesta para que funcione sin conexión. Pero, estos encabezados no permiten que OkClient use la red cuando está disponible. ¿Hay alguna forma de establecer una política / estrategia de caché para usar la red cuando esté disponible?
osrl
No estoy seguro de si puedes hacer eso en la misma solicitud. Puede verificar la clase CacheControl relevante y los encabezados Cache-Control . Si no existe tal comportamiento, probablemente optaría por hacer dos solicitudes, una solicitud solo en caché (solo si está en caché), seguida de una red (edad máxima = 0).
Hassan Ibraheem
eso fue lo primero que me vino a la mente. Pasé días en esas clases de CacheControl y CacheStrategy . Pero la idea de dos solicitudes no tenía mucho sentido. Si max-stale + max-agese pasa, se solicita de la red. Pero quiero establecer max-stale a la semana. Esto hace que lea la respuesta del caché incluso si hay una red disponible.
osrl

Respuestas:

189

Editar para Retrofit 2.x:

OkHttp Interceptor es la forma correcta de acceder a la memoria caché cuando está desconectado:

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) Cliente de configuración:

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 actualizar

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

También puedes ver @kosiara - Bartosz Kosarzycki 's respuesta . Es posible que deba eliminar algún encabezado de la respuesta.


OKHttp 2.0.x (Verifique la respuesta original):

Desde OKHttp 2.0.x HttpResponseCachees Cache, setResponseCachees setCache. Entonces te debería setCachegustar esto:

        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: publicque hacer OkClientpara leer desde el caché.

Además, si desea solicitar desde la red cuando esté disponible, debe agregar el Cache-Control: max-age=0encabezado 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);
            }
        }
});
osrl
fuente
(Me preguntaba por qué esto no funcionó; resultó que olvidé configurar el caché real para que OkHttpClient lo use. Vea el código en la pregunta o en esta respuesta .)
Jonik
2
Sólo una palabra de consejo: HttpResponseCache has been renamed to Cache.** Install it with OkHttpClient.setCache(...) instead of OkHttpClient.setResponseCache(...).
Henrique de Sousa
2
No recibo llamadas de interceptor cuando la red no está disponible. No estoy seguro de cómo puede afectar la condición cuando la red no está disponible. ¿Me estoy perdiendo de algo?
Androidme
2
es la if (Utils.isNetworkAvailable(context))correcta o se supone que se invierte, es decir if (!Utils.isNetworkAvailable(context))?
ericn
2
Estoy usando Retrofit 2.1.0 y cuando el teléfono está desconectado, public okhttp3.Response intercept(Chain chain) throws IOExceptionnunca se llama, solo se llama cuando estoy en línea
ericn
28

Todas las respuestas anteriores no funcionaron para mí. Traté de implementar caché sin conexión en la actualización 2.0.0-beta2 . Agregué un interceptor usando el okHttpClient.networkInterceptors()método, pero recibí java.net.UnknownHostExceptioncuando intenté usar el caché sin conexión. Resultó que tenía que agregar okHttpClient.interceptors()también.

El problema era que el caché no se escribió en el almacenamiento flash porque el servidor regresó, lo Pragma:no-cacheque impide que OkHttp almacene la respuesta. La memoria caché sin conexión no funcionó incluso después de modificar los valores del encabezado de la solicitud. Después de algunas pruebas y errores, conseguí que el caché funcionara sin modificar el lado del backend eliminando pragma de la respuesta en lugar de la solicitud:response.newBuilder().removeHeader("Pragma");

Actualización: 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();

}
kosiara - Bartosz Kosarzycki
fuente
66
Se parece a tu interceptors ()y networkInterceptors ()son idénticos. ¿Por qué duplicaste esto?
toobsco42
diferentes tipos de interceptores leen aquí github.com/square/okhttp/wiki/Interceptors
Rohit Bandil
sí, pero ambos hacen lo mismo, así que estoy bastante seguro de que 1 interceptor debería ser suficiente, ¿verdad?
Ovidiu Latcu
¿Hay alguna razón específica para no usar la misma instancia de interceptor para ambos .networkInterceptors().add()y interceptors().add()?
ccpizza hace
22

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();
}
Arkadiusz Konior
fuente
3
no funciona para mí ... Conseguir 504 Solicitud insatisfactoria (solo si está en caché)
zacharia
Solo tu solución me ayuda, muchas gracias. Perder 2 días para desplazarse hacia abajo
Ярослав Мовчан
1
Sí, la única solución de trabajo en mi caso. (Retrofit 1.9.x + okHttp3)
Hoang Nguyen Huu
1
Funciona con Retrofit RETROFIT_VERSION = 2.2.0 OKHTTP_VERSION = 3.6.0
Tadas Valaitis
¿Cómo agregar builder.addheader () en este método para acceder a la API con autorización?
Abhilash
6

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

  • Usar caché sin conexión
  • Use la respuesta en caché primero hasta que caduque, luego la red
  • Use la red primero y luego la caché para algunas solicitudes
  • No almacenar en caché para algunas respuestas

Construí una pequeña biblioteca auxiliar para configurar la memoria caché OKHttp fácilmente, puede ver la prueba de unidad relacionada 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 está desconectado:

@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 caducado. Espero que ayude.

Nicolas Cornette
fuente
2
¡Tu lib es genial! Gracias por tu arduo trabajo. The lib: github.com/ncornette/OkCacheControl
Hoang Nguyen Huu
2

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

Luego simplemente agregue el cliente al generador de actualizaciones:

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

Владимир Широков
fuente
1
cuando cargo por primera vez con el modo fuera de línea, ¡se cuelga! de lo contrario, funciona correctamente
Zafer Celaloglu
Esto no funciona para mi. Lo pegué y lo probé después de intentar integrar el principio, pero net lo puso a funcionar.
Niño
App.sApp.getCacheDir () ¿qué hace esto?
Huzaifa Asif