写这篇博客目的是记录下自己写的网络请求框架,因为公司目前工作需要,需要一个可以动态变更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