Preface: Some time ago, when developing APPs, I often couldn't get the data obtained from the network due to the user's device environment, so the results displayed on the APP are always a blank box. This situation is extremely bad for the user experience. Therefore, I thought hard and decided to start with OKHTTP (because the network request framework I used in the project is OKHTTP), and I wrote such a network data cache interceptor.
OK, then we decided to start writing. Let me first talk about my ideas:
Ideas
Since we are writing about network data cache interceptors, which mainly utilize the powerful interceptor function of OKHTTP, what data should we cache, or under what circumstances should we enable the data cache mechanism?
First: POST requests are supported, because the official has provided a cache interceptor, but one disadvantage is that they can only cache the data requested by GET, but they do not support POST.
Second: When the network is normal, it is to fetch data from the network. If the network is abnormal, such as TimeOutException UnKnowHostException and other problems, then we need to cache and retrieve data.
Third: If the data fetched from the cache is empty, then we still need to let this request go through the remaining normal process.
Fourth: The caller must have complete control over the caching mechanism and can selectively decide whether to cache the data according to his or her business needs.
Fifth: It must be simple to use, which is the most important point.
OK, we have listed five points above, which are our general ideas. Let’s talk about the code part now:
Code Article
Caching framework: The cache framework I use here is DiskLruCache https://github.com/JakeWharton/DiskLruCache This cache framework can be stored locally and has been approved by Google. This is also the main reason for choosing this framework. I also encapsulate a CacheManager class to the cache framework:
import android.content.Context;import android.content.pm.PackageInfo;import android.content.pm.PackageManager;import com.xiaolei.OkhttpCacheInterceptor.Log.Log;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.io.OutputStream;import java.io.UnsupportedEncodingException;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;/** * Created by xiaolei on 2017/5/17. */public class CacheManager{ public static final String TAG = "CacheManager"; //max cache size 10mb private static final long DISK_CACHE_SIZE = 1024 * 10; private static final int DISK_CACHE_INDEX = 0; private static final String CACHE_DIR = "responses"; private DiskLruCache mDiskLruCache; private volatile static CacheManager mCacheManager; public static CacheManager getInstance(Context context) { if (mCacheManager == null) { synchronized (CacheManager.class) { if (mCacheManager == null) { mCacheManager = new CacheManager(context); } } } return mCacheManager; } private CacheManager(Context context) { File diskCacheDir = getDiskCacheDir(context, CACHE_DIR); if (!diskCacheDir.exists()) { boolean b = diskCacheDir.mkdir(); Log.d(TAG, "!diskCacheDir.exists() --- diskCacheDir.mkdir()=" + b); } if (diskCacheDir.getUsableSpace() > DISK_CACHE_SIZE) { try { mDiskLruCache = DiskLruCache.open(diskCacheDir, getAppVersion(context), 1/*How many files correspond to a key*/, DISK_CACHE_SIZE); Log.d(TAG, "mDiskLruCache created"); } catch (IOException e) { e.printStackTrace(); } } } /** * Synchronously set cache*/ public void putCache(String key, String value) { if (mDiskLruCache == null) return; OutputStream os = null; try { DiskLruCache.Editor editor = mDiskLruCache.edit(encryptMD5(key)); os = editor.newOutputStream(DISK_CACHE_INDEX); os.write(value.getBytes()); os.flush(); editor.commit(); mDiskLruCache.flush(); } catch (IOException e) { e.printStackTrace(); } finally { if (os != null) { try { os.close(); } catch (IOException e) { e.printStackTrace(); } } } } /** * Asynchronously set cache*/ public void setCache(final String key, final String value) { new Thread() { @Override public void run() { putCache(key, value); } }.start(); } /** * Synchronously getCache getCache(String key) { if (mDiskLruCache == null) { return null; } FileInputStream fis = null; ByteArrayOutputStream bos = null; try { DiskLruCache.Snapshot snapshot = mDiskLruCache.get(encryptMD5(key)); if (snapshot != null) { fis = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX); bos = new ByteArrayOutputStream(); byte[] buf = new byte[1024]; int len; while ((len = fis.read(buf)) != -1) { bos.write(buf, 0, len); } byte[] data = bos.toByteArray(); return new String(data); } } catch (IOException e) { e.printStackTrace(); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } if (bos != null) { try { bos.close(); } catch (IOException e) { e.printStackTrace(); } } } return null; } /** * Asynchronously getCache*/ public void getCache(final String key, final CacheCallback callback) { new Thread() { @Override public void run() { String cache = getCache(key); callback.onGetCache(cache); } }.start(); } /** * Remove cache*/ public boolean removeCache(String key) { if (mDiskLruCache != null) { try { return mDiskLruCache.remove(encryptMD5(key)); } catch (IOException e) { e.printStackTrace(); } } return false; } /** * Get cache directory*/ private File getDiskCacheDir(Context context, String uniqueName) { String cachePath = context.getCacheDir().getPath(); return new File(cachePath + File.separator + uniqueName); } /** * MD5 encoding of the string*/ public static String encryptMD5(String string) { try { byte[] hash = MessageDigest.getInstance("MD5").digest( string.getBytes("UTF-8")); StringBuilder hex = new StringBuilder(hash.length * 2); for (byte b : hash) { if ((b & 0xFF) < 0x10) { hex.append("0"); } hex.append(Integer.toHexString(b & 0xFF)); } return hex.toString(); } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { e.printStackTrace(); } return string; } /** * Get APP version number*/ private int getAppVersion(Context context) { PackageManager pm = context.getPackageManager(); try { PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0); return pi == null ? 0 : pi.versionCode; } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } return 0; }} CacheInterceptor interceptor: uses OkHttp's Interceptor interceptor mechanism to intelligently judge cache scenarios and network conditions, and handle different scenarios.
import android.content.Context;import com.xiaolei.OkhttpCacheInterceptor.Catch.CacheManager;import com.xiaolei.OkhttpCacheInterceptor.Log.Log;import java.io.IOException;import okhttp3.FormBody;import okhttp3.Interceptor;import okhttp3.Protocol;import okhttp3.Request;import okhttp3.Response;import okhttp3.ResponseBody;/** * Cache class of string* Created by xiaolei on 2017/12/9. */public class CacheInterceptor implements Interceptor{ private Context context; public void setContext(Context context) { this.context = context; } public CacheInterceptor(Context context) { this.context = context; } @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); String cacheHead = request.header("cache"); String cache_control = request.header("Cache-Control"); if ("true".equals(cacheHead) || // It means to cache(cache_control != null && !cache_control.isEmpty())) // It also supports cache headers for WEB-side protocols { long oldnow = System.currentTimeMillis(); String url = request.url().url().toString(); String responseStr = null; String reqBodyStr = getPostParams(request); try { Response response = chain.proceed(request); if (response.isSuccessful()) // Cache processing is performed only after the network request returns successfully. Otherwise, 404 is stored in the cache. Isn't it a joke? { ResponseBody responseBody = response.body(); if (responseBody != null) { responseStr = responseBody.string(); if (responStr == null) { responseStr = ""; } CacheManager.getInstance(context).setCache(CacheManager.encryptMD5(url + reqBodyStr), responseStr);//Storage the cache, use the link + parameter to MD5 encoding as KEY storage Log.i("HttpRetrofit", "--> Push Cache:" + url + " :Success"); } return getOnlineResponse(response, responseStr); } else { return chain.proceed(request); } } catch (Exception e) { Response response = getCacheResponse(request, oldnow); // An exception occurred, I started to cache here, but it may not be cached, so I need to throw it to the next round of processing for a long time if (response == null) { return chain.proceed(request);//Drop to the next round of processing} else { return response; } } } else { return chain.proceed(request); } } private Response getCacheResponse(Request request, long oldNow) { Log.i("HttpRetrofit", "--> Try to Get Cache ---------"); String url = request.url().url().toString(); String params = getPostParams(request); String cacheStr = CacheManager.getInstance(context).getCache(CacheManager.encryptMD5(url + params));//Get cache, use link + parameters to MD5 encoding to KEY to get if (cacheStr == null) { Log.i("HttpRetrofit", "<-- Get Cache Failure -----------"); return null; } Response response = new Response.Builder() .code(200) .body(ResponseBody.create(null, cacheStr)) .request(request) .message("OK") .protocol(Protocol.HTTP_1_0) .build(); long useTime = System.currentTimeMillis() - oldNow; Log.i("HttpRetrofit", "<-- Get Cache: " + response.code() + " " + response.message() + " " + url + " (" + useTime + "ms)"); Log.i("HttpRetrofit", cacheStr + ""); return response; } private Response getOnlineResponse(Response response, String body) { ResponseBody responseBody = response.body(); return new Response.Builder() .code(response.code()) .body(ResponseBody.create(responseBody == null ? null : responseBody.contentType(), body)) .request(response.request()) .message(response.message()) .protocol(response.protocol()) .build(); } /** * Get in Post mode. Parameters sent to the server * * @param request * @return */ private String getPostParams(Request request) { String reqBodyStr = ""; String method = request.method(); if ("POST".equals(method)) // If it is Post, parse each parameter as much as possible { StringBuilder sb = new StringBuilder(); if (request.body() instanceof FormBody) { FormBody body = (FormBody) request.body(); if (body != null) { for (int i = 0; i < body.size(); i++) { sb.append(body.encodedName(i)).append("=").append(body.encodedValue(i)).append(","); } sb.delete(sb.length() - 1, sb.length()); } reqBodyStr = sb.toString(); sb.delete(0, sb.length()); } } return reqBodyStr; }}The above is the main idea and the main implementation code. Let’s talk about the usage method now
How to use:
gradle uses:
compile 'com.xiaolei:OkhttpCacheInterceptor:1.0.0'
Since it has just been submitted to Jcenter, it may not be able to pull it down (it has not been reviewed yet). An anxious reader can add my maven link to the repositories in your Project:build.gradle:
allprojects { repositories { maven{url 'https://dl.bintray.com/kavipyouxiang/maven'} }}We create a new project, and the project screenshot is as follows:
Project screenshot
demo is very simple, one main page, one bean, one Retrofit, one network request interface
Note that because it is a network, cache, and related, there is no doubt that we need to add network request permissions and file read and write permissions in the manifest:
<uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
When using it, you just need to add an Interceptor to your OKHttpClient:
client = new OkHttpClient.Builder() .addInterceptor(new CacheInterceptor(context))//Add cache interceptor, add cache support.retryOnConnectionFailure(true)//Failed reconnect.connectTimeout(30, TimeUnit.SECONDS)//The network request timeout unit is seconds.build();
If you want to cache data of which interface, then for a long time, add a request header for your network interface. The CacheHeaders.java class contains all the situations. Generally, only CacheHeaders.NORMAL is needed.
public interface Net{ @Headers(CacheHeaders.NORMAL) // Here is the key @FormUrlEncoded @POST("geocoding") public Call<DataBean> getIndex(@Field("a") String a);}Business code:
Net net = retrofitBase.getRetrofit().create(Net.class); Call<DataBean> call = net.getIndex("Suzhou City"); call.enqueue(new Callback<DataBean>() { @Override public void onResponse(Call<DataBean> call, Response<DataBean> response) { DataBean data = response.body(); Date date = new Date(); textview.setText(date.getMinutes() + " " + date.getSeconds() + ":/n" + data + ""); } @Override public void onFailure(Call<DataBean> call, Throwable t) { textview.setText("Request failed!"); } });If our network request is successful, we will output text on the interface, and add the current time. If the network fails, the output of a request fails.
Probably this is the code, the detailed code will be posted at the end of the article
Look at the effect: Demo
Here we demonstrate that from normal network to abnormal network, and then returning to normal situations.
Ending
The above chapter is the entire process from ideas, code, and then to renderings. Here is the address of DEMO. If you like it, you can click Start.
Demo address: https://github.com/xiaolei123/OkhttpCacheInterceptor
The above is all the content of this article. I hope it will be helpful to everyone's learning and I hope everyone will support Wulin.com more.