RxJava2+Retrofit2+RxLifecycle3+OkHttp3網路請求封裝
入職公司後,公司要求元件化開發,經過討論後我將網路請求框架單獨進行了封裝,不過當時框架裡將常用的util和ui均放入到了共同的Common包下,導致裡面部分程式碼耦合,後來為了降低耦合性又將Common拆分為了lib_common和lib_ui,但是lib_ui依賴了lib_common,還是導致部分程式碼耦合,最新一期為了降低元件之間的耦合性,所以單獨將lib_common中的網路請求單獨拆分,並且我又做了新的封裝和完善,總之網路框架經過3次大的改造後,使用已經非常穩定了。
使用步驟
1.在Application類中進行初始化操作
ApiConfig build = new ApiConfig.Builder() .setBaseUrl(baseUrl)//BaseUrl,這個地方加入後項目中預設使用該url .setInvalidateToken(0)//Token失效碼 .setSucceedCode(200)//成功返回碼 .setFilter("com.mp5a5.quit.broadcastFilter")//失效廣播Filter設定 //.setDefaultTimeout(2000)//響應時間,可以不設定,預設為2000毫秒 //.setHeads(headMap)//動態新增的header,也可以在其他地方通過ApiConfig.setHeads()設定 //.setOpenHttps(true)//開啟HTTPS驗證 //.setSslSocketConfigure(sslSocketConfigure)//HTTPS認證配置 .build(); build.init(this);
2.定義介面
public interface UploadApi {
@Multipart
@POST("ues/app/upload/pictures")
Observable postUpload(@Part List partList);
}
3.建立請求例項
public class UploadService { private final UploadApi mUploadApi; private UploadService() { //推薦使用這種,因為BaseUrl已經初始化了 mUploadApi = RetrofitFactory.getInstance().create(UploadApi.class); } public static UploadService getInstance() { return UploadServiceHolder.S_INSTANCE; } private static class UploadServiceHolder { private static final UploadService S_INSTANCE = new UploadService(); } public Observable uploadPic(List picList) { return mUploadApi.postUpload(picList); } }
4.傳送請求
UploadService.getInstance() .uploadPic(t) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .compose([email protected]()) .subscribe(object : BaseObserver<UploadEntity>(this, true) { override fun onSuccess(response: UploadEntity?) { toast(response?.msg!!) } })
5.效果展示
封裝思想
JDK1.8的使用
由於JDK1.8中介面可以有不需要實現的方法,所以我採用了1.8的新特新封裝了網路請求返回引數的回撥。1.8還有許多新特性,沒有升級的同學可以自己查詢資料學習一波。
public interface OnBaseResponseListener {
/**
* 成功
*
* @param response 成功引數
*/
void onSuccess(R response);
/**
* 失敗
*
* @param response 失敗引數
*/
default void onFailing(R response) {}
/**
* 錯誤
*/
default void onError() {}
}
其中onSuccess()方法是必須實現的,onFailing(R response)和onError()可以不用實現,如果專案中想處理網路請求失敗和錯誤,則需要重寫onFailing(R response)和onError()方法,如果用到了封裝的loading框和Toast,則需要super.onFailing(response)和super.onError(e),否則則可以不用super。
網路請求返回的Bean
public class BaseResponseEntity implements Serializable {
private static final long serialVersionUID = 1L;
public int code;
public String msg;
public boolean success() {
return ApiConfig.getSucceedCode() == code;
}
public int getTokenInvalid() {
return ApiConfig.getInvalidateToken();
}
}
這個是專案中所有用到網路請求返回bean的父類,其中code是返回碼,根據code判斷網路請求狀態,例如我有一個請求NBA資料的例項,我返回的bean只需要這樣寫。
public class NBAEntity extends BaseResponseEntity {
@SerializedName("error_code")
public int code;
public String reason;
public ResultBean result;
public static class ResultBean {
public String title;
}
}
由於我的專案返回的code碼這個欄位並不是code,所以可以採用起別名的方式,
@SerializedName("error_code")
public int code;
這樣就可以解決公司返回的code碼欄位和我封裝的欄位不相同的問題,當然每個bean都寫這段程式碼顯然不是特別友好,所以你可以再封裝一個bean繼承自BaseResponseEntity,然後給code起別名就可以了,那樣專案中的其他的bean只需要繼承你自己封裝的bean。
網路狀態的封裝類BaseObserver
public abstract class BaseObserver<T extends BaseResponseEntity> implements Observer<T> {
/**
* dialog 顯示文字
*/
private String mMsg;
private CustomProgressDialogUtils progressDialogUtils;
private Context mContext;
private boolean mShowLoading = false;
/**
* token失效 傳送廣播標識
*/
public static final String TOKEN_INVALID_TAG = "token_invalid";
public static final String QUIT_APP = "quit_app";
private static final String CONNECT_ERROR = "網路連線失敗,請檢查網路";
private static final String CONNECT_TIMEOUT = "連線超時,請稍後再試";
private static final String BAD_NETWORK = "伺服器異常";
private static final String PARSE_ERROR = "解析伺服器響應資料失敗";
private static final String UNKNOWN_ERROR = "未知錯誤";
private static final String RESPONSE_RETURN_ERROR = "伺服器返回資料失敗";
public BaseObserver() {
}
/**
* 如果傳入上下文,那麼表示您將開啟自定義的進度條
*
* @param context 上下文
*/
public BaseObserver(Context context, boolean isShow) {
this.mContext = context;
this.mShowLoading = isShow;
}
/**
* 如果傳入上下文,那麼表示您將開啟自定義的進度條
*
* @param context 上下文
*/
public BaseObserver(Context context, boolean isShow, String msg) {
this.mContext = context;
this.mShowLoading = isShow;
this.mMsg = msg;
}
@Override
public void onSubscribe(Disposable d) {
onRequestStart();
}
@Override
public void onNext(T response) {
onRequestEnd();
if (response.success()) {
try {
onSuccess(response);
} catch (Exception e) {
e.printStackTrace();
}
} else if (response.getTokenInvalid() == response.code) {
//token失效捕捉,傳送廣播,在專案中接收該動態廣播然後做退出登入等一些列操作
Intent intent = new Intent();
intent.setAction(ApiConfig.getQuitBroadcastReceiverFilter());
intent.putExtra(TOKEN_INVALID_TAG, QUIT_APP);
AppContextUtils.getContext().sendBroadcast(intent);
} else {
try {
onFailing(response);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void onError(Throwable e) {
onRequestEnd();
if (e instanceof retrofit2.HttpException) {
//HTTP錯誤
onException(ExceptionReason.BAD_NETWORK);
} else if (e instanceof ConnectException || e instanceof UnknownHostException) {
//連線錯誤
onException(ExceptionReason.CONNECT_ERROR);
} else if (e instanceof InterruptedIOException) {
//連線超時
onException(ExceptionReason.CONNECT_TIMEOUT);
} else if (e instanceof JsonParseException || e instanceof JSONException || e instanceof ParseException) {
//解析錯誤
onException(ExceptionReason.PARSE_ERROR);
} else {
//其他錯誤
onException(ExceptionReason.UNKNOWN_ERROR);
}
}
private void onException(ExceptionReason reason) {
switch (reason) {
case CONNECT_ERROR:
Toast.makeText(AppContextUtils.getContext(), CONNECT_ERROR, Toast.LENGTH_SHORT).show();
break;
case CONNECT_TIMEOUT:
Toast.makeText(AppContextUtils.getContext(), CONNECT_TIMEOUT, Toast.LENGTH_SHORT).show();
break;
case BAD_NETWORK:
Toast.makeText(AppContextUtils.getContext(), BAD_NETWORK, Toast.LENGTH_SHORT).show();
break;
case PARSE_ERROR:
Toast.makeText(AppContextUtils.getContext(), PARSE_ERROR, Toast.LENGTH_SHORT).show();
break;
case UNKNOWN_ERROR:
default:
Toast.makeText(AppContextUtils.getContext(), UNKNOWN_ERROR, Toast.LENGTH_SHORT).show();
break;
}
}
@Override
public void onComplete() {
onRequestEnd();
}
/**
* 網路請求成功並返回正確值
*
* @param response 返回值
*/
public abstract void onSuccess(T response);
/**
* 網路請求成功但是返回值是錯誤的
*
* @param response 返回值
*/
public void onFailing(T response) {
String message = response.msg;
if (TextUtils.isEmpty(message)) {
Toast.makeText(AppContextUtils.getContext(), RESPONSE_RETURN_ERROR, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(AppContextUtils.getContext(), message, Toast.LENGTH_SHORT).show();
}
}
/**
* 網路請求失敗原因
*/
public enum ExceptionReason {
/**
* 解析資料失敗
*/
PARSE_ERROR,
/**
* 網路問題
*/
BAD_NETWORK,
/**
* 連線錯誤
*/
CONNECT_ERROR,
/**
* 連線超時
*/
CONNECT_TIMEOUT,
/**
* 未知錯誤
*/
UNKNOWN_ERROR
}
/**
* 網路請求開始
*/
private void onRequestStart() {
if (mShowLoading) {
showProgressDialog();
}
}
/**
* 網路請求結束
*/
private void onRequestEnd() {
closeProgressDialog();
}
/**
* 開啟Dialog
*/
private void showProgressDialog() {
progressDialogUtils = new CustomProgressDialogUtils();
if (TextUtils.isEmpty(mMsg)) {
progressDialogUtils.showProgress(mContext);
} else {
progressDialogUtils.showProgress(mContext, mMsg);
}
}
/**
* 關閉Dialog
*/
private void closeProgressDialog() {
if (progressDialogUtils != null) {
progressDialogUtils.dismissProgress();
}
}
}
在onNext(T response)對網路請求成功和失敗進行處理,如果網路請求成功並且code返回的是成功的code碼,則直接用抽象方法做了處理,public abstract void onSuccess(T response),如果code返回成功但是是token失效的code碼,則使用了動態廣播將訊息傳送出去,專案中受到該廣播後,可以做退出登入等一系列操作。如果網路請求返回失敗,我只做了Toast操作。網路請求錯誤則是在onErrorr(Throwable e)中做了處理。
@Override
public void onError(Throwable e) {
onRequestEnd();
if (e instanceof retrofit2.HttpException) {
//HTTP錯誤
onException(ExceptionReason.BAD_NETWORK);
} else if (e instanceof ConnectException || e instanceof UnknownHostException) {
//連線錯誤
onException(ExceptionReason.CONNECT_ERROR);
} else if (e instanceof InterruptedIOException) {
//連線超時
onException(ExceptionReason.CONNECT_TIMEOUT);
} else if (e instanceof JsonParseException || e instanceof JSONException || e instanceof ParseException) {
//解析錯誤
onException(ExceptionReason.PARSE_ERROR);
} else {
//其他錯誤
onException(ExceptionReason.UNKNOWN_ERROR);
}
}
Retrofit封裝RetrofitFactory
public class RetrofitFactory {
private final Retrofit.Builder retrofit;
private Retrofit build;
private RetrofitFactory() {
// 指定快取路徑,快取大小100Mb
File cacheFile = new File(AppContextUtils.getContext().getCacheDir(), "HttpCache");
Cache cache = new Cache(cacheFile, 1024 * 1024 * 100);
OkHttpClient.Builder httpClientBuilder = new OkHttpClient().newBuilder()
.readTimeout(ApiConfig.getDefaultTimeout(), TimeUnit.MILLISECONDS)
.connectTimeout(ApiConfig.getDefaultTimeout(), TimeUnit.MILLISECONDS)
.addInterceptor(HttpLoggerInterceptor.getLoggerInterceptor())
.addInterceptor(new HttpHeaderInterceptor())
.addNetworkInterceptor(new HttpCacheInterceptor())
.cache(cache);
if (ApiConfig.getOpenHttps()) {
httpClientBuilder.sslSocketFactory(1 == ApiConfig.getSslSocketConfigure().getVerifyType() ?
SslSocketFactory.getSSLSocketFactory(ApiConfig.getSslSocketConfigure().getCertificateInputStream()) :
SslSocketFactory.getSSLSocketFactory(), new UnSafeTrustManager());
httpClientBuilder.hostnameVerifier(new UnSafeHostnameVerify());
}
OkHttpClient httpClient = httpClientBuilder.build();
Gson gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd HH:mm:ss")
.serializeNulls()
.registerTypeAdapterFactory(new NullTypeAdapterFactory())
.create();
retrofit = new Retrofit.Builder()
.client(httpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create());
if (!TextUtils.isEmpty(ApiConfig.getBaseUrl())) {
build = retrofit.baseUrl(ApiConfig.getBaseUrl()).build();
}
}
private static class RetrofitFactoryHolder {
private static final RetrofitFactory INSTANCE = new RetrofitFactory();
}
public static final RetrofitFactory getInstance() {
return RetrofitFactoryHolder.INSTANCE;
}
/**
* 根據Api介面類生成Api實體
*
* @param clazz 傳入的Api介面類
* @return Api實體類
*/
public <T> T create(Class<T> clazz) {
checkNotNull(build, "BaseUrl not init,you should init first!");
return build.create(clazz);
}
/**
* 根據Api介面類生成Api實體
*
* @param baseUrl baseUrl
* @param clazz 傳入的Api介面類
* @return Api實體類
*/
public <T> T create(String baseUrl, Class<T> clazz) {
return retrofit.baseUrl(baseUrl).build().create(clazz);
}
private <T> T checkNotNull(@Nullable T object, String message) {
if (object == null) {
throw new NullPointerException(message);
}
return object;
}
}
create()方法之所以有兩個,是因為當時專案中涉及到動態切換BaseUrl需求所以才封裝了一個兩個引數的create方法。
攔截器封裝
快取攔截器
public class HttpCacheInterceptor implements Interceptor {
@Override
@EverythingIsNonNull
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
//沒網強制從快取讀取
if (!NetworkUtils.isConnected(AppContextUtils.getContext())) {
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build();
Log.e("-->", "no network");
}
Response originalResponse = chain.proceed(request);
if (NetworkUtils.isConnected(AppContextUtils.getContext())) {
//有網的時候讀介面上的@Headers裡的配置
String cacheControl = request.cacheControl().toString();
return originalResponse.newBuilder()
.header("Cache-Control", cacheControl)
.removeHeader("Pragma")
.build();
} else {
return originalResponse.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=2419200")
.removeHeader("Pragma")
.build();
}
}
}
請求頭攔截器
public class HttpHeaderInterceptor implements Interceptor {
@Override
@EverythingIsNonNull
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Map<String, String> heads = ApiConfig.getHeads();
String token = ApiConfig.getToken();
Request.Builder authorization = originalRequest.newBuilder()
.header("Content-type", "application/json")
.header("Authorization", token)
.addHeader("Connection", "close")
.addHeader("Accept-Encoding", "identity");
//動態新增Header
if (null != heads) {
heads.forEach(new BiConsumer<String, String>() {
@Override
public void accept(String key, String value) {
authorization.addHeader(key, value);
}
});
}
Request build = authorization.build();
return chain.proceed(build);
}
}
log攔截器
public class HttpLoggerInterceptor {
public static HttpLoggingInterceptor getLoggerInterceptor() {
//日誌攔截器
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(message -> {
Log.e("-->", message.toString());
});
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
return interceptor;
}
}
HTTPS認證
public class SslSocketFactory {
/**
* HTTPS單向認證
*
* @return
*/
public static SSLSocketFactory getSSLSocketFactory(InputStream... certificates) {
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance(ApiConfig.getSslSocketConfigure().getCertificateType());
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
int index = 0;
for (InputStream input : certificates) {
String iAlias = Integer.toString(index++);
keyStore.setCertificateEntry(iAlias, certificateFactory.generateCertificate(input));
try {
if (null != input) {
input.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
SSLContext sslContext = SSLContext.getInstance(ApiConfig.getSslSocketConfigure().getProtocolType());
TrustManagerFactory managerF = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
managerF.init(keyStore);
sslContext.init(null, managerF.getTrustManagers(), new SecureRandom());
return sslContext.getSocketFactory();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* HTTPS雙向認證
*
* @return
*/
public static SSLSocketFactory getSSLSocketFactory() {
try {
KeyStore keyStore = KeyStore.getInstance(ApiConfig.getSslSocketConfigure().getKeystoreType());
KeyStore trustStore = KeyStore.getInstance(ApiConfig.getSslSocketConfigure().getKeystoreType());
InputStream keyInput = AppContextUtils.getContext().getAssets().open(ApiConfig.getSslSocketConfigure().getClientPriKey());
InputStream trustInput = AppContextUtils.getContext().getAssets().open(ApiConfig.getSslSocketConfigure().getTrustPubKey());
keyStore.load(keyInput, ApiConfig.getSslSocketConfigure().getClientBKSPassword().toCharArray());
trustStore.load(trustInput, ApiConfig.getSslSocketConfigure().getTruststoreBKSPassword().toCharArray());
try {
if (null != keyInput) {
keyInput.close();
}
if (null != keyInput) {
trustInput.close();
}
} catch (IOException e) {
e.printStackTrace();
}
SSLContext sslContext = SSLContext.getInstance(ApiConfig.getSslSocketConfigure().getProtocolType());
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(ApiConfig.getSslSocketConfigure().getCertificateType());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(ApiConfig.getSslSocketConfigure().getCertificateType());
trustManagerFactory.init(trustStore);
keyManagerFactory.init(keyStore, ApiConfig.getSslSocketConfigure().getClientBKSPassword().toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
return sslContext.getSocketFactory();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
public class UnSafeHostnameVerify implements HostnameVerifier {
@SuppressLint("BadHostnameVerifier")
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
}
public class UnSafeTrustManager implements X509TrustManager {
@SuppressLint("TrustAllX509TrustManager")
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@SuppressLint("TrustAllX509TrustManager")
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
Gson返回null處理
public class NullTypeAdapterFactory<T> implements TypeAdapterFactory {
@Override
@SuppressWarnings("unchecked")
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Class<T> rawType= (Class<T>) type.getRawType();
if (rawType!=String.class) {
return null;
}
return (TypeAdapter<T>) new NullAdapter();
}
}
public class NullAdapter extends TypeAdapter<String> {
@Override
public void write(JsonWriter out, String value) throws IOException {
if (null == value) {
out.nullValue();
return;
}
out.value(value);
}
@Override
public String read(JsonReader in) throws IOException {
if (JsonToken.NULL == in.peek()) {
in.nextNull();
return "";
}
return in.nextString();
}
}
使用配置類ApiConfig
這個類是對所有初始化引數進行配置的地方,比如返回碼code,BaseURL,失效InvalidateToken,請求頭Heads,是否開啟https認證等的一系配置。採用了建造者模式,就是為了方便開發者鏈式呼叫。
public class ApiConfig implements Serializable {
private static int mInvalidateToken;
private static String mBaseUrl;
private static int mDefaultTimeout = 2000;
private static int mSucceedCode;
private static String mQuitBroadcastReceiverFilter;
private static ArrayMap<String, String> mHeads;
private static String mToken = "";
private static boolean mOpenHttps;
private static SslSocketConfigure mSslSocketConfigure;
private ApiConfig(Builder builder) {
mInvalidateToken = builder.invalidateToken;
mBaseUrl = builder.baseUrl;
mDefaultTimeout = builder.defaultTimeout;
mSucceedCode = builder.succeedCode;
mQuitBroadcastReceiverFilter = builder.broadcastFilter;
mHeads = builder.heads;
mOpenHttps = builder.openHttps;
mSslSocketConfigure = builder.sslSocketConfigure;
}
public void init(Context appContext) {
AppContextUtils.init(appContext);
}
public static int getInvalidateToken() {
return mInvalidateToken;
}
public static String getBaseUrl() {
return mBaseUrl;
}
public static int getDefaultTimeout() {
return mDefaultTimeout;
}
public static int getSucceedCode() {
return mSucceedCode;
}
public static String getQuitBroadcastReceiverFilter() {
return mQuitBroadcastReceiverFilter;
}
public static ArrayMap<String, String> getHeads() {
return mHeads;
}
public static void setHeads(ArrayMap<String, String> mHeads) {
ApiConfig.mHeads = mHeads;
}
public static String getToken() {
return mToken;
}
public static void setToken(String mToken) {
ApiConfig.mToken = mToken;
}
public static boolean getOpenHttps() {
return mOpenHttps;
}
public static SslSocketConfigure getSslSocketConfigure() {
return mSslSocketConfigure;
}
public static final class Builder {
private int invalidateToken;
private String baseUrl;
private int defaultTimeout;
private int succeedCode;
private String broadcastFilter;
private ArrayMap<String, String> heads;
private boolean openHttps = false;
private SslSocketConfigure sslSocketConfigure;
public Builder setHeads(ArrayMap<String, String> heads) {
this.heads = heads;
return this;
}
public Builder setFilter(@NonNull String filter) {
this.broadcastFilter = filter;
return this;
}
public Builder setSucceedCode(int succeedCode) {
this.succeedCode = succeedCode;
return this;
}
public Builder setBaseUrl(String mBaseUrl) {
this.baseUrl = mBaseUrl;
return this;
}
public Builder setInvalidateToken(int invalidateToken) {
this.invalidateToken = invalidateToken;
return this;
}
public Builder setDefaultTimeout(int defaultTimeout) {
this.defaultTimeout = defaultTimeout;
return this;
}
public Builder setOpenHttps(boolean open) {
this.openHttps = open;
return this;
}
public Builder setSslSocketConfigure(SslSocketConfigure sslSocketConfigure) {
this.sslSocketConfigure = sslSocketConfigure;
return this;
}
public ApiConfig build() {
return new ApiConfig(this);
}
}
}
檔案請求上傳
採用RxJava進行流的切換操作。
public Single<List<MultipartBody.Part>> uploadMultiPicList(@NonNull List<File> pathList) {
return Flowable.fromIterable(pathList).concatMap((Function<File, Flowable<MultipartBody.Part>>) f -> {
Bitmap bitmap = BitmapFactory.decodeFile(f.toString());
File file = compressBitmapToFile(bitmap, AppContextUtils.getContext());
Log.e("-->檔案大小:", bytesTrans(file.length()) + ",fileSize=" + file.length() / 1024 + "kb");
RequestBody requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), file);
MultipartBody.Part imageBodyPart = MultipartBody.Part.createFormData("file", file.getName(), requestBody);
return Flowable.just(imageBodyPart);
}).collect((Callable<List<MultipartBody.Part>>) ArrayList::new, List::add)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
由於好久都沒有寫文章了,所以可能存在一些問題歡迎大家指正。
程式碼傳送門:https://github.com/Mp5A5/HttpRequest 歡迎大家fork或者star。