สามารถติดตั้งเพิ่มด้วย OKHttp ใช้ข้อมูลแคชเมื่อออฟไลน์


148

ฉันพยายามใช้ Retrofit & OKHttp เพื่อแคชการตอบกลับ HTTP ฉันติดตามกระทู้นี้และลงเอยด้วยรหัสนี้:

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

และนี่คือ 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
   );

ก่อนอื่นฉันขอออนไลน์และตรวจสอบไฟล์แคช การตอบสนองและส่วนหัวของ JSON ที่ถูกต้องนั้นอยู่ที่นั่น RetrofitError UnknownHostExceptionแต่เมื่อฉันพยายามที่จะร้องขอออฟไลน์ผมเคยได้รับ มีอะไรอีกบ้างที่ฉันควรทำเพื่อให้ Retrofit อ่านการตอบสนองจากแคช?

แก้ไข: ตั้งแต่ OKHttp 2.0.x HttpResponseCacheคือCache, setResponseCacheคือsetCache


1
เซิร์ฟเวอร์ที่คุณโทรติดต่อนั้นตอบสนองด้วยส่วนหัว Cache-Control ที่เหมาะสมหรือไม่?
Hassan Ibraheem

มันคืนค่านี้Cache-Control: s-maxage=1209600, max-age=1209600ฉันไม่รู้ว่ามันเพียงพอหรือไม่
osrl

ดูเหมือนว่าpublicคำหลักจะต้องอยู่ในส่วนหัวการตอบสนองเพื่อให้ทำงานแบบออฟไลน์ แต่ส่วนหัวเหล่านี้จะไม่ยอมให้ OkClient ใช้เครือข่ายเมื่อมีให้ใช้งาน จะมีการตั้งค่านโยบาย / กลยุทธ์แคชเพื่อใช้เครือข่ายเมื่อพร้อมใช้งานหรือไม่
osrl

ฉันไม่แน่ใจว่าคุณสามารถทำได้ในคำขอเดียวกันหรือไม่ คุณสามารถตรวจสอบคลาสCacheControl ที่เกี่ยวข้องและส่วนหัวของCache-Control หากไม่มีพฤติกรรมดังกล่าวฉันอาจเลือกทำสองคำขอเป็นคำขอเฉพาะที่แคช (เท่านั้นถ้าแคช) แล้วตามด้วยเครือข่าย (max-age = 0) หนึ่ง
Hassan Ibraheem

นั่นคือสิ่งแรกที่ฉันนึกถึง ฉันใช้เวลาหลายวันในคลาสCacheControl และCacheStrategy แต่ความคิดที่ร้องขอสองข้อนั้นไม่สมเหตุสมผลนัก ถ้าmax-stale + max-ageผ่านมันจะขอจากเครือข่าย แต่ฉันต้องการตั้งค่าสูงสุดค้างหนึ่งสัปดาห์ สิ่งนี้ทำให้อ่านการตอบสนองจากแคชแม้ว่าจะมีเครือข่าย
osrl

คำตอบ:


189

แก้ไขสำหรับ Retrofit 2.x:

OkHttp Interceptor เป็นวิธีที่เหมาะสมในการเข้าถึงแคชเมื่อออฟไลน์:

1) สร้าง 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) การติดตั้งไคลเอนต์:

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) เพิ่มลูกค้าเพื่อติดตั้งเพิ่มเติม

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

ตรวจสอบคำตอบของ@kosiara - Bartosz Kosarzyckiด้วย คุณอาจต้องลบส่วนหัวบางส่วนออกจากการตอบกลับ


OKHttp 2.0.x (ตรวจสอบคำตอบดั้งเดิม):

ตั้งแต่ OKHttp 2.0.x HttpResponseCacheคือCache, คือsetResponseCache setCacheดังนั้นคุณควรจะ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);

คำตอบเดิม:

ปรากฎว่าการตอบสนองของเซิร์ฟเวอร์จะต้องCache-Control: publicทำให้OkClientอ่านจากแคช

นอกจากนี้หากคุณต้องการขอจากเครือข่ายเมื่อพร้อมใช้งานคุณควรเพิ่มCache-Control: max-age=0ส่วนหัวคำขอ คำตอบนี้แสดงวิธีการกำหนดพารามิเตอร์ นี่คือวิธีที่ฉันใช้:

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

(ผมสงสัยว่าทำไมถึงไม่ได้ทำงานเปิดออกฉันลืมที่จะตั้งค่าแคชที่แท้จริงสำหรับ OkHttpClient กับการใช้งานดูรหัสในคำถามหรือใน. คำตอบนี้ .)
Jonik

2
เพียงแค่คำแนะนำ: HttpResponseCache has been renamed to Cache.** Install it with OkHttpClient.setCache(...) instead of OkHttpClient.setResponseCache(...).
Henrique de Sousa

2
ฉันไม่ได้รับ interceptor ที่เรียกว่าเมื่อเครือข่ายไม่พร้อมใช้งาน ฉันไม่แน่ใจว่าจะทำอย่างไรเมื่อเครือข่ายไม่พร้อมใช้งานจะถูกโจมตี ฉันทำอะไรบางอย่างหายไปหรือเปล่า
Androidme

2
เป็นif (Utils.isNetworkAvailable(context))ที่ถูกต้องหรือมันควรจะเป็นตรงกันข้ามคือif (!Utils.isNetworkAvailable(context))?
ericn

2
ฉันใช้ Retrofit 2.1.0 และเมื่อโทรศัพท์ออฟไลน์public okhttp3.Response intercept(Chain chain) throws IOExceptionไม่เคยถูกเรียกมันจะถูกเรียกก็ต่อเมื่อฉันออนไลน์
ericn

28

ผู้ใช้ทั้งหมดข้างต้นไม่ได้ผลสำหรับฉัน ฉันพยายามที่จะใช้แคชแบบออฟไลน์ในretrofit 2.0.0-beta2 ฉันเพิ่มตัวดักโดยใช้okHttpClient.networkInterceptors()วิธีการ แต่ได้รับjava.net.UnknownHostExceptionเมื่อฉันพยายามใช้แคชออฟไลน์ ปรากฎว่าฉันต้องเพิ่มokHttpClient.interceptors()เช่นกัน

ปัญหาคือแคชไม่ได้ถูกเขียนไปยังที่เก็บข้อมูลแฟลชเนื่องจากเซิร์ฟเวอร์ส่งคืนPragma:no-cacheซึ่งป้องกันไม่ให้ OkHttp จัดเก็บการตอบกลับ แคชออฟไลน์ไม่ทำงานแม้ว่าจะแก้ไขค่าส่วนหัวคำขอก็ตาม หลังจากการทดลองและข้อผิดพลาดฉันได้รับแคชให้ทำงานโดยไม่ต้องแก้ไขด้านหลังโดยลบ pragma จาก reponse แทนคำขอ -response.newBuilder().removeHeader("Pragma");

ชุดติดตั้งเพิ่มเติม: 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
ดูเหมือนว่าคุณinterceptors ()และnetworkInterceptors ()เหมือนกัน ทำไมคุณทำซ้ำสิ่งนี้
toobsco42

ตัวดักต่างชนิดอ่านได้ที่นี่ github.com/square/okhttp/wiki/Interceptors
Rohit Bandil

ใช่ แต่พวกเขาทั้งสองทำสิ่งเดียวกันดังนั้นฉันค่อนข้างแน่ใจว่า 1 interceptor ควรจะเพียงพอใช่ไหม
Ovidiu Latcu

มีเหตุผลที่เฉพาะเจาะจงหรือไม่ที่จะไม่ใช้อินสแตนซ์ของตัวดักจับเดียวกันสำหรับทั้งสอง.networkInterceptors().add()และinterceptors().add()?
ccpizza

22

ทางออกของฉัน:

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
ไม่ทำงานสำหรับฉัน ... รับ 504 คำขอที่ไม่น่าพอใจ (เฉพาะในกรณีที่แคชไว้เท่านั้น)
34414

วิธีแก้ปัญหาของคุณเท่านั้นที่ช่วยฉันขอบคุณมาก เสียเวลา 2 วันในการเลื่อนลง
ЯрославМовчан

1
ใช่วิธีแก้ปัญหาการทำงานเฉพาะในกรณีของฉัน (ชุดติดตั้งเพิ่ม 1.9.x + okHttp3)
Hoang Nguyen Huu

1
ทำงานร่วมกับ Retrofit RETROFIT_VERSION = 2.2.0 OKHTTP_VERSION = 3.6.0
Tadas Valaitis

วิธีเพิ่ม builder.addheader () ในวิธีการนี้เพื่อเข้าถึง API ด้วยการอนุญาต?
Abhilash

6

คำตอบคือใช่จากคำตอบข้างต้นฉันเริ่มเขียนการทดสอบหน่วยเพื่อตรวจสอบกรณีการใช้ที่เป็นไปได้ทั้งหมด:

  • ใช้แคชเมื่อออฟไลน์
  • ใช้การตอบกลับที่แคชก่อนจนกว่าเครือข่ายจะหมดอายุ
  • ใช้เครือข่ายก่อนจากนั้นแคชสำหรับคำขอบางอย่าง
  • อย่าเก็บในแคชสำหรับคำตอบบางอย่าง

ฉันสร้าง lib ตัวช่วยเล็ก ๆ เพื่อกำหนดค่าแคช OKHttp ได้อย่างง่ายดายคุณสามารถดู unittest ที่เกี่ยวข้องได้ที่นี่ใน Github: https://github.com/ncornette/OkCacheControl/blob/master/okcache-control/src/test/java/com/ ncornette / แคช / OkCacheControlTest.java

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

อย่างที่คุณเห็นแคชสามารถใช้ได้แม้ว่าจะหมดอายุแล้วก็ตาม หวังว่ามันจะช่วย


2
lib ของคุณยอดเยี่ยมมาก! ขอบคุณสำหรับการทำงานหนักของคุณ The lib: github.com/ncornette/OkCacheControl
Hoang Nguyen Huu


2

แคชที่มี Retrofit2 และ 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 () วิธีการคงที่:

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

จากนั้นเพิ่มลูกค้าลงในเครื่องมือสร้างชุดติดตั้งเพิ่มเติม:

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

แหล่งที่มาดั้งเดิม: https://newfivefour.com/android-retrofit2-okhttp3-cache-network-request-offline.html


1
เมื่อฉันโหลดครั้งแรกด้วยโหมดออฟไลน์มันล้มเหลว! มิฉะนั้นจะทำงานอย่างถูกต้อง
Zafer Celaloglu

สิ่งนี้ไม่ได้ผลสำหรับฉัน ฉันคัดลอกวางมันพยายามหลังจากที่ฉันพยายามที่จะรวมหลักการของมัน แต่ทำสุทธิให้ทำงาน
บอย

App.sApp.getCacheDir () สิ่งนี้ทำอะไร?
Huzaifa Asif
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.