O Retrofit com OKHttp pode usar dados de cache quando offline

148

Estou tentando usar o Retrofit & OKHttp para armazenar em cache as respostas HTTP. Eu segui essa essência e, acabei com 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);

E esse é o MyApi com os cabeçalhos de controle de cache

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

Primeiro solicito on-line e verifico os arquivos de cache. A resposta JSON correta e os cabeçalhos estão lá. Mas quando tento solicitar offline, sempre entendo RetrofitError UnknownHostException. Há mais alguma coisa que devo fazer para fazer com que o Retrofit leia a resposta do cache?

EDIT: Como OKHttp 2.0.x HttpResponseCacheé Cache, setResponseCacheésetCache

osrl
fonte
1
O servidor para o qual você está chamando está respondendo com um cabeçalho de controle de cache apropriado?
Hassan Ibraheem
retorna isso Cache-Control: s-maxage=1209600, max-age=1209600, não sei se é suficiente.
osrl
Parece que a publicpalavra - chave precisava estar no cabeçalho de resposta para fazê-la funcionar offline. Porém, esses cabeçalhos não permitem que o OkClient use a rede quando houver disponível. Existe alguma maneira de definir a política / estratégia de cache para usar a rede quando disponível?
osrl
Não tenho certeza se você pode fazer isso na mesma solicitação. Você pode verificar a classe CacheControl relevante e os cabeçalhos de controle de cache . Se não houver esse comportamento, eu provavelmente optaria por fazer duas solicitações, uma solicitação apenas em cache (apenas se em cache), seguida por uma rede (idade máxima = 0).
Hassan Ibraheem
essa foi a primeira coisa que me veio à mente. Passei dias nessas classes CacheControl e CacheStrategy . Mas a ideia de dois pedidos não fazia muito sentido. Se max-stale + max-agepassado, ele solicita da rede. Mas quero definir o máximo de uma semana. Isso faz com que leia a resposta do cache, mesmo se houver rede disponível.
osrl

Respostas:

189

Edite para o Retrofit 2.x:

O OkHttp Interceptor é o caminho certo para acessar o cache quando offline:

1) Criar 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 instalação:

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) Adicione o cliente para modernizar

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

Além disso, verifique @kosiara - Bartosz Kosarzycki 's resposta . Pode ser necessário remover algum cabeçalho da resposta.


OKHttp 2.0.x (verifique a resposta original):

Desde OKHttp 2.0.x HttpResponseCacheé Cache, setResponseCacheé setCache. Então você deve setCacheassim:

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

Resposta original:

Acontece que a resposta do servidor deve ter Cache-Control: publicque ser feita OkClientpara ler do cache.

Além disso, se você desejar solicitar da rede quando disponível, adicione o Cache-Control: max-age=0cabeçalho da solicitação. Esta resposta mostra como fazê-lo parametrizado. Foi assim que eu o usei:

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
fonte
(Eu queria saber por que isso não funcionou; acabou que eu esqueci de definir o cache real para OkHttpClient para uso Veja o código em questão ou em. Esta resposta .)
Jonik
2
Apenas uma palavra de conselho: HttpResponseCache has been renamed to Cache.** Install it with OkHttpClient.setCache(...) instead of OkHttpClient.setResponseCache(...).
Henrique de Sousa
2
Não recebo o interceptador chamado quando a rede não está disponível. Não sei como será atingida a condição quando a rede não estiver disponível. Estou faltando alguma coisa aqui?
Androidme 12/04
2
é o if (Utils.isNetworkAvailable(context))correto ou deve ser revertido, ou seja if (!Utils.isNetworkAvailable(context))?
ericn
2
Estou usando Retrofit 2.1.0 e quando o telefone estiver desligada, public okhttp3.Response intercept(Chain chain) throws IOExceptionnunca é chamado, ele só é chamado quando estou online
ericn
28

Todas as respostas acima não funcionaram para mim. Tentei implementar o cache offline no retrofit 2.0.0-beta2 . Adicionei um interceptador usando o okHttpClient.networkInterceptors()método, mas recebi java.net.UnknownHostExceptionquando tentei usar o cache offline. Acabou que eu tinha que adicionar okHttpClient.interceptors()também.

O problema era que o cache não foi gravado no armazenamento flash porque o servidor retornou, o Pragma:no-cacheque impede que o OkHttp armazene a resposta. O cache offline não funcionou mesmo depois de modificar os valores do cabeçalho da solicitação. Após algumas tentativas e erros, consegui que o cache funcionasse sem modificar o lado do back-end, removendo o pragma da resposta em vez da solicitação -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();

}
kosiara - Bartosz Kosarzycki
fonte
6
Parece que o seu interceptors ()e networkInterceptors ()são idênticos. Por que você duplicou isso?
toobsco42
diferentes tipos de interceptores, leia aqui. github.com/square/okhttp/wiki/Interceptors
Rohit Bandil
sim, mas os dois fazem as mesmas coisas, por isso tenho certeza de que um interceptador deve ser suficiente, certo?
Ovidiu Latcu
existe um motivo específico para não usar a mesma instância de interceptador para ambos .networkInterceptors().add()e interceptors().add()?
ccpizza
22

Minha solução:

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
fonte
3
não funciona para mim ... Obtendo 504 Request insatisfatível (apenas se-cache)
Zacharia
Somente a sua solução me ajuda, muito obrigado. Resíduos de 2 dias para rolar para baixo
Ярослав Мовчан
1
Sim, a única solução de trabalho no meu caso. (Retrofit 1.9.x + okHttp3)
Hoang Nguyen Huu
1
Funciona com o retrofit RETROFIT_VERSION = 2.2.0 OKHTTP_VERSION = 3.6.0
Tadas Valaitis
como adicionar builder.addheader () neste método para acessar a API com autorização?
precisa
6

A resposta é SIM, com base nas respostas acima, comecei a escrever testes de unidade para verificar todos os possíveis casos de uso:

  • Usar cache quando offline
  • Use a resposta em cache primeiro até expirar, depois a rede
  • Use primeiro a rede e depois o cache para alguns pedidos
  • Não armazene no cache para algumas respostas

Eu construí uma pequena biblioteca auxiliar para configurar o cache OKHttp facilmente, você pode ver o unittest relacionado aqui no Github: https://github.com/ncornette/OkCacheControl/blob/master/okcache-control/src/test/java/com/ ncornette / cache / OkCacheControlTest.java

Unittest que demonstra o uso do cache quando 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);
}

Como você pode ver, o cache pode ser usado mesmo que tenha expirado. Espero que ajude.

Nicolas Cornette
fonte
2
Sua lib é ótima! Obrigado pelo seu trabalho duro. A lib: github.com/ncornette/OkCacheControl
Hoang Nguyen Huu
2

Cache com Retrofit2 e 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();
    }

Em seguida, basta adicionar o cliente ao construtor de retrofit:

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

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

Владимир Широков
fonte
1
quando carrego pela primeira vez com o modo offline, ele está travando! caso contrário ele está funcionando corretamente
Zafer Celaloglu
isso não funciona para mim. Copiei e colei e tentei depois de tentar integrar o princípio, mas a net faz com que funcione.
Rapaz
App.sApp.getCacheDir () o que isso faz?
Huzaifa Asif