寫這篇博客目的是記錄下自己寫的網絡請求框架,因爲公司目前工作需要,需要一個可以動態變更BaseURL的請求框架(OEM廠商好幾個),但是,後臺還沒寫好(接口都沒定義),所以我得自己模擬網絡請求,所以還添加了攔截接口響應信息的攔截器。還有,這期間我參考了很多大神的博客:
比如動態切換BaseURL是:https://www.jianshu.com/p/2919bdb8d09a 非常感謝!!!
一、先說MVP:
1,項目結構圖奉上~超級簡單的
然後是各種base類:
首先BaseActivity:
主要是爲了防止內存泄漏,在destory時釋放資源
public abstract class SimpleBaseActivity<P extends IPresenter> extends Activity implements IView{
protected P presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//去掉標題欄
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(getLayoutId());
presenter = bindPresenter();
start();
}
//abstract類中帶有abstract標記的方法必須由子類繼承並重新,抽象就是將可變的東西讓子類去實現
public abstract int getLayoutId();
// 綁定Presenter
protected abstract P bindPresenter();
//開始了,子類可以在這個方法初始化view或其他初始化工作
public void start(){
}
@Override
public Activity getSelfActivity() {
return this;
}
@Override
protected void onDestroy() {
super.onDestroy();
if(presenter != null)
presenter.detachView();
}
}
再看實現的接口IView,主要是當Presenter中需要獲取上下文對象時,傳遞上下文對象,不會讓Presenter直接持有
public interface IView {
Activity getSelfActivity();
}
最後是BasePresenter類:實現的接口只有一個方法,實現這個接口主要方便Activity綁定Presenter
public interface IPresenter {
void detachView();
}
public abstract class BasePresenter<V extends IView> implements IPresenter {
private WeakReference<V> mWeakActivity;
//Disposable容器,收集Disposable,主要用於內存泄漏管理
private CompositeDisposable mDisposables;
public BasePresenter(V view){
attachView(view);
}
/**
* 綁定view
* @param v
*/
public void attachView(V v){
mWeakActivity = new WeakReference<V>(v);
mDisposables = new CompositeDisposable();
}
/**
* 解綁view,就在BaseActivity中銷燬activity時調用的
*/
public void detachView(){
if(null != mWeakActivity){
mDisposables.clear();
mDisposables = null;
mWeakActivity.clear();
mWeakActivity = null;
System.gc();
}
}
//獲取View
public V getView(){
if(null != mWeakActivity){
return mWeakActivity.get();
}
return null;
}
//返回是否還綁定view
protected boolean isViewAttach(){
return null != mWeakActivity && null != mWeakActivity.get();
}
/**
* @param disposable 添加Disposable到CompositeDisposable
* 通過解除disposable處理內存泄漏問題
*/
protected boolean addDisposable(Disposable disposable) {
if (isNullOrDisposed(disposable)) {
return false;
}
return mDisposables.add(disposable);
}
/**
* @param d 判斷d是否爲空或者dispose
* @return true:一次任務未開始或者已結束
*/
protected boolean isNullOrDisposed(Disposable d) {
return d == null || d.isDisposed();
}
/**
* @param d 判斷d是否dispose
* @return true:一次任務還未結束
*/
protected boolean isNotDisposed(Disposable d) {
return d != null && !d.isDisposed();
}
}
Base類結束,很簡單吧。
2,再看使用,先創建一個contract類,來約定MVP各層需要做的事兒~
public interface MainContract {
interface Model{
/**
* Model層負責處理登陸的網絡請求
* @param name 需要傳的參數
* @param observer 在presenter層傳來的,負責處理數據的邏輯
*/
void login(String name, DisposableSingleObserver<NetResponse<UserInfo>> observer);
}
interface View extends IView{
//獲取參數,也就是Model層login方法需要的name
String getParam();
/**
* 更新View,由Presenter處理數據後調用
* @param userInfo
*/
void updateView(UserInfo userInfo);
}
interface Presenter{
//Model層和View層的橋樑,負責處理數據邏輯
void login();
}
}
接下來一氣呵成看MVP各層
首先Activity層,也就是View層,主要用來更新View的:
/**
* 繼承SimpleBaseActivity<MainPresenter>,則bindPresenter則爲MainPresenter類型(因爲父類是泛型)
* 實現MainContract約束文件中的View接口
*/
public class MainActivity extends SimpleBaseActivity<MainPresenter> implements MainContract.View {
private TextView infoView;
@Override
public void start() {
super.start();
infoView = findViewById(R.id.info);
}
@Override
public int getLayoutId() {
return R.layout.activity_main;
}
//返回的是MainPresenter類型presenter,並綁定
@Override
protected MainPresenter bindPresenter() {
return new MainPresenter((MainContract.View) getSelfActivity());
}
@Override
public String getParam() {
return "李白";
}
//更新View,不許考慮網絡請求、數據處理等其他的事兒,只要更新好view就好
@Override
public void updateView(UserInfo userInfo) {
infoView.setText(userInfo.toString());
}
//界面按鈕的點擊方法,在xml文件中定義的
public void get(View view) {
//調用presenter層的login方法,開始進行網絡請求啦
presenter.login();
}
}
然後Model層,超級簡單,網絡請求之後再講~:
public class MainModel implements MainContract.Model {
@Override
public void login(String s, DisposableSingleObserver<NetResponse<UserInfo>> observer) {
//下面這行主要是用來修改baseURL的,如果不需要可以不加的
//第一個參數表示要修改哪個網絡請求的BaseURL,第二個參數表示要修改成什麼樣的URL
RetrofitUrlManager.getInstance().putDomain(Api.URL_VALUE_SECOND, Api.BASEURL2);
RetrofitHelper.getInstance()
.getRetrofit()
.create(Api.class)
.login(s)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.newThread())
.subscribeWith(observer);
}
}
最後看presenter層是如何把M層和V層連接在一起的:
public class MainPresenter extends BasePresenter<MainContract.View> implements MainContract.Presenter {
//先生成一個Model對象
private MainContract.Model model;
public MainPresenter(MainContract.View view) {
super(view);
model = new MainModel();
}
@Override
public void login() {
//處理數據
DisposableSingleObserver<NetResponse<UserInfo>> disposableSingleObserver = new DisposableSingleObserver<NetResponse<UserInfo>>(){
@Override
public void onSuccess(NetResponse<UserInfo> value) {
//如果網絡請求返回的狀態碼爲1,則讓View層更新View
if (value.getStatus() == 1)
getView().updateView(value.getData());
}
@Override
public void onError(Throwable e) {
}
};
//防止內存泄漏,所以統一管理,在destory時銷燬
addDisposable(disposableSingleObserver);
//看!調用View層的獲取參數的方法,拿到返回值傳給M層進行網絡請求,這就是橋樑啊
model.login(getView().getParam(),disposableSingleObserver);
}
}
好MVP到現在就結束了!!!!!!接下來看網絡請求吧~
二、Retrofit封裝
1,先看下Api接口,這兒想補充下關於接口的一些小知識:
抽象abstract 是將不可變的東西封裝到一起,將可變的東西讓子類去實現,是可以定義變量、常量以及方法的實現的,比如上面的BasePresenter類就是抽象類。但接口可以看錯是高層的抽象,接口不會定義變量,即使是子類實現了接口,也不能修改接口中的屬性。對於屬性值都是public static final,對於方法都是public abstract。
public interface Api {
//成員變量,默認修飾符 public static final
//成員方法,默認修飾符 public abstract
String BASEURL = "https://api.apiopen.top";//測試api
String BASEURL2 = "https://api.apiopen.second";//測試api,錯誤的
//header中添加 URL_KEY,表示這個請求是需要替換BaseUrl的
String URL_KEY = "URL_KEY";
//header中的value值,用於區分需要替換的BaseUrl是哪一個
String URL_VALUE_SECOND = "URL_VALUE_SECOND";
//這兒添加Headers是爲了修改BaseURL的,key用於識別是不是需要修改BaseURL,value用來識別需要修改哪個BaseURL
//使用時只需要在網絡請求前添加: RetrofitUrlManager.getInstance().putDomain(Api.URL_VALUE_SECOND,"https://new.address.com");
@Headers({URL_KEY+":"+URL_VALUE_SECOND})
@POST("login")
Single<NetResponse<UserInfo>> login(@Query("name") String key);
}
2,再看RetrofitHelper類,主要用於管理網絡請求,設置了下修改BaseURL,還設置了攔截響應數據的攔截器:
public class RetrofitHelper {
private static volatile RetrofitHelper instance;
private final Retrofit retrofit;
private static final int READ_TIMEOUT = 12;//讀取超時時間(秒)
private static final int CONN_TIMEOUT = 12;//連接超時時間(秒)
/**
* 在生成OKHTTP的builder時,RetrofitUrlManager.getInstance()得到的是用於修改BaseURL的類的實例,with返回的是OkHttpClient.Builder
* 如果不需要修改BaseURL,可以去掉RetrofitUrlManager.getInstance().with(),直接new OkHttpClient().newBuilder()即可
*/
private RetrofitHelper() {
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
retrofit = new Retrofit.Builder()
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(Api.BASEURL)
.client(RetrofitUrlManager.getInstance()//設置更換BaseURL的實例,不需要就不要設置了
.with(new OkHttpClient().newBuilder())
.addInterceptor(loggingInterceptor)
.addInterceptor(new simulateResponse())//攔截響應的攔截器,不需要就不要加
.connectTimeout(CONN_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.build())
.build();
}
/**
* 單例模式
* @return
*/
public static RetrofitHelper getInstance() {
if (instance == null) {
synchronized (RetrofitHelper.class) {
if (instance == null) {
instance = new RetrofitHelper();
}
}
}
return instance;
}
public Retrofit getRetrofit() {
return retrofit;
}
/**
* 打印日誌的攔截器
*/
private HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(new HttpLoggingInterceptor.Logger() {
@Override
public void log(String message) {
showLog("retrofitBack = " + message);
}
});
/**
* 用於模擬API響應數據的攔截器
* 應用場景:後臺還沒寫好接口,沒法兒測?根本沒後臺,自己想做個展示類Demo。還有好多場景
*/
private class simulateResponse implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
final HttpUrl url = request.url();//獲取路徑
//判斷是否需要攔截,如果URL是需要攔截的URL,則會根據關鍵字apiNname進行攔截
String apiNname = NetTest.isExist(url.toString());
if(null != apiNname){
//返回需要攔截的請求要進行模擬的數據
String responseContent = NetTest.getVisualResponseByApi(apiNname);
return new Response.Builder()
.code(200)
.message("模擬響應")
.body(ResponseBody.create(MediaType.parse("UTF-8"),responseContent))
.request(request)
.protocol(HTTP_1_1)
.build();
}
return chain.proceed(request);
}
}
private void showLog(String msg) {
Log.i(getClass().getName(), msg);
}
}
3,再看攔截響應的攔截器的工具類:
public class NetTest {
private static HashMap<String, String> apiResponse = new HashMap<>();
/**
* 判斷這個URL是不是需要攔截,如果需要攔截就返回interceptApi,即apiResponse的key值
* @param apiName 需要攔截的URL
* @return
*/
public static String isExist(String apiName) {
//初始化apiResponse,添加鍵值對
init();
for (String interceptApi : apiResponse.keySet()) {
if (apiName.contains(interceptApi)) {
return interceptApi;
}
}
return null;
}
/**
* 根據鍵值對獲取需要替換的響應值
* @param apiName
* @return
*/
public static String getVisualResponseByApi(String apiName) {
return apiResponse.get(apiName);
}
public static void init() {
if (apiResponse.size() > 0) {
return;
}
apiResponse.put("register", "{\"status\":1,\"msg\":\"調用成功\",\"data\":{\"userId\":\"20191118\",\"userType\":0,\"userBirthYear\":1994,\"userName\":\"王二小\",\"userStature\":\"185cm\",\"userWeight\":\"50kg\"}}");
apiResponse.put("login", "{\"status\":1,\"msg\":\"調用成功\",\"data\":{\"userId\":\"20191118\",\"userType\":0,\"userBirthYear\":1994,\"userName\":\"王二小\",\"userStature\":\"185cm\",\"userWeight\":\"50kg\"}}");
}
}
4,最後是改變BaseURL的了,不需要更改BaseURL的童鞋可以到此止步了。
先看用於更換BaseURL的實體類
public class DefaultUrlParser implements UrlParser{
private Cache<String,String> mCache;
@Override
public void init(RetrofitUrlManager retrofitUrlManager) {
//根據LRU規則,初始化一個用於管理諸多BaseURL的列表
this.mCache = new CacheLRU<>(20);
}
/**
* 主要操作,用來替換BaseURL
* @param domainUrl 用於替換的 URL 地址
* @param oldUrl 舊 URL 地址
* @return
*/
@Override
public HttpUrl parseUrl(HttpUrl domainUrl, HttpUrl oldUrl) {
if(null == domainUrl)
return oldUrl;
HttpUrl.Builder builder = oldUrl.newBuilder();
if(TextUtils.isEmpty(mCache.getValue(getKey(domainUrl,oldUrl)))){
for(int i = 0;i<oldUrl.pathSize();i++){
builder.removePathSegment(0);
}
List<String> newPathSegments = new ArrayList<>();
newPathSegments.addAll(domainUrl.encodedPathSegments());
newPathSegments.addAll(oldUrl.encodedPathSegments());
for(String PathSegment : newPathSegments) {
builder.addEncodedPathSegment(PathSegment);
}
}else {
builder.encodedPath(mCache.getValue(getKey(domainUrl,oldUrl)));
}
HttpUrl httpUrl = builder
.scheme(domainUrl.scheme())
.host(domainUrl.host())
.port(domainUrl.port())
.build();
if(TextUtils.isEmpty(mCache.getValue(getKey(domainUrl,oldUrl))))
mCache.put(getKey(domainUrl,oldUrl),httpUrl.encodedPath());
return httpUrl;
}
private String getKey(HttpUrl domainUrl,HttpUrl oldUrl){
return domainUrl.encodedPath() + oldUrl.encodedPath();
}
}
最後是修改BaseURL的工具類:
public class RetrofitUrlManager {
private static final String TAG = "RetrofitUrlManager";
private static final String GLOBAL_DOMAIN_NAME = "URL_VALUE_DEFAULT";
private final Map<String, HttpUrl> mDomainNameHub = new HashMap<>();
private final Interceptor mInterceptor;
private final List<onUrlChangeListener> mListeners = new ArrayList<>();
private UrlParser mUrlParser;
private RetrofitUrlManager() {
UrlParser urlParser = new DefaultUrlParser();
urlParser.init(this);
setUrlParser(urlParser);
this.mInterceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
return chain.proceed(processRequest(chain.request()));
}
};
}
//單例模式
private static class RetrofitUrlManagerHolder {
private static final RetrofitUrlManager INSTANCE = new RetrofitUrlManager();
}
public static final RetrofitUrlManager getInstance() {
return RetrofitUrlManagerHolder.INSTANCE;
}
//添加攔截器
public OkHttpClient.Builder with(OkHttpClient.Builder builder) {
checkNotNull(builder, "builder cannot be null");
return builder.addInterceptor(mInterceptor);
}
//對request進行修改
public Request processRequest(Request request) {
if (request == null) return request;
Request.Builder newBuilder = request.newBuilder();
//檢測header中是否存在需要更換BaseUrl的key值,如果存在則返回對應的value值
String domainName = obtainDomainNameFromHeaders(request);
HttpUrl httpUrl;
Object[] listeners = listenersToArray();
// 如果有 header,獲取 header 中 domainName 所映射的 url,若沒有,則檢查全局的 BaseUrl,未找到則爲null
if (!TextUtils.isEmpty(domainName)) {
notifyListener(request, domainName, listeners);
httpUrl = fetchDomain(domainName);
newBuilder.removeHeader(Api.URL_KEY);
} else {
notifyListener(request, GLOBAL_DOMAIN_NAME, listeners);
httpUrl = getGlobalDomain();
}
if (null != httpUrl) {
HttpUrl newUrl = mUrlParser.parseUrl(httpUrl, request.url());
Log.d(RetrofitUrlManager.TAG, "The new url is { " + newUrl.toString() + " }, old url is { " + request.url().toString() + " }");
if (listeners != null) {
for (int i = 0; i < listeners.length; i++) {
((onUrlChangeListener) listeners[i]).onUrlChanged(newUrl, request.url()); // 通知監聽器此 Url 的 BaseUrl 已被切換
}
}
return newBuilder
.url(newUrl)
.build();
}
return newBuilder.build();
}
/**
* 通知所有監聽器的 onUrlChangeListener#onUrlChangeBefore(HttpUrl, String)方法
* @param request {@link Request}
* @param domainName 域名的別名
* @param listeners 監聽器列表
*/
private void notifyListener(Request request, String domainName, Object[] listeners) {
if (listeners != null) {
for (int i = 0; i < listeners.length; i++) {
((onUrlChangeListener) listeners[i]).onUrlChangeBefore(request.url(), domainName);
}
}
}
/**
* 全局動態替換 BaseUrl, 優先級: Header中配置的 BaseUrl > 全局配置的 BaseUrl
* 除了作爲備用的 BaseUrl, 當您項目中只有一個 BaseUrl, 但需要動態切換
* 這種方式不用在每個接口方法上加入 Header, 就能實現動態切換 BaseUrl
*
* @param globalDomain 全局 BaseUrl
*/
public void setGlobalDomain(String globalDomain) {
checkNotNull(globalDomain, "globalDomain cannot be null");
synchronized (mDomainNameHub) {
mDomainNameHub.put(GLOBAL_DOMAIN_NAME, checkUrl(globalDomain));
}
}
private HttpUrl checkUrl(String url) {
HttpUrl parseUrl = HttpUrl.parse(url);
if (null == parseUrl) {
throw new RuntimeException("You've configured an invalid url : "+url);
} else {
return parseUrl;
}
}
/**
* 獲取全局 BaseUrl
*/
public synchronized HttpUrl getGlobalDomain() {
return mDomainNameHub.get(GLOBAL_DOMAIN_NAME);
}
/**
* 移除全局 BaseUrl
*/
public void removeGlobalDomain() {
synchronized (mDomainNameHub) {
mDomainNameHub.remove(GLOBAL_DOMAIN_NAME);
}
}
/**
* 存放 Domain(BaseUrl) 的映射關係
*
* @param domainName
* @param domainUrl
*/
public void putDomain(String domainName, String domainUrl) {
checkNotNull(domainName, "domainName cannot be null");
checkNotNull(domainUrl, "domainUrl cannot be null");
synchronized (mDomainNameHub) {
mDomainNameHub.put(domainName, checkUrl(domainUrl));
}
}
/**
* 取出對應 {@code domainName} 的 Url(BaseUrl)
*
* @param domainName
* @return
*/
public synchronized HttpUrl fetchDomain(String domainName) {
checkNotNull(domainName, "domainName cannot be null");
return mDomainNameHub.get(domainName);
}
/**
* 移除某個 {@code domainName}
*
* @param domainName {@code domainName}
*/
public void removeDomain(String domainName) {
checkNotNull(domainName, "domainName cannot be null");
synchronized (mDomainNameHub) {
mDomainNameHub.remove(domainName);
}
}
/**
* 清理所有 Domain(BaseUrl)
*/
public void clearAllDomain() {
mDomainNameHub.clear();
}
/**
* 存放 Domain(BaseUrl) 的容器中是否存在這個 {@code domainName}
*
* @param domainName {@code domainName}
* @return {@code true} 爲存在, {@code false} 爲不存在
*/
public synchronized boolean haveDomain(String domainName) {
return mDomainNameHub.containsKey(domainName);
}
/**
* 存放 Domain(BaseUrl) 的容器, 當前的大小
*
* @return 容量大小
*/
public synchronized int domainSize() {
return mDomainNameHub.size();
}
/**
* 可自行實現 {@link UrlParser} 動態切換 Url 解析策略
*
* @param parser {@link UrlParser}
*/
public void setUrlParser(UrlParser parser) {
checkNotNull(parser, "parser cannot be null");
this.mUrlParser = parser;
}
/**
* 註冊監聽器(當 Url 的 BaseUrl 被切換時會被回調的監聽器)
*
* @param listener 監聽器列表
*/
public void registerUrlChangeListener(onUrlChangeListener listener) {
checkNotNull(listener, "listener cannot be null");
synchronized (mListeners) {
mListeners.add(listener);
}
}
/**
* 註銷監聽器(當 Url 的 BaseUrl 被切換時會被回調的監聽器)
*
* @param listener 監聽器列表
*/
public void unregisterUrlChangeListener(onUrlChangeListener listener) {
checkNotNull(listener, "listener cannot be null");
synchronized (mListeners) {
mListeners.remove(listener);
}
}
private Object[] listenersToArray() {
Object[] listeners = null;
synchronized (mListeners) {
if (mListeners.size() > 0) {
listeners = mListeners.toArray();
}
}
return listeners;
}
//檢測header中是否存在需要更換BaseUrl的key值
private String obtainDomainNameFromHeaders(Request request) {
//查詢header中是否包含Api.URL_KEY,如果包含則表示這個request需要替換BaseUrl
List<String> headers = request.headers(Api.URL_KEY);
if (headers == null || headers.size() == 0)
return null;
//header中應該只有一個Api.URL_KEY
if (headers.size() > 1)
throw new IllegalArgumentException("Only one Domain-Name in the headers");
//返回header中對應Api.URL_KEY的value值
return request.header(Api.URL_KEY);
}
private <T> T checkNotNull(final T reference, final Object errorMessage) {
if (reference == null) {
throw new NullPointerException(String.valueOf(errorMessage));
}
return reference;
}
}
到此,就結束了。
項目地址:https://github.com/androidGL/RetrofitChangeUrlMVP