Dapat Retrofit dengan OKHttp menggunakan data cache saat offline

148

Saya mencoba menggunakan Retrofit & OKHttp untuk menyimpan tanggapan HTTP. Saya mengikuti intisari ini dan, berakhir dengan kode ini:

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

Dan ini adalah MyApi dengan header 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
   );

Pertama saya minta online dan periksa file cache. Respons dan header JSON yang benar ada di sana. Tetapi ketika saya mencoba untuk meminta offline, saya selalu mendapatkannya RetrofitError UnknownHostException. Apakah ada hal lain yang harus saya lakukan untuk membuat Retrofit membaca respons dari cache?

EDIT: Karena OKHttp 2.0.x HttpResponseCacheadalah Cache, setResponseCacheadalahsetCache

osrl
sumber
1
Apakah server yang Anda panggil merespons dengan tajuk Kontrol-Cache yang sesuai?
Hassan Ibraheem
mengembalikan ini Cache-Control: s-maxage=1209600, max-age=1209600saya tidak tahu apakah itu cukup.
osrl
Sepertinya publickata kunci diperlukan dalam tajuk respons untuk membuatnya berfungsi offline. Tapi, tajuk ini tidak membiarkan OkClient menggunakan jaringan saat ada. Apakah ada cara untuk mengatur kebijakan / strategi cache untuk menggunakan jaringan bila tersedia?
osrl
Saya tidak yakin apakah Anda bisa melakukan itu dalam permintaan yang sama. Anda dapat memeriksa kelas CacheControl yang relevan , dan header Kontrol-Cache . Jika tidak ada perilaku seperti itu, saya mungkin akan memilih untuk membuat dua permintaan, satu-satunya permintaan di-cache (hanya-jika-di-cache), diikuti oleh jaringan (maks-usia = 0) satu.
Hassan Ibraheem
itulah hal pertama yang terlintas di benak saya. Saya menghabiskan berhari-hari di kelas CacheControl dan CacheStrategy itu . Tapi dua permintaan ide tidak masuk akal. Jika max-stale + max-agedisahkan, itu memang meminta dari jaringan. Tapi saya ingin mengatur max-basi seminggu. Ini membuatnya membaca respons dari cache bahkan jika ada jaringan yang tersedia.
osrl

Jawaban:

189

Edit untuk Retrofit 2.x:

OkHttp Interceptor adalah cara yang tepat untuk mengakses cache saat offline:

1) Buat 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) Setup klien:

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) Tambahkan klien ke retrofit

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

Juga memeriksa @kosiara - Bartosz Kosarzycki 's jawaban . Anda mungkin perlu menghapus beberapa tajuk dari respons.


OKHttp 2.0.x (Periksa jawaban asli):

Karena OKHttp 2.0.x HttpResponseCacheadalah Cache, setResponseCacheadalah setCache. Jadi, Anda harus setCacheseperti ini:

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

Jawaban asli:

Ternyata respons server harus Cache-Control: publicmembuat OkClientmembaca dari cache.

Jika Anda ingin meminta dari jaringan saat tersedia, Anda harus menambahkan Cache-Control: max-age=0header permintaan. Jawaban ini menunjukkan bagaimana melakukannya dengan parameter. Ini adalah bagaimana saya menggunakannya:

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
sumber
(Saya bertanya-tanya mengapa ini tidak berhasil; ternyata saya lupa mengatur cache yang sebenarnya untuk OkHttpClient untuk digunakan. Lihat kode dalam pertanyaan atau dalam jawaban ini .)
Jonik
2
Hanya kata nasihat: HttpResponseCache has been renamed to Cache.** Install it with OkHttpClient.setCache(...) instead of OkHttpClient.setResponseCache(...).
Henrique de Sousa
2
Saya tidak mendapatkan pencegat dipanggil saat jaringan tidak tersedia. Saya tidak yakin bagaimana kondisi ketika jaringan tidak tersedia akan menekan. Apakah saya melewatkan sesuatu di sini?
Androidme
2
apakah yang if (Utils.isNetworkAvailable(context))benar atau itu seharusnya dibalik yaitu if (!Utils.isNetworkAvailable(context))?
ericn
2
Saya menggunakan Retrofit 2.1.0 dan ketika telepon sedang offline, public okhttp3.Response intercept(Chain chain) throws IOExceptiontidak pernah dipanggil, itu hanya dipanggil ketika saya sedang online
ericn
28

Semua jawaban di atas tidak bekerja untuk saya. Saya mencoba menerapkan cache offline di retrofit 2.0.0-beta2 . Saya menambahkan interceptor menggunakan okHttpClient.networkInterceptors()metode tetapi diterima java.net.UnknownHostExceptionketika saya mencoba menggunakan cache offline. Ternyata saya harus menambahkan okHttpClient.interceptors()juga.

Masalahnya adalah bahwa cache tidak ditulis ke penyimpanan flash karena server kembali Pragma:no-cacheyang mencegah OkHttp menyimpan respons. Cache offline tidak berfungsi bahkan setelah memodifikasi nilai header permintaan. Setelah beberapa percobaan-dan-kesalahan saya membuat cache berfungsi tanpa memodifikasi sisi backend dengan menghapus pragma dari tanggapan alih-alih permintaan -response.newBuilder().removeHeader("Pragma");

Retrofit: 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
sumber
6
Sepertinya Anda interceptors ()dan networkInterceptors ()identik. Mengapa Anda menduplikasi ini?
toobsco42
berbagai jenis pencegat baca di sini. github.com/square/okhttp/wiki/Interceptors
Rohit Bandil
ya tapi mereka berdua melakukan hal yang sama jadi saya cukup yakin bahwa 1 interceptor sudah cukup, bukan?
Ovidiu Latcu
apakah ada alasan khusus untuk tidak menggunakan contoh interceptor yang sama untuk keduanya .networkInterceptors().add()dan interceptors().add()?
ccpizza
22

Solusi saya:

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
sumber
3
tidak bekerja untuk saya ... Mendapatkan 504 Permintaan yang Tidak Memuaskan (hanya-jika-di-cache)
zacharia
Hanya solusi Anda yang membantu saya, terima kasih banyak. Buang 2 hari untuk menggulir ke bawah
Ярослав Мовчан
1
yup, satu-satunya solusi yang berfungsi dalam kasus saya. (Retrofit 1.9.x + okHttp3)
Hoang Nguyen Huu
1
Bekerja dengan Retrofit RETROFIT_VERSION = 2.2.0 OKHTTP_VERSION = 3.6.0
Tadas Valaitis
bagaimana cara menambahkan builder.addheader () dalam metode ini untuk mengakses api dengan otorisasi?
Abhilash
6

Jawabannya adalah YA, berdasarkan jawaban di atas, saya mulai menulis tes unit untuk memverifikasi semua kemungkinan penggunaan:

  • Gunakan cache saat offline
  • Gunakan respons yang di-cache terlebih dahulu hingga kedaluwarsa, kemudian jaringan
  • Pertama-tama gunakan jaringan, lalu cache untuk beberapa permintaan
  • Jangan simpan dalam cache untuk beberapa tanggapan

Saya membangun lib pembantu kecil untuk mengkonfigurasi cache OKHttp dengan mudah, Anda dapat melihat unittest terkait di sini di Github: https://github.com/ncornette/OkCacheControl/blob/master/okcache-control/src/test/java/com/ ncornette / cache / OkCacheControlTest.java

Terkecil yang menunjukkan penggunaan cache saat 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);
}

Seperti yang Anda lihat, cache dapat digunakan bahkan jika telah kedaluwarsa. Semoga ini bisa membantu.

Nicolas Cornette
sumber
2
Lib Anda hebat! Terima kasih atas kerja kerasnya. Lib: github.com/ncornette/OkCacheControl
Hoang Nguyen Huu
2

Tembolok dengan Retrofit2 dan 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();

Metode statis 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();
    }

Kemudian tambahkan saja klien ke pembuat retrofit:

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

Sumber asli: https://newfivefour.com/android-retrofit2-okhttp3-cache-network-request-offline.html

Владимир Широков
sumber
1
ketika saya pertama kali memuat dengan mode offline, itu crash! jika tidak berfungsi dengan baik
Zafer Celaloglu
ini tidak bekerja untuk saya. Saya salin-tempel dan mencobanya setelah saya mencoba mengintegrasikan prinsip itu, tetapi apakah itu bisa berfungsi.
Boy
App.sApp.getCacheDir () apa fungsinya?
Huzaifa Asif