web-dev-qa-db-fra.com

OKHttp peut-il utiliser des données de cache en mode hors connexion?

J'essaie d'utiliser Retrofit & OKHttp pour mettre en cache les réponses HTTP. J'ai suivi this Gist et, 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
   );

D'abord je demande en ligne et vérifie les fichiers de cache. La réponse JSON correcte et les en-têtes sont là. Mais lorsque j'essaie de faire une demande hors ligne, j'obtiens toujours RetrofitError UnknownHostException. Y a-t-il autre chose que je devrais faire pour que Retrofit lise la réponse dans la mémoire cache?

EDIT: Depuis OKHttp 2.0.x HttpResponseCache est Cache, setResponseCache est setCache

140
osrl

Éditer 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 le client à la modernisation

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

Vérifiez également @ kosiara - Bartosz Kosarzycki 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):

Depuis OKHttp 2.0.x HttpResponseCache est Cache, setResponseCache est setCache. Donc, vous devriez setCache comme 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 avoir Cache-Control: public pour que OkClient puisse être lu dans le cache.

De même, si vous souhaitez demander du réseau, le cas échéant, vous devez ajouter l'en-tête de requête Cache-Control: max-age=0. Cette réponse montre comment procéder 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);
            }
        }
});
180
osrl

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

Le problème était que le cache n'était pas écrit dans la mémoire flash car le serveur avait renvoyé Pragma:no-cache, ce qui empêchait 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é serveur en supprimant pragma de reponse au lieu de la requête - response.newBuilder().removeHeader("Pragma");

Rénovation: 2.0.0-beta2; OkHttp: 2.5.

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

}

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

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 en mode hors connexion
  • Utilisez d'abord la réponse en cache jusqu'à expiration, puis en réseau
  • Utiliser d'abord le réseau puis le cache pour certaines requêtes
  • Ne pas stocker en cache pour certaines réponses

J'ai construit une petite bibliothèque d'assistance pour configurer facilement le cache OKHttp, vous pouvez voir le lien unittest 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 constater, le cache peut être utilisé même s'il a expiré. J'espère que ça va aider.

5
Nicolas Cornette

en me basant sur answer de @ kosiara-bartosz-kasarzycki, j'ai créé un exemple de projet qui se charge correctement à partir de la mémoire-> disque-> du réseau à l'aide de retrofit, okhttp, rxjava et goyave. https://github.com/digitalbuddha/StoreDemo

5
FriendlyMikhail

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

NetworkUtils.isNetworkAvailable () méthode statique:

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