Można Retrofit za pomocą OKHttp używać danych pamięci podręcznej w trybie offline


148

Próbuję użyć Retrofit & OKHttp do buforowania odpowiedzi HTTP. Podążałem za tym sednem i skończyłem z tym kodem:

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

A to jest MyApi z nagłówkami 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
   );

Najpierw żądam online i sprawdzam pliki pamięci podręcznej. Znajduje się tam poprawna odpowiedź JSON i nagłówki. Ale kiedy próbuję poprosić o dostęp offline, zawsze otrzymuję RetrofitError UnknownHostException. Czy jest jeszcze coś, co powinienem zrobić, aby Retrofit odczytywał odpowiedź z pamięci podręcznej?

EDYCJA: Ponieważ OKHttp 2.0.x HttpResponseCachejest Cache, setResponseCachejestsetCache


1
Czy serwer, do którego dzwonisz, odpowiada odpowiednim nagłówkiem Cache-Control?
Hassan Ibraheem

zwraca to Cache-Control: s-maxage=1209600, max-age=1209600, nie wiem, czy to wystarczy.
osrl

Wygląda na to, że publicsłowo kluczowe musiało znajdować się w nagłówku odpowiedzi, aby działało w trybie offline. Ale te nagłówki nie pozwalają OkClient na korzystanie z sieci, gdy jest dostępna. Czy mimo to można ustawić zasady / strategię pamięci podręcznej w celu korzystania z sieci, gdy jest dostępna?
osrl

Nie jestem pewien, czy możesz to zrobić w tym samym żądaniu. Możesz sprawdzić odpowiednią klasę CacheControl i nagłówki Cache-Control . Jeśli nie ma takiego zachowania, prawdopodobnie wybrałbym wysyłanie dwóch żądań, tylko żądania buforowanego (tylko w przypadku buforowania), a następnie sieci (max-age = 0).
Hassan Ibraheem

to była pierwsza rzecz, która przyszła mi do głowy. Spędziłem wiele dni na zajęciach CacheControl i CacheStrategy . Ale pomysł dwóch próśb nie miał większego sensu. Jeśli max-stale + max-agezostanie przekazane, żąda z sieci. Ale chcę ustawić maksymalny czas na tydzień. To sprawia, że ​​odczytuje odpowiedź z pamięci podręcznej, nawet jeśli jest dostępna sieć.
osrl

Odpowiedzi:


189

Edycja dla Retrofit 2.x:

OkHttp Interceptor to właściwy sposób na dostęp do pamięci podręcznej w trybie offline:

1) Utwórz 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) Konfiguracja klienta:

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) Dodaj klienta do modernizacji

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

Należy również sprawdzić @kosiara - Bartosz Kosarzycki „s odpowiedź . Może być konieczne usunięcie nagłówka z odpowiedzi.


OKHttp 2.0.x (Sprawdź oryginalną odpowiedź):

Ponieważ OKHttp 2.0.x HttpResponseCachejest Cache, setResponseCachejest setCache. Więc powinieneś setCachepolubić to:

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

Oryginalna odpowiedź:

Okazuje się, że odpowiedź serwera musi Cache-Control: publicwykonać OkClientodczyt z pamięci podręcznej.

Ponadto, jeśli chcesz żądać z sieci, gdy jest dostępna, powinieneś dodać Cache-Control: max-age=0nagłówek żądania. Ta odpowiedź pokazuje, jak to zrobić sparametryzowane. Tak to wykorzystałem:

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

(Zastanawiałem się, dlaczego to nie zadziałało; okazało się, że zapomniałem ustawić rzeczywistą pamięć podręczną dla OkHttpClient do użycia. Zobacz kod w pytaniu lub w tej odpowiedzi .)
Jonik

2
Tylko słowo rada: HttpResponseCache has been renamed to Cache.** Install it with OkHttpClient.setCache(...) instead of OkHttpClient.setResponseCache(...).
Henrique de Sousa

2
Nie mam wywołania przechwytywacza, gdy sieć nie jest dostępna. Nie jestem pewien, jak może dojść do sytuacji, gdy sieć nie jest dostępna. Czy coś mi umyka?
Androidme

2
jest if (Utils.isNetworkAvailable(context))poprawne, czy ma być odwrócone tj. if (!Utils.isNetworkAvailable(context))?
ericn

2
Używam Retrofit 2.1.0 i kiedy telefon jest offline, public okhttp3.Response intercept(Chain chain) throws IOExceptionnigdy nie
dzwoni, dzwoni

28

Wszystkie powyższe odpowiedzi nie działają dla mnie. Próbowałem zaimplementować pamięć podręczną offline w modernizacji 2.0.0-beta2 . Dodałem przechwytywacz za pomocą okHttpClient.networkInterceptors()metody, ale otrzymałem, java.net.UnknownHostExceptiongdy próbowałem użyć pamięci podręcznej w trybie offline. Okazało się, że muszę też dodać okHttpClient.interceptors().

Problem polegał na tym, że pamięć podręczna nie została zapisana w pamięci flash, ponieważ serwer zwrócił, Pragma:no-cacheco uniemożliwia OkHttp przechowywanie odpowiedzi. Pamięć podręczna offline nie działała nawet po zmodyfikowaniu wartości nagłówka żądania. Po kilku próbach i błędach udało mi się uruchomić pamięć podręczną bez modyfikowania strony zaplecza, usuwając pragmę z odpowiedzi zamiast żądania -response.newBuilder().removeHeader("Pragma");

Modernizacja: 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
Wygląda jak twój interceptors ()i networkInterceptors ()są identyczne. Dlaczego to zduplikowałeś?
toobsco42

czytaj tutaj różne typy przechwytywaczy. github.com/square/okhttp/wiki/Interceptors
Rohit Bandil

tak, ale obaj robią to samo, więc jestem prawie pewien, że jeden przechwytywacz powinien wystarczyć, prawda?
Ovidiu Latcu

czy istnieje konkretny powód, aby nie używać tej samej instancji przechwytywacza zarówno dla, jak .networkInterceptors().add()i interceptors().add()?
ccpizza

22

Moje rozwiązanie:

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
nie działa dla mnie ... Pobieranie 504 niezadowalającego żądania (tylko w przypadku buforowania)
zacharia

Pomaga mi tylko Twoje rozwiązanie, wielkie dzięki. Marnuj 2 dni, aby przewinąć w dół
Ярослав Мовчан

1
tak, jedyne działające rozwiązanie w moim przypadku. (Retrofit 1.9.x + okHttp3)
Hoang Nguyen Huu

1
Działa z Retrofit RETROFIT_VERSION = 2.2.0 OKHTTP_VERSION = 3.6.0
Tadas Valaitis

jak dodać builder.addheader () w tej metodzie, aby uzyskać dostęp do API z autoryzacją?
Abhilash

6

Odpowiedź brzmi TAK, na podstawie powyższych odpowiedzi zacząłem pisać testy jednostkowe, aby zweryfikować wszystkie możliwe przypadki użycia:

  • Używaj pamięci podręcznej w trybie offline
  • Użyj najpierw odpowiedzi z pamięci podręcznej, aż wygaśnie, a następnie sieci
  • Najpierw użyj sieci, a następnie buforuj dla niektórych żądań
  • Nie przechowuj w pamięci podręcznej niektórych odpowiedzi

Zbudowałem małą bibliotekę pomocniczą, aby łatwo skonfigurować pamięć podręczną OKHttp, możesz zobaczyć powiązane unittest tutaj na Github: https://github.com/ncornette/OkCacheControl/blob/master/okcache-control/src/test/java/com/ ncornette / cache / OkCacheControlTest.java

Unittest, który demonstruje użycie pamięci podręcznej w trybie 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);
}

Jak widać, pamięci podręcznej można używać, nawet jeśli wygasła. Mam nadzieję, że to pomoże.


2
Twoja libacja jest świetna! Dziękuję za twoją ciężką pracę. Biblioteka: github.com/ncornette/OkCacheControl
Hoang Nguyen Huu


2

Pamięć podręczna z Retrofit2 i 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 () metoda statyczna:

public static boolean isNetworkAvailable(Context context) {
        ConnectivityManager cm =
                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
        return activeNetwork != null &&
                activeNetwork.isConnectedOrConnecting();
    }

Następnie po prostu dodaj klienta do konstruktora modernizacji:

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

Oryginalne źródło: https://newfivefour.com/android-retrofit2-okhttp3-cache-network-request-offline.html


1
kiedy po raz pierwszy ładuję w trybie offline, następuje awaria! inaczej to działa prawidłowo
Zafer Celaloglu

to nie działa dla mnie. Skopiowałem i wkleiłem go i wypróbowałem po tym, jak próbowałem zintegrować jego zasadę, ale netto sprawi, że zadziała.
Boy

App.sApp.getCacheDir () co to robi?
Huzaifa Asif
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.