Android改造OkHttp离线缓存

时间:2020-02-23 14:29:13  来源:igfitidea点击:

在本教程中,我们将在android应用程序中讨论和实现离线缓存。
我们将使用Retrofit和Okhttp库。

离线缓存

在没有互联网的情况下打开应用程序并且看不到以前的数据是很常见的情况。
首先想到的两种处理加载网络请求的方法是:

  • 共享首选项
  • SQLite的

使用它们中的任何一个都具有相当多的缺点(以及更多要编写的代码)。

虽然在SharedPreferences中添加数据很容易。
检索所需的数据非常耗时。
加上可扩展性是一个问题。

带表的SQLite使得将来很难进行许多更改。
另外,SQLite操作繁重。

OkHtttp缓存内置的HTTP响应时,为什么同时使用两者?

缓存请求

我们知道OkHttp是Retrofit的默认HttpClient。
OkHttp带有功能强大的拦截器组件。

拦截器通常有两种类型:

  • 应用程序拦截器–为您提供最终答案。

  • 网络拦截器–拦截中间请求。

使用拦截器,您可以阅读和修改请求。
显然,我们将在响应中添加缓存控件。

缓存控制是一个标头,用于指定客户端请求和服务器响应中的缓存策略。

您无法缓存POST请求。
只能缓存GET请求。

在拦截器内部,您需要获取" chain.request()"以获取当前请求并向其中添加缓存选项。

例如,我们可以向请求添加标头" Cache-Control",例如:"" public,仅当缓存时,max-stale = 60""

然后执行一个chain.proceed(request)来处理修改后的请求以返回响应。

最大年龄vs最大stale

max-age是最早的限制(下限),直到可以从缓存中返回响应为止。
max-stale是无法返回缓存的最高限制。

在下一节中,我们将使用OkHttp作为客户端并使用RxJava进行改造请求。
我们将缓存请求,以便在没有互联网/获取最新请求的问题的情况下,下次可以显示它们。

项目结构

app的build.gradle如下:

implementation('com.squareup.retrofit2:retrofit:2.1.0') {
      exclude module: 'okhttp'
  }
  implementation 'com.google.code.gson:gson:2.8.2'
  implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
  implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1'
  implementation 'com.squareup.okhttp3:okhttps:3.10.0'

  implementation 'io.reactivex.rxjava2:rxjava:2.1.9'
  implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
  implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'

代码

下面给出了activity_main.xml布局的代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="https://schemas.android.com/apk/res/android"
  xmlns:app="https://schemas.android.com/apk/res-auto"
  xmlns:tools="https://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity">

  <TextView
      android:id="@+id/textView"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Hello World! See this space for jokes"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintLeft_toLeftOf="parent"
      app:layout_constraintRight_toRightOf="parent"
      app:layout_constraintTop_toTopOf="parent" 

  <Button
      android:id="@+id/button"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginTop="16dp"
      android:text="GET RANDOM JOKE"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toBottomOf="@+id/textView" 

</android.support.constraint.ConstraintLayout>

APIService.java的代码如下:

package com.theitroad.androidretrofitofflinecaching;

import io.reactivex.Observable;
import retrofit2.http.GET;
import retrofit2.http.Path;

public interface APIService {

  String BASE_URL = "https://api.chucknorris.io/jokes/";

  @GET("{path}")
  Observable<Jokes> getRandomJoke(@Path("path") String path);
}

下面给出了Jokes.java模型类的代码:

package com.theitroad.androidretrofitofflinecaching;

import com.google.gson.annotations.SerializedName;

public class Jokes {

  @SerializedName("url")
  public String url;
  @SerializedName("icon_url")
  public String icon_url;
  @SerializedName("value")
  public String value;
}

MainActivity.java的代码如下:

package com.theitroad.androidretrofitofflinecaching;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import com.google.gson.Gson;

import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
import okhttp3.Cache;
import okhttp3.CacheControl;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;

import static com.theitroad.androidretrofitofflinecaching.APIService.BASE_URL;

public class MainActivity extends AppCompatActivity {

  TextView textView;
  Button btnGetRandomJoke;

  APIService apiService;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);

      textView = findViewById(R.id.textView);
      btnGetRandomJoke = findViewById(R.id.button);

      setupRetrofitAndOkHttp();

      btnGetRandomJoke.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              getRandomJokeFromAPI();

          }
      });

  }

  private void setupRetrofitAndOkHttp() {

      HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor();
      httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);

      File httpCacheDirectory = new File(getCacheDir(), "offlineCache");

      //10 MB
      Cache cache = new Cache(httpCacheDirectory, 10 * 1024 * 1024);

      OkHttpClient httpClient = new OkHttpClient.Builder()
              .cache(cache)
              .addInterceptor(httpLoggingInterceptor)
              .addNetworkInterceptor(provideCacheInterceptor())
              .addInterceptor(provideOfflineCacheInterceptor())
              .build();

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

      apiService = retrofit.create(APIService.class);

  }

  public void getRandomJokeFromAPI() {
      Observable<Jokes> observable = apiService.getRandomJoke("random");
      observable.subscribeOn(Schedulers.newThread()).
              observeOn(AndroidSchedulers.mainThread())
              .map(new Function<Jokes, String>() {
                  @Override
                  public String apply(Jokes jokes) throws Exception {
                      return jokes.value;
                  }
              }).subscribe(new Observer<String>() {
          @Override
          public void onSubscribe(Disposable d) {
          }

          @Override
          public void onNext(String s) {
              textView.setText(s);
          }

          @Override
          public void onError(Throwable e) {
              Toast.makeText(getApplicationContext(), "An error occurred in the Retrofit request. Perhaps no response/cache", Toast.LENGTH_SHORT).show();
          }

          @Override
          public void onComplete() {

          }
      });

  }

  private Interceptor provideCacheInterceptor() {
      
      return new Interceptor() {
          @Override
          public Response intercept(Chain chain) throws IOException {
              Request request = chain.request();
              Response originalResponse = chain.proceed(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-stale=0")) {

                  CacheControl cc = new CacheControl.Builder()
                          .maxStale(1, TimeUnit.DAYS)
                          .build();

                  request = request.newBuilder()
                          .cacheControl(cc)
                          .build();

                  return chain.proceed(request);

              } else {
                  return originalResponse;
              }
          }
      };

  }

  private Interceptor provideOfflineCacheInterceptor() {
      
      return new Interceptor() {
          @Override
          public Response intercept(Chain chain) throws IOException {
              try {
                  return chain.proceed(chain.request());
              } catch (Exception e) {

                  CacheControl cacheControl = new CacheControl.Builder()
                          .onlyIfCached()
                          .maxStale(1, TimeUnit.DAYS)
                          .build();

                  Request offlineRequest = chain.request().newBuilder()
                          .cacheControl(cacheControl)
                          .build();
                  return chain.proceed(offlineRequest);
              }
          }
      };
  }
}

OkHttpClient Builder中拦截器的顺序很重要。
" addNetworkInterceptor"将缓存控件添加到请求中。

在" addInterceptor"中,调用" provideOfflineCacheInterceptor"。
如果有一个异常(通常是一个ConnectException或者NoRouteFoundException),则再次重试该请求,这次使用一个标头从缓存中获取响应。

或者,您可以将标头中的缓存设置为:

return originalResponse.newBuilder()
                          .header("Cache-Control", "public, max-stale=" + 60 * 60 * 24)
                          .build();

添加removeHeader(" Pragma")是一个好习惯。

因此,我们在第一个改装请求后就禁用了设备上的wifi。
而且我们仍然能够从Http Cache加载响应。