Peut Retrofit avec OKHttp utiliser les données de cache en mode hors connexion


148

J'essaie d'utiliser Retrofit & OKHttp pour mettre en cache les réponses HTTP. J'ai suivi l'essentiel et j'ai fini avec ce 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);

Et voici MyApi avec les en-têtes 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
   );

Je demande d'abord en ligne et vérifie les fichiers de cache. La réponse et les en-têtes JSON corrects sont là. Mais lorsque j'essaie de demander hors ligne, je reçois toujours RetrofitError UnknownHostException. Dois-je faire autre chose pour que Retrofit lise la réponse à partir du cache?

EDIT: Puisque OKHttp 2.0.x HttpResponseCacheest Cache, setResponseCacheestsetCache


1
Le serveur que vous appelez répond-il avec un en-tête Cache-Control approprié?
— Hassan Ibraheem

ça renvoie ça Cache-Control: s-maxage=1209600, max-age=1209600je ne sais pas si ça suffit.
— osrl

On dirait que le publicmot - clé devait être dans l'en-tête de réponse pour le faire fonctionner hors ligne. Mais ces en-têtes ne permettent pas à OkClient d'utiliser le réseau lorsqu'il est disponible. Est-il possible de définir une politique / stratégie de cache pour utiliser le réseau lorsqu'il est disponible?
— osrl

Je ne sais pas si vous pouvez le faire dans la même demande. Vous pouvez vérifier la classe CacheControl appropriée et les en - têtes Cache-Control . S'il n'y a pas un tel comportement, j'opterais probablement pour faire deux requêtes, une requête uniquement mise en cache (uniquement si mise en cache), suivie d'une requête réseau (max-age = 0).
— Hassan Ibraheem

c'était la première chose qui m'est venue à l'esprit. J'ai passé des jours dans ces classes CacheControl et CacheStrategy . Mais l'idée de deux demandes n'avait pas beaucoup de sens. Si max-stale + max-ageest passé, il demande au réseau. Mais je veux définir max-stale une semaine. Cela permet de lire la réponse du cache même si un réseau est disponible.
— osrl

Réponses:


189

Modifier pour Retrofit 2.x:

OkHttp Interceptor est le bon moyen d'accéder au cache en mode hors connexion:

1) Créer un intercepteur:

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) Client d'installation:

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) Ajouter un client à la modernisation

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

Jetez également un coup @kosiara - Bartosz Kosarzycki de réponse . Vous devrez peut-être supprimer un en-tête de la réponse.


OKHttp 2.0.x (Vérifiez la réponse d'origine):

Puisque OKHttp 2.0.x HttpResponseCacheest Cache, setResponseCacheest setCache. Vous devriez donc setCacheaimer ceci:

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

Réponse originale:

Il s'avère que la réponse du serveur doit être Cache-Control: publicobligée OkClientde lire à partir du cache.

De plus, si vous souhaitez faire une demande à partir du réseau lorsqu'il est disponible, vous devez ajouter un en- Cache-Control: max-age=0tête de demande. Cette réponse montre comment le faire paramétré. Voici comment je l'ai utilisé:

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

(Je me demandais pourquoi cela ne fonctionnait pas; il s'est avéré que j'ai oublié de définir le cache réel pour OkHttpClient à utiliser. Voir le code dans la question ou dans cette réponse .)
— Jonik

2
Juste un conseil: HttpResponseCache has been renamed to Cache.** Install it with OkHttpClient.setCache(...) instead of OkHttpClient.setResponseCache(...).
— Henrique de Sousa

2
Je ne suis pas appelé par l'intercepteur lorsque le réseau n'est pas disponible. Je ne sais pas comment la condition lorsque le réseau n'est pas disponible peut se produire. Est-ce que j'ai râté quelque chose?
— Androidme

2
est le if (Utils.isNetworkAvailable(context))correct ou est-il censé être inversé ie if (!Utils.isNetworkAvailable(context))?
— ericn

2
J'utilise Retrofit 2.1.0 et lorsque le téléphone est hors ligne, public okhttp3.Response intercept(Chain chain) throws IOExceptionn'est jamais appelé, il ne l'appelle que lorsque je suis en ligne
— ericn

28

Toutes les réponses ci-dessus n'ont pas fonctionné pour moi. J'ai essayé d'implémenter le cache hors ligne dans la mise à niveau 2.0.0-beta2 . J'ai ajouté un intercepteur en utilisant la okHttpClient.networkInterceptors()méthode mais java.net.UnknownHostExceptionje l'ai reçu lorsque j'ai essayé d'utiliser le cache hors ligne. Il s'est avéré que je devais okHttpClient.interceptors()aussi ajouter .

Le problème était que le cache n'était pas écrit sur le stockage flash car le serveur est retourné, Pragma:no-cachece qui empêche OkHttp de stocker la réponse. Le cache hors ligne ne fonctionnait pas même après la modification des valeurs d'en-tête de demande. Après quelques essais et erreurs, j'ai fait fonctionner le cache sans modifier le côté backend en supprimant pragma de la réponse au lieu de la requête -response.newBuilder().removeHeader("Pragma");

Rénovation: 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();

}

6
Il ressemble au vôtre interceptors ()et networkInterceptors ()est identique. Pourquoi avez-vous dupliqué cela?
— toobsco42

différents types d'intercepteurs lisez ici. github.com/square/okhttp/wiki/Interceptors
— Rohit Bandil

oui mais ils font tous les deux les mêmes choses, donc je suis presque sûr qu'un seul intercepteur devrait suffire, non?
— Ovidiu Latcu

y a-t-il une raison spécifique de ne pas utiliser la même instance d'intercepteur pour les deux .networkInterceptors().add()et interceptors().add()?
— ccpizza le

22

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

3
ne fonctionne pas pour moi ... Obtenir 504 Requête Insatisfiable (uniquement si mis en cache)
— zacharia

Seule votre solution m'aide, merci beaucoup. Perdre 2 jours pour faire défiler vers le bas
— Ярослав Мовчан

1
oui, la seule solution de travail dans mon cas. (Retrofit 1.9.x + okHttp3)
— Hoang Nguyen Huu

1
Fonctionne avec Retrofit RETROFIT_VERSION = 2.2.0 OKHTTP_VERSION = 3.6.0
— Tadas Valaitis

comment ajouter builder.addheader () dans cette méthode pour accéder à l'api avec autorisation?
— Abhilash

6

La réponse est OUI, sur la base des réponses ci-dessus, j'ai commencé à écrire des tests unitaires pour vérifier tous les cas d'utilisation possibles:

  • Utiliser le cache hors ligne
  • Utilisez d'abord la réponse en cache jusqu'à expiration, puis le réseau
  • Utilisez d'abord le réseau puis le cache pour certaines demandes
  • Ne pas stocker dans le cache pour certaines réponses

J'ai construit une petite bibliothèque d'aide pour configurer facilement le cache OKHttp, vous pouvez voir l'unittest associé ici sur Github: https://github.com/ncornette/OkCacheControl/blob/master/okcache-control/src/test/java/com/ ncornette / cache / OkCacheControlTest.java

Unittest qui démontre l'utilisation du cache en mode hors connexion:

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

Comme vous pouvez le voir, le cache peut être utilisé même s'il a expiré. J'espère que cela aidera.


2
Votre lib est génial! Merci pour votre travail acharné. La lib: github.com/ncornette/OkCacheControl
— Hoang Nguyen Huu


2

Cache avec Retrofit2 et 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éthode statique 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();
    }

Ensuite, ajoutez simplement le client au constructeur de rénovation:

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

Source originale: https://newfivefour.com/android-retrofit2-okhttp3-cache-network-request-offline.html


1
lorsque je charge pour la première fois en mode hors ligne, il plante! sinon cela fonctionne correctement
— Zafer Celaloglu

Cela ne fonctionne pas pour moi. Je l'ai copié-collé et essayé après avoir essayé d'en intégrer le principe, mais ne le faites pas fonctionner.
— Garçon du

App.sApp.getCacheDir () qu'est-ce que cela fait?
— Huzaifa Asif
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.