OKHttp ile Retrofit, çevrimdışıyken önbellek verilerini kullanabilir


151

HTTP yanıtlarını önbelleğe almak için Retrofit & OKHttp'yi kullanmaya çalışıyorum. Bu özü takip ettim ve şu kodu aldım :

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

Ve bu, Cache-Control başlıklarına sahip MyApi

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

Önce çevrimiçi talep ediyorum ve önbellek dosyalarını kontrol ediyorum. Doğru JSON yanıtı ve üstbilgiler var. Ancak çevrimdışı talep etmeye çalıştığımda her zaman alıyorum RetrofitError UnknownHostException. Retrofit'in önbellekten gelen yanıtı okumasını sağlamak için yapmam gereken başka bir şey var mı?

DÜZENLEME: OKHttp 2.0.x yana HttpResponseCacheolduğunu Cache, setResponseCacheolupsetCache


1
Aradığınız sunucu uygun bir Cache-Control başlığıyla yanıt veriyor mu?
Hassan Ibraheem

bunu döndürür Cache-Control: s-maxage=1209600, max-age=1209600, yeterli olup olmadığını bilmiyorum.
osrl

Görünüşe göre publicanahtar kelimenin çevrimdışı çalışması için yanıt başlığında olması gerekiyordu. Ancak bu başlıklar, mevcut olduğunda OkClient'in ağı kullanmasına izin vermez. Mevcut olduğunda ağı kullanmak için önbellek politikasını / stratejisini ayarlamanın bir yolu var mı?
osrl

Bunu aynı istekte yapıp yapamayacağından emin değilim. İlgili CacheControl sınıfını ve Cache-Control başlıklarını kontrol edebilirsiniz . Böyle bir davranış yoksa, muhtemelen iki istek yapmayı tercih ederim, yalnızca önbelleğe alınmış bir istek (yalnızca önbelleğe alınmışsa) ve ardından bir ağ (max-age = 0).
Hassan Ibraheem

aklıma ilk gelen şey buydu. CacheControl ve CacheStrategy sınıflarında günler geçirdim . Ancak iki istek fikri pek mantıklı gelmedi. Eğer max-stale + max-agegeçirilir, bu ağdan isteğini yapar. Ama haftada maksimum bayat ayarlamak istiyorum. Bu, kullanılabilir ağ olsa bile önbellekten yanıt okumasını sağlar.
osrl

Yanıtlar:


190

Retrofit 2.x için düzenleme:

OkHttp Interceptor, çevrimdışıyken önbelleğe erişmenin doğru yoludur:

1) Interceptor Oluşturun:

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) Kurulum istemcisi:

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) Güçlendirmeye müşteri ekleyin

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

Ayrıca kontrol @kosiara - Bartosz Kosarzycki 'ın cevabı . Yanıttan bazı başlıkları kaldırmanız gerekebilir.


OKHttp 2.0.x (Orijinal yanıtı kontrol edin):

OKHttp 2.0.x yana HttpResponseCacheolduğunu Cache, setResponseCacheolduğunu setCache. Yani şunu beğenmelisiniz setCache:

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

Orijinal Cevap:

Sunucu yanıtının önbellekten okumak Cache-Control: publiciçin yapması gerektiği ortaya çıktı OkClient.

Ayrıca mevcut olduğunda şebekeden talepte bulunmak istiyorsanız, Cache-Control: max-age=0istek başlığı eklemelisiniz . Bu cevap , parametreleştirmenin nasıl yapılacağını gösterir. Ben böyle kullandım:

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

(Bunun neden işe yaramadığını merak ediyordum; OkHttpClient'ın kullanması için gerçek önbelleği ayarlamayı unuttum. Sorudaki veya bu
yanıttaki

2
Sadece bir tavsiye kelime: HttpResponseCache has been renamed to Cache.** Install it with OkHttpClient.setCache(...) instead of OkHttpClient.setResponseCache(...).
Henrique de Sousa

2
Ağ mevcut olmadığında müdahale eden aranmıyorum. Ağ kullanılamadığında durumun nasıl gerçekleşeceğinden emin değilim. Burada bir şey mi kaçırıyorum?
Androidme

2
olduğu if (Utils.isNetworkAvailable(context))doğru ya da ie ters olması gerekiyordu if (!Utils.isNetworkAvailable(context))?
ericn

2
Retrofit 2.1.0 kullanıyorum ve telefon çevrimdışıyken public okhttp3.Response intercept(Chain chain) throws IOExceptionasla
aranmıyor

28

Yukarıdaki cevapların hiçbiri benim için işe yaramadı. Güçlendirme 2.0.0-beta2'de çevrimdışı önbelleği uygulamaya çalıştım . okHttpClient.networkInterceptors()Yöntemi kullanarak bir engelleyici ekledim ancak java.net.UnknownHostExceptionönbelleği çevrimdışı kullanmaya çalıştığımda aldım. Benim de eklemem gerektiği ortaya çıktı okHttpClient.interceptors().

Sorun, önbelleğin flash belleğe yazılmamasıydı, çünkü sunucu geri döndü ve Pragma:no-cachebu da OkHttp'nin yanıtı depolamasını engelliyordu. Çevrimdışı önbellek, istek başlığı değerleri değiştirildikten sonra bile çalışmadı. Biraz deneme yanılma işleminden sonra, istek yerine yanıttan pragmayı kaldırarak arka uç tarafını değiştirmeden önbelleğin çalışmasını sağladım -response.newBuilder().removeHeader("Pragma");

Güçlendirme: 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
Size benziyor interceptors ()ve networkInterceptors ()aynı. Bunu neden kopyaladın?
toobsco42

farklı tür önleyiciler burada okur. github.com/square/okhttp/wiki/Interceptors
Rohit Bandil

evet ama ikisi de aynı şeyi yapıyor, bu yüzden 1 engelleyicinin yeterli olması gerektiğinden oldukça eminim, değil mi?
Ovidiu Latcu

Belirli bir neden yoktur değil ikisi için de aynı önleme örneğini kullanabilir .networkInterceptors().add()ve interceptors().add()?
ccpizza

22

Çözümüm:

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
(önbelleğe sadece-eğer-) 504 edilemezdir İsteği alınıyor ... benim için çalışmıyor
zacharia

Sadece sizin çözümünüz bana yardımcı oluyor, çok teşekkürler. Aşağı kaydırmak için 2 gün
harcayın

1
evet, benim durumumdaki tek çalışan çözüm. (Güçlendirme 1.9.x + okHttp3)
Hoang Nguyen Huu

1
Retrofit ile çalışır RETROFIT_VERSION = 2.2.0 OKHTTP_VERSION = 3.6.0
Tadas Valaitis

yetkilendirmeli api'ye erişmek için bu yönteme builder.addheader () nasıl eklenir?
Abhilash

6

Cevap EVET, yukarıdaki cevaplara dayanarak, tüm olası kullanım durumlarını doğrulamak için birim testleri yazmaya başladım:

  • Çevrimdışıyken önbelleği kullan
  • Önce süresi dolana kadar önbelleğe alınmış yanıtı, ardından ağı kullanın
  • Önce ağı kullan, sonra bazı istekler için önbelleğe al
  • Bazı yanıtlar için önbellekte saklamayın

OKHttp önbelleğini kolayca yapılandırmak için küçük bir yardımcı kitap oluşturdum, ilgili birim testini burada Github'da görebilirsiniz: https://github.com/ncornette/OkCacheControl/blob/master/okcache-control/src/test/java/com/ ncornette / önbellek / OkCacheControlTest.java

Çevrimdışıyken önbellek kullanımını gösteren Unittest:

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

Gördüğünüz gibi, önbellek süresi dolmuş olsa bile kullanılabilir. Umarım yardımcı olur.


2
Kitaplığınız harika! Sıkı çalışman için teşekkürler. Kitaplık
Hoang Nguyen Huu


2

Retrofit2 ve OkHTTP3 ile önbellek:

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 () statik yöntemi:

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

Ardından, iyileştirme oluşturucuya müşteri ekleyin:

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

Orijinal kaynak: https://newfivefour.com/android-retrofit2-okhttp3-cache-network-request-offline.html


1
Çevrimdışı mod ile ilk yüklediğimde, çöküyor! aksi halde düzgün çalışıyor
Zafer Celaloğlu

bu benim için çalışmıyor. İlkesini bütünleştirmeye çalıştıktan sonra denedikten sonra kopyalayıp yapıştırdım, ama işe yaramasını sağlayın.
Boy

1
App.sApp.getCacheDir () bu ne yapar?
Huzaifa Asif
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.