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

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

}
Kosiara - Bartosz Kosarzycki
la source
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();
}
Arkadiusz Konior
la source
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.

Nicolas Cornette
la source
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

Владимир Широков
la source
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