오프라인 일 때 OKHttp로 Retrofit을 사용하여 캐시 데이터를 사용할 수 있음


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

그리고 이것은 Cache-Control 헤더를 가진 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 헤더로 응답합니까?
하산 이브라힘

그것이 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 인터셉터는 오프라인 일 때 캐시에 액세스하는 올바른 방법입니다.

1) 인터셉터 생성 :

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 HttpResponseCacheCache이므로 setResponseCacheis 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
네트워크를 사용할 수 없을 때 인터셉터가 호출되지 않습니다. 네트워크를 사용할 수없는 상태가 어떻게 될 수 있는지 잘 모르겠습니다. 여기에 뭔가 빠졌습니까?
Androidme

2
if (Utils.isNetworkAvailable(context))올바른거나, 즉 반전하도록되어 if (!Utils.isNetworkAvailable(context))?
ericn

2
Retrofit 2.1.0을 사용하고 있으며 전화가 오프라인 일 때 전화를 public okhttp3.Response intercept(Chain chain) throws IOException
걸지

28

위의 모든 답변은 나를 위해 작동하지 않았습니다. retrofit 2.0.0-beta2 에서 오프라인 캐시를 구현하려고했습니다 . okHttpClient.networkInterceptors()메소드를 사용하여 인터셉터를 추가 했지만 java.net.UnknownHostException캐시를 오프라인으로 사용하려고 할 때 수신되었습니다 . 내가 추가해야한다는 것이 밝혀졌습니다 okHttpClient.interceptors().

문제는 서버가 리턴 Pragma:no-cache되어 OkHttp가 응답을 저장하지 못 하기 때문에 캐시가 플래시 스토리지에 기록되지 않았다는 것 입니다. 요청 헤더 값을 수정 한 후에도 오프라인 캐시가 작동하지 않았습니다. 시행 착오 후에 요청 대신 pragma를 응답에서 제거하여 백엔드 측을 수정하지 않고 캐시가 작동하도록했습니다.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/ 인터셉터
Rohit Bandil

예, 그러나 둘 다 동일한 작업을 수행하므로 1 개의 인터셉터가 충분해야합니다.
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 불만족스러운 요청 받기 (캐시 전용)
zacharia

당신의 솔루션 만 도움이됩니다. 고마워. 아래로 스크롤 2 일 낭비
Ярослав Мовчан

1
내 경우에는 유일하게 작동하는 솔루션입니다. (Retrofit 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

위의 답변을 바탕으로 대답은 예입니다. 가능한 모든 사용 사례를 확인하기 위해 단위 테스트를 작성하기 시작했습니다.

  • 오프라인 일 때 캐시 사용
  • 만료 될 때까지 먼저 캐시 된 응답을 사용한 다음 네트워크
  • 네트워크를 먼저 사용한 다음 일부 요청에 대해 캐시
  • 일부 응답을 위해 캐시에 저장하지 마십시오

OKHttp 캐시를 쉽게 구성하기 위해 작은 도우미 라이브러리를 만들었습니다 .Github에서 관련 단위 테스트를 볼 수 있습니다 : https://github.com/ncornette/OkCacheControl/blob/master/okcache-control/src/test/java/com/ ncornette / cache / 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는 훌륭합니다! 열심히 노력해 주셔서 감사합니다. 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

이것은 나를 위해 작동하지 않습니다. 나는 그것의 원리를 통합하려고 시도한 후에 그것을 복사하여 붙여 넣었지만, 그것을 작동 시키려면 그물을 사용하십시오.
Boy

App.sApp.getCacheDir ()이 기능은 무엇입니까?
Huzaifa Asif
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.