android 超簡單的MVP+Retrofit+RxAndroid+模擬接口響應信息+隨時切換BaseURL

寫這篇博客目的是記錄下自己寫的網絡請求框架,因爲公司目前工作需要,需要一個可以動態變更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

 

    
 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章