Có thể trang bị thêm với OKHttp sử dụng dữ liệu bộ đệm khi ngoại tuyến


148

Tôi đang cố gắng sử dụng Retrofit & OKHttp để lưu các phản hồi HTTP. Tôi đã theo ý chính này và, kết thúc với mã này:

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

Và đây là MyApi với các tiêu đề 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
   );

Đầu tiên tôi yêu cầu trực tuyến và kiểm tra các tập tin bộ nhớ cache. Phản hồi và tiêu đề JSON chính xác là có. Nhưng khi tôi cố gắng yêu cầu ngoại tuyến, tôi luôn nhận được RetrofitError UnknownHostException. Có điều gì khác tôi nên làm để Retrofit đọc phản hồi từ bộ đệm không?

EDIT: Vì OKHttp 2.0.x HttpResponseCacheCache, setResponseCachesetCache


1
Máy chủ bạn đang gọi có phản hồi với tiêu đề Kiểm soát bộ đệm phù hợp không?
Hassan Ibraheem

Nó trả về cái này Cache-Control: s-maxage=1209600, max-age=1209600tôi không biết nếu nó đủ.
osrl

Có vẻ như publictừ khóa là cần thiết trong tiêu đề phản hồi để làm cho nó hoạt động ngoại tuyến. Nhưng, những tiêu đề này không cho phép OkClient sử dụng mạng khi có sẵn. Có cách nào để thiết lập chính sách / chiến lược bộ đệm để sử dụng mạng khi khả dụng không?
osrl

Tôi không chắc liệu bạn có thể làm điều đó trong cùng một yêu cầu hay không. Bạn có thể kiểm tra lớp CacheControl có liên quan và các tiêu đề Kiểm soát bộ đệm . Nếu không có hành vi như vậy, có lẽ tôi sẽ chọn thực hiện hai yêu cầu, một yêu cầu chỉ được lưu trong bộ nhớ cache (chỉ khi được lưu trong bộ nhớ cache), theo sau là một mạng (max-age = 0).
Hassan Ibraheem

đó là điều đầu tiên tôi nghĩ đến Tôi đã dành nhiều ngày trong các lớp CacheControlCacheStrargety đó . Nhưng hai ý tưởng yêu cầu không có nhiều ý nghĩa. Nếu max-stale + max-ageđược thông qua, nó không yêu cầu từ mạng. Nhưng tôi muốn thiết lập tối đa một tuần. Điều này làm cho nó đọc phản hồi từ bộ đệm ngay cả khi có mạng.
osrl

Câu trả lời:


189

Chỉnh sửa cho trang bị thêm 2.x:

OkHttp Interceptor là cách phù hợp để truy cập bộ đệm khi ngoại tuyến:

1) Tạo đánh chặn:

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) Cài đặt máy khách:

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) Thêm khách hàng để trang bị thêm

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

Đồng thời kiểm tra câu trả lời của @kosiara - Bartosz Kosarzycki . Bạn có thể cần phải xóa một số tiêu đề từ phản hồi.


OKHttp 2.0.x (Kiểm tra câu trả lời gốc):

Vì OKHttp 2.0.x HttpResponseCacheCache, setResponseCachesetCache. Vì vậy, bạn nên setCachethích điều này:

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

Câu trả lời gốc:

Nó chỉ ra rằng phản ứng của máy chủ phải Cache-Control: publicthực hiện OkClientđể đọc từ bộ đệm.

Ngoài ra Nếu bạn muốn yêu cầu từ mạng khi có sẵn, bạn nên thêm Cache-Control: max-age=0tiêu đề yêu cầu. Câu trả lời này cho thấy làm thế nào để làm nó tham số hóa. Đây là cách tôi sử dụng nó:

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

(Tôi đã tự hỏi tại sao điều này không hoạt động; hóa ra tôi đã quên đặt bộ đệm thực tế cho OkHttpClient để sử dụng. Xem mã trong câu hỏi hoặc trong câu trả lời này .)
Jonik

2
Chỉ cần một lời khuyên : HttpResponseCache has been renamed to Cache.** Install it with OkHttpClient.setCache(...) instead of OkHttpClient.setResponseCache(...).
Henrique de Sousa

2
Tôi không nhận được cuộc gọi chặn khi mạng không khả dụng. Tôi không chắc làm thế nào điều kiện khi mạng không có sẵn sẽ xảy ra. Am i thiếu cái gì ở đây?
Androidme

2
if (Utils.isNetworkAvailable(context))chính xác hay nó được cho là đảo ngược tức là if (!Utils.isNetworkAvailable(context))?
ericn

2
Tôi đang sử dụng Retrofit 2.1.0 và khi điện thoại ngoại tuyến, public okhttp3.Response intercept(Chain chain) throws IOExceptionkhông bao giờ được gọi, nó chỉ được gọi khi tôi trực tuyến
vào

28

Tất cả các anwsers ở trên không làm việc cho tôi. Tôi đã cố gắng thực hiện bộ đệm ngoại tuyến trong retrofit 2.0.0-beta2 . Tôi đã thêm một okHttpClient.networkInterceptors()phương thức chặn bằng cách sử dụng nhưng nhận được java.net.UnknownHostExceptionkhi tôi cố gắng sử dụng bộ đệm ẩn ngoại tuyến. Hóa ra tôi cũng phải thêm okHttpClient.interceptors()vào.

Vấn đề là bộ đệm không được ghi vào bộ lưu trữ flash vì máy chủ đã trả về Pragma:no-cache, điều đó ngăn OkHttp lưu trữ phản hồi. Bộ nhớ cache ngoại tuyến không hoạt động ngay cả sau khi sửa đổi giá trị tiêu đề yêu cầu. Sau một số lỗi và thử, tôi đã có bộ đệm hoạt động mà không sửa đổi phía phụ trợ bằng cách xóa pragma khỏi phản hồi thay vì yêu cầu -response.newBuilder().removeHeader("Pragma");

Trang bị thêm: 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
Nó trông giống như của bạn interceptors ()networkInterceptors ()giống hệt nhau. Tại sao bạn lại nhân đôi điều này?
toobsco42

các loại đánh chặn khác nhau đọc ở đây. github.com/sapes/okhttp/wiki/Interceptors
Rohit Bandil

đúng nhưng cả hai đều làm những việc giống nhau nên tôi khá chắc chắn rằng 1 thiết bị đánh chặn là đủ, phải không?
Ovidiu Latcu

Có một lý do cụ thể để không sử dụng cùng một thể hiện đánh chặn cho cả hai .networkInterceptors().add()interceptors().add()?
ccpizza

22

Giải pháp của tôi:

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
không làm việc cho tôi ... Nhận 504 Yêu cầu không thỏa mãn (chỉ khi được lưu trong bộ nhớ cache)
zacharia

Chỉ có giải pháp của bạn giúp tôi, cảm ơn rất nhiều. Lãng phí 2 ngày để di chuyển xuống
Ярослав Мовчан

1
yup, giải pháp làm việc duy nhất trong trường hợp của tôi. (Trang bị thêm 1.9.x + okHttp3)
Hoàng Nguyên Hữu

1
Hoạt động với Retrofit RETROFIT_VERSION = 2.2.0 OKHTTP_VERSION = 3.6.0
Tadas Vala viêm

Làm thế nào để thêm builder.addheader () trong phương thức này để truy cập api với ủy quyền?
Abhilash

6

Câu trả lời là CÓ, dựa trên các câu trả lời ở trên, tôi bắt đầu viết bài kiểm tra đơn vị để xác minh tất cả các trường hợp sử dụng có thể:

  • Sử dụng bộ đệm khi ngoại tuyến
  • Sử dụng phản hồi lưu trữ trước tiên cho đến khi hết hạn, sau đó mạng
  • Sử dụng mạng trước sau đó lưu vào bộ đệm cho một số yêu cầu
  • Không lưu trữ trong bộ nhớ cache cho một số phản hồi

Tôi đã xây dựng một trình trợ giúp nhỏ lib để cấu hình bộ đệm OKHttp một cách dễ dàng, bạn có thể thấy phần mềm không liên quan ở đây trên Github: https://github.com/ncornette/OkCacheControl/blob/master/okcache-control/src/test/java/com/ ncornette / cache / OkCacheControlTest.java

Unittest thể hiện việc sử dụng bộ đệm khi ngoại tuyến:

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

Như bạn có thể thấy, bộ đệm có thể được sử dụng ngay cả khi nó đã hết hạn. Hy vọng nó sẽ giúp.


2
Lib của bạn là tuyệt vời! Cảm ơn bạn đã làm việc chăm chỉ. The lib: github.com/ncornette/OkCacheControl
Hoàng Nguyễn Hữu


2

Bộ nhớ cache với Retrofit2 và 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();

Phương thức tĩnh NetworkUtils.isNetworkAv Available ():

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

Sau đó, chỉ cần thêm ứng dụng khách vào trình xây dựng trang bị thêm:

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

Nguồn gốc: https://newfivefour.com/android-retrofit2-okhttp3-cache-network-request-offline.html


1
Khi tôi tải lần đầu tiên với chế độ ngoại tuyến, nó bị sập! nếu không thì nó hoạt động bình thường
Zafer Celaloglu

Điều này không làm việc cho tôi. Tôi đã sao chép nó và đã thử nó sau khi tôi cố gắng tích hợp nguyên tắc của nó, nhưng làm cho mạng hoạt động.
Cậu bé

App.sApp.getCacheDir () cái này làm gì?
Huzaifa Asif
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.