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

そして、これは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にキャッシュからの応答を読み取らせるために他にすべきことはありますか?

EDIT: OKHttp 2.0.xのがあるのでHttpResponseCacheありCachesetResponseCachesetCache


1
呼び出しているサーバーは適切なCache-Controlヘッダーで応答していますか?
ハッサンイブラヒーム2014年

それがCache-Control: s-maxage=1209600, max-age=1209600十分かどうかはわかりませんが、これを返します。
osrl 2014年

publicオフラインで機能させるには、キーワードが応答ヘッダーに含まれている必要があるようです。ただし、これらのヘッダーでは、利用可能な場合、OkClientはネットワークを使用できません。とにかく、ネットワークを使用できるようにキャッシュポリシー/戦略を設定する方法はありますか?
osrl 2014年

同じリクエストでそれができるかどうかはわかりません。関連するCacheControlクラスとCache-Controlヘッダーを確認できます。そのような動作がない場合は、おそらくキャッシュされた唯一のリクエスト(only-if-cached)とそれに続くネットワーク(max-age = 0)の2つのリクエストを行うことを選ぶでしょう。
ハッサンイブラヒーム2014年

それが最初に思い浮かんだ。私はそのCacheControlクラスとCacheStrategyクラスで何日も過ごしました。しかし、2つのリクエストのアイデアはあまり意味がありませんでした。max-stale + max-age渡された場合、ネットワークからのリクエストを行います。しかし、私はmax-staleを週に設定したいと思います。これにより、ネットワークが利用可能であっても、キャッシュから応答を読み取ります。
osrl 2014年

回答:


189

Retrofit 2.x用に編集します。

OkHttp Interceptorは、オフライン時にキャッシュにアクセスする正しい方法です。

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 HttpResponseCacheはなのでCachesetResponseCacheです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 2016

2
Retrofit 2.1.0を使用していて、電話がオフラインのpublic okhttp3.Response intercept(Chain chain) throws IOExceptionときに呼び出されず、オンラインのときにのみ呼び出される
ericn

28

上記のすべてのアンサーがうまくいきませんでした。レトロフィット2.0.0-beta2にオフラインキャッシュを実装しようとしました。okHttpClient.networkInterceptors()メソッドを使用してインターセプターを追加しましたがjava.net.UnknownHostException、キャッシュをオフラインで使用しようとすると受け取りました。私okHttpClient.interceptors()も追加する必要があることがわかりました。

問題は、サーバーが戻りPragma:no-cache、OkHttpが応答を保存できないため、キャッシュがフラッシュストレージに書き込まれなかったことです。リクエストヘッダーの値を変更した後でも、オフラインキャッシュが機能しませんでした。試行錯誤の後、リクエストの代わりに応答からプラグマを削除して、バックエンド側を変更せずにキャッシュを機能させました-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 2016年

さまざまなタイプのインターセプターがここにあります。github.com/square/okhttp/wiki/Interceptors
Rohit Bandil 2017年

はい、しかしどちらも同じことをするので、インターセプターは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
Unsatisfiable

あなたの解決策だけが私を助けてくれて、どうもありがとう。スクロールダウンして2日間廃棄物
ЯрославМовчанに

1
うん、私の場合唯一の実用的な解決策。(Retrofit 1.9.x + okHttp3)
Hoang Nguyen Huu

1
レトロフィットで動作RETROFIT_VERSION = 2.2.0 OKHTTP_VERSION = 3.6.0
Tadas Valaitis 2017

このメソッドにbuilder.addheader()を追加して、承認を得てAPIにアクセスする方法
-Abhilash

6

答えはYESです。上記の回答に基づいて、考えられるすべてのユースケースを検証するための単体テストの作成を開始しました。

  • オフライン時にキャッシュを使用する
  • 期限切れになるまでキャッシュされた応答を最初に使用し、次にネットワーク
  • 最初にネットワークを使用してから、いくつかのリクエストをキャッシュする
  • 一部の応答ではキャッシュに保存しない

OKHttpキャッシュを簡単に設定するための小さなヘルパーライブラリを作成しました。関連するユニットテストは、Githubで確認できます。https//github.com/ncornette/OkCacheControl/blob/master/okcache-control/src/test/java/com/ ncornette / cache / OkCacheControlTest.java

オフライン時のキャッシュの使用を示すユニットテスト:

@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は素晴らしいです!お疲れ様でした。ライブラリ
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 2016年

これは私にはうまくいきません。私はそれの原則を統合しようとした後、それを試しにコピーして貼り付けましたが、それを機能させるためにネットを使用します。
少年

App.sApp.getCacheDir()これは何をしますか?
Huzaifa Asif
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.