Kann das Nachrüsten mit OKHttp Cache-Daten verwenden, wenn es offline ist?

148

Ich versuche, Retrofit & OKHttp zum Zwischenspeichern von HTTP-Antworten zu verwenden. Ich folgte diesem Kern und endete mit diesem 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);

Und das ist MyApi mit den Cache-Control-Headern

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

Zuerst fordere ich online an und überprüfe die Cache-Dateien. Die richtige JSON-Antwort und die richtigen Header sind vorhanden. Aber wenn ich versuche, offline anzufordern, bekomme ich immer RetrofitError UnknownHostException. Gibt es noch etwas, das ich tun sollte, damit Retrofit die Antwort aus dem Cache liest?

EDIT: Da OKHttp 2.0.x HttpResponseCacheist Cache, setResponseCacheistsetCache

osrl
quelle
1
Antwortet der Server, den Sie anrufen, mit einem entsprechenden Cache-Control-Header?
Hassan Ibraheem
es gibt diese Cache-Control: s-maxage=1209600, max-age=1209600Ich weiß nicht , ob es genug ist.
osrl
Es scheint, dass das publicSchlüsselwort im Antwortheader enthalten sein musste, damit es offline funktioniert. Mit diesen Headern kann OkClient das Netzwerk jedoch nicht verwenden, wenn es verfügbar ist. Gibt es überhaupt eine Möglichkeit, die Cache-Richtlinie / -Strategie für die Verwendung des Netzwerks festzulegen, wenn diese verfügbar ist?
osrl
Ich bin mir nicht sicher, ob Sie das in derselben Anfrage tun können. Sie können die relevante CacheControl- Klasse und die Cache-Control- Header überprüfen . Wenn es kein solches Verhalten gibt, würde ich mich wahrscheinlich dafür entscheiden, zwei Anfragen zu stellen, eine nur zwischengespeicherte Anfrage (nur wenn zwischengespeichert), gefolgt von einer Netzwerkanforderung (max-age = 0).
Hassan Ibraheem
Das war das erste, was mir in den Sinn kam. Ich habe Tage in diesen CacheControl- und CacheStrategy- Klassen verbracht. Aber zwei Anfragen Idee machte nicht viel Sinn. Wenn max-stale + max-ageübergeben, wird vom Netzwerk angefordert. Aber ich möchte maximal eine Woche alt werden. Dadurch wird die Antwort aus dem Cache gelesen, auch wenn ein Netzwerk verfügbar ist.
osrl

Antworten:

189

Für Nachrüstung bearbeiten 2.x:

OkHttp Interceptor ist der richtige Weg, um offline auf den Cache zuzugreifen:

1) Interceptor erstellen:

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) Setup-Client:

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) Client zur Nachrüstung hinzufügen

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

Prüfen Sie auch @kosiara - Bartosz Kosarzycki ‚s Antwort . Möglicherweise müssen Sie einen Header aus der Antwort entfernen.


OKHttp 2.0.x (Überprüfen Sie die ursprüngliche Antwort):

Da OKHttp 2.0.x HttpResponseCacheist Cache, setResponseCacheist setCache. Das sollte dir also setCachegefallen:

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

Ursprüngliche Antwort:

Es stellt sich heraus, dass die Serverantwort erfolgen muss Cache-Control: public, um OkClientaus dem Cache zu lesen.

Wenn Sie eine Anforderung aus dem Netzwerk anfordern möchten, sollten Sie den Cache-Control: max-age=0Anforderungsheader hinzufügen . Diese Antwort zeigt, wie es parametrisiert wird. So habe ich es benutzt:

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
quelle
(Ich habe mich gefragt, warum dies nicht funktioniert hat. Es stellte sich heraus, dass ich vergessen habe, den tatsächlichen Cache für OkHttpClient festzulegen. Siehe den Code in der Frage oder in dieser Antwort .)
Jonik
2
Nur ein Ratschlag : HttpResponseCache has been renamed to Cache.** Install it with OkHttpClient.setCache(...) instead of OkHttpClient.setResponseCache(...).
Henrique de Sousa
2
Ich werde nicht abgefangen, wenn das Netzwerk nicht verfügbar ist. Ich bin nicht sicher, wie die Bedingung, wenn das Netzwerk nicht verfügbar ist, eintreten kann. Vermisse ich hier etwas?
Androidme
2
ist das if (Utils.isNetworkAvailable(context))richtig oder soll es umgekehrt sein dh if (!Utils.isNetworkAvailable(context))?
Ericic
2
Ich benutze Retrofit 2.1.0 und wenn das Telefon offline ist, public okhttp3.Response intercept(Chain chain) throws IOExceptionwird es nie angerufen, es wird nur angerufen, wenn ich online bin
ericn
28

Alle oben genannten Antworten haben bei mir nicht funktioniert. Ich habe versucht, den Offline-Cache in Retrofit 2.0.0-Beta2 zu implementieren . Ich habe einen Interceptor mit der okHttpClient.networkInterceptors()Methode hinzugefügt , aber erhalten, java.net.UnknownHostExceptionals ich versucht habe, den Cache offline zu verwenden. Es stellte sich heraus, dass ich auch hinzufügen musste okHttpClient.interceptors().

Das Problem war, dass der Cache nicht in den Flash-Speicher geschrieben wurde, da der Server zurückgegeben wurde, Pragma:no-cachewodurch verhindert wurde, dass OkHttp die Antwort speichert. Der Offline-Cache funktionierte auch nach dem Ändern der Anforderungsheaderwerte nicht. Nach einigem Ausprobieren funktionierte der Cache, ohne die Backend-Seite zu ändern, indem Pragma anstelle der Anforderung aus der Antwort entfernt wurde.response.newBuilder().removeHeader("Pragma");

Nachrüstung: 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
quelle
6
Es sieht aus wie dein interceptors ()und networkInterceptors ()ist identisch. Warum hast du das dupliziert?
toobsco42
Hier werden verschiedene Arten von Abfangjägern gelesen. github.com/square/okhttp/wiki/Interceptors
Rohit Bandil
Ja, aber beide machen die gleichen Dinge, also bin ich mir ziemlich sicher, dass 1 Abfangjäger ausreichen sollte, oder?
Ovidiu Latcu
22

Meine Lösung:

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
quelle
3
funktioniert nicht für mich ... 504 Unbefriedigende Anfrage (nur wenn zwischengespeichert)
Zacharia
Nur Ihre Lösung hilft mir, vielen Dank. Verschwenden Sie 2 Tage, um nach unten zu scrollen
Ярослав Мовчан
1
Ja, die einzige funktionierende Lösung in meinem Fall. (Nachrüstung 1.9.x + okHttp3)
Hoang Nguyen Huu
1
Funktioniert mit Nachrüstung RETROFIT_VERSION = 2.2.0 OKHTTP_VERSION = 3.6.0
Tadas Valaitis
Wie füge ich builder.addheader () zu dieser Methode hinzu, um mit Autorisierung auf die API zuzugreifen?
Abhilash
6

Die Antwort lautet JA. Basierend auf den obigen Antworten habe ich begonnen, Komponententests zu schreiben, um alle möglichen Anwendungsfälle zu überprüfen:

  • Verwenden Sie den Cache, wenn Sie offline sind
  • Verwenden Sie zuerst die zwischengespeicherte Antwort, bis sie abgelaufen ist, und dann das Netzwerk
  • Verwenden Sie zuerst das Netzwerk und dann den Cache für einige Anforderungen
  • Für einige Antworten nicht im Cache speichern

Ich habe eine kleine Hilfsbibliothek erstellt, um den OKHttp-Cache einfach zu konfigurieren. Sie können die entsprechenden Unittest hier auf Github sehen: https://github.com/ncornette/OkCacheControl/blob/master/okcache-control/src/test/java/com/ ncornette / cache / OkCacheControlTest.java

Unittest, das die Verwendung des Caches im Offline-Modus demonstriert:

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

Wie Sie sehen, kann der Cache auch dann verwendet werden, wenn er abgelaufen ist. Hoffe es wird helfen.

Nicolas Cornette
quelle
2
Deine Bibliothek ist großartig! Vielen Dank für Ihre harte Arbeit. Die Bibliothek: github.com/ncornette/OkCacheControl
Hoang Nguyen Huu
5

Aufbauend auf der Antwort von @ kosiara-bartosz-kasarzycki habe ich ein Beispielprojekt erstellt, das mithilfe von Retrofit, Okhttp, Rxjava und Guave ordnungsgemäß aus dem Speicher-> Festplatten-> Netzwerk geladen wird. https://github.com/digitalbuddha/StoreDemo

FriendlyMikhail
quelle
2

Cache mit Retrofit2 und 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();

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

Fügen Sie dann einfach einen Client zum Retrofit Builder hinzu:

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

Ursprüngliche Quelle: https://newfivefour.com/android-retrofit2-okhttp3-cache-network-request-offline.html

Владимир Широков
quelle
1
Wenn ich zum ersten Mal im Offline-Modus lade, stürzt es ab! ansonsten funktioniert es richtig
Zafer Celaloglu
das funktioniert bei mir nicht. Ich habe es kopiert und ausprobiert, nachdem ich versucht hatte, das Prinzip zu integrieren, aber ich habe es netto zum Laufen gebracht.
Junge
App.sApp.getCacheDir () Was macht das?
Huzaifa Asif