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

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



답변

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


답변

위의 모든 답변은 나를 위해 작동하지 않았습니다. 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();

}


답변

내 해결책 :

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


답변

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

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

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

보시다시피 캐시가 만료 된 경우에도 캐시를 사용할 수 있습니다. 그것이 도움이되기를 바랍니다.


답변

@ kosiara-bartosz-kasarzycki의 답변을 바탕으로, retrofit, okhttp, rxjava 및 guava를 사용하여 메모리-> 디스크-> 네트워크에서 올바르게로드하는 샘플 프로젝트를 만들었습니다.
https://github.com/digitalbuddha/StoreDemo


답변

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


답변