从0来搭建超灵活的MVP商用框架<一>-------从MVC至MVP的转变、通用网络切换框架
其实早就想记录关于MVC、MVP、MVVM架构的演变细节了,所以接下来借此机会准备对于这块的东东详细进行一个梳理,就自己实际工作而言还是习惯用MVC传统的代码习惯,毕境习惯了它N多年了有点改不过来,而对于MVP的代码风格在目前的项目上基本上已经越来越普及了,而往往项目中MVC和MVP风格都并存在的,所以习惯MVP风格的代码风格也是当务之急的,最终会打造目前公司所用的MVP的非常精简的企业级实现框架,好下面从点滴开始。
从MVC风格开始:
在正式学习MVP之前,还是对于人人皆知的MVC风格代码进行一个复习,这里以一个加载本地图片列表功能为例,之后会用MVP进行改造的,先看下效果:
由于比较简单,代码贴出来如下:
package com.android.mvparcstudy; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.widget.ListView; import com.android.mvparcstudy.adapter.MyAdapter; import com.android.mvparcstudy.bean.Shoe; import java.util.ArrayList; import java.util.List; public class MainActivity extends AppCompatActivity { private ListView listView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView = findViewById(R.id.listView); loadData(); } private void loadData() { List<Shoe> data = new ArrayList<>(); data.add(new Shoe(R.drawable.shop1, 1000, 10000)); data.add(new Shoe(R.drawable.shop2, 111, 90)); data.add(new Shoe(R.drawable.shop3, 2222, 800)); data.add(new Shoe(R.drawable.shop4, 333, 110)); data.add(new Shoe(R.drawable.shop5, 4444, 220)); data.add(new Shoe(R.drawable.shop6, 100, 330)); data.add(new Shoe(R.drawable.shop7, 20, 0)); data.add(new Shoe(R.drawable.shop8, 10000, 20)); data.add(new Shoe(R.drawable.shop9, 500, 120)); data.add(new Shoe(R.drawable.shop10, 30, 400)); listView.setAdapter(new MyAdapter(this, data)); } }
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:id="@+id/listView" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </LinearLayout>
package com.android.mvparcstudy.bean; /** * 实体 */ public class Shoe { public int icon; public int sales;//销量 public int inventory;//库存 public Shoe(int icon, int sales, int inventory) { this.icon = icon; this.sales = sales; this.inventory = inventory; } public Shoe() { } public int getIcon() { return icon; } public void setIcon(int icon) { this.icon = icon; } public int getSales() { return sales; } public void setSales(int sales) { this.sales = sales; } public int getInventory() { return inventory; } public void setInventory(int inventory) { this.inventory = inventory; } }
package com.android.mvparcstudy.adapter; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; import com.android.mvparcstudy.R; import com.android.mvparcstudy.bean.Shoe; import java.util.List; public class MyAdapter extends BaseAdapter { private LayoutInflater inflater; private List<Shoe> shoes; public MyAdapter(Context context, List<Shoe> girs) { inflater = LayoutInflater.from(context); this.shoes = girs; } @Override public int getCount() { return shoes.size(); } @Override public Object getItem(int position) { return shoes.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { View view = inflater.inflate(R.layout.item, null); Shoe g = shoes.get(position); ImageView iv_icon = (ImageView) view.findViewById(R.id.iv_icon); iv_icon.setImageResource(g.icon); TextView tv_like = (TextView) view.findViewById(R.id.tv_like); tv_like.setText("销量:" + g.sales); TextView tv_style = (TextView) view.findViewById(R.id.tv_style); tv_style.setText("库存:" + g.inventory); return view; } }
item.xml:
<?xml version="1.0" encoding="UTF-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/iv_icon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" /> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="8dip" android:layout_marginTop="20dip" android:layout_toRightOf="@id/iv_icon" android:orientation="vertical"> <TextView android:id="@+id/tv_style" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#666" android:textSize="18sp" /> <TextView android:id="@+id/tv_like" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dip" android:textColor="#666" android:textSize="15sp" /> </LinearLayout> </RelativeLayout>
另外图片都是从网上下载的,这里就不贴了,这种代码是我们最最常见的。
MVC与MVP的区别:
这里贴一张对比图,其实基本上都知道它们之间的区别:
简单来说对于MVP架构来说,就是把UI逻辑抽象成View接口,而把业务逻辑抽象成Presenter接口,Model还是原来的数据。大致流程会是这样:
将MVC改造成MVP:
好,接下来咱们将咱们的这个MVC的DEMO一点点转换成MVP, 先来建三个MVP的文件夹,好对代码进行归类:
定义V:
再来定义View,这里面只定义纯跟UI相关的接口:
定义M:
先来再来定义Model的接口,加载数据及数据的回调主要是靠它:
好,接下来则定义一个该Model的实现类:
package com.android.mvparcstudy.model;
import com.android.mvparcstudy.R;
import com.android.mvparcstudy.bean.Shoe;
import java.util.ArrayList;
import java.util.List;
public class MainModel implements IMainModel {
@Override
public void loadData(OnLoadListener onLoadListener) {
List<Shoe> data = new ArrayList<>();
data.add(new Shoe(R.drawable.shop1, 1000, 10000));
data.add(new Shoe(R.drawable.shop2, 111, 90));
data.add(new Shoe(R.drawable.shop3, 2222, 800));
data.add(new Shoe(R.drawable.shop4, 333, 110));
data.add(new Shoe(R.drawable.shop5, 4444, 220));
data.add(new Shoe(R.drawable.shop6, 100, 330));
data.add(new Shoe(R.drawable.shop7, 20, 0));
data.add(new Shoe(R.drawable.shop8, 10000, 20));
data.add(new Shoe(R.drawable.shop9, 500, 120));
data.add(new Shoe(R.drawable.shop10, 30, 400));
onLoadListener.onComplete(data);
}
}
定义P:
package com.android.mvparcstudy.presenter; import com.android.mvparcstudy.bean.Shoe; import com.android.mvparcstudy.model.IMainModel; import com.android.mvparcstudy.model.MainModel; import com.android.mvparcstudy.view.IMainView; import java.util.List; public class MainPresenter { //持有左边(VIEW) IMainView iMainView; //持有右边(MODEL) IMainModel iMainModel = new MainModel(); public MainPresenter(IMainView mainView) { this.iMainView = mainView; } //执行UI逻辑 public void fetch() { if (iMainModel != null && iMainView != null) { iMainModel.loadData(new IMainModel.OnLoadListener() { @Override public void onComplete(List<Shoe> shoes) { //再交给view iMainView.showListView(shoes); } }); } } }
应用于Activity上:
此时则在Activity将P进行初始化并处理回调,如下:
package com.android.mvparcstudy; import android.os.Bundle; import android.widget.ListView; import androidx.appcompat.app.AppCompatActivity; import com.android.mvparcstudy.adapter.MyAdapter; import com.android.mvparcstudy.bean.Shoe; import com.android.mvparcstudy.presenter.MainPresenter; import com.android.mvparcstudy.view.IMainView; import java.util.List; public class MainActivity extends AppCompatActivity implements IMainView { private ListView listView; private MainPresenter mainPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView = findViewById(R.id.listView); mainPresenter = new MainPresenter(this); loadData(); } private void loadData() { mainPresenter.fetch(); } @Override public void showListView(List<Shoe> shoes) { listView.setAdapter(new MyAdapter(this, shoes)); } }
MVP框架进一步优化:
P层对于V层的强引用消除:
对于上面MVP的使用中其实还是有一些待优化的地方,首先是P层强引用了V,如下:
这样会干扰V的回收动作,所以接下来用弱引用来解决这种强引用的关系:
这样的话就不存在内存泄漏的问题了。
加入绑定与解绑管理:
目前P层加入了虚引用了之后,其回收动作并不是特别及时,正常应该是在Activity进入时创建,而在Activity退出时则需要将P中的这个引用给去掉,这样性能也比较好,所以接下来加入绑定与解绑的处理来进一步改造P中的代码,如下:
所以此时的Activity中的调用变化为:
抽一个BasePresenter:
在上面一步我们对P中增加了绑定与解绑的操作,对于这个操作其实每个P层都需要,很显然需要将这个通用行为往上抽,而不是每个同样类似的代码都散步在各个具体的P层中,这也是平常写业务代码时好的一个习惯,于是乎抽一个Base就很有必要的:
package com.android.mvparcstudy.presenter; import com.android.mvparcstudy.view.IMainView; import java.lang.ref.WeakReference; public class BasePresenter<T extends IMainView> { //持有左边(VIEW) WeakReference<T> iMainView; public void attachView(T view) { this.iMainView = new WeakReference<>(view); } public void detachView() { if (iMainView != null) { iMainView.clear(); iMainView = null; } } }
嗯,但是呢,这个Base还是有点问题:
所以,对于这个UI接口还得抽一个Base出来:
目前通用的UI接口一般就是显示与隐藏对话框,这个根据实际具体的业务需要可以再次进行扩展。
抽取一个BaseActivity:
对于Activity中创建P层的代码其实也可以往上抽,这样更加的精简,如下:
package com.android.mvparcstudy.view; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.android.mvparcstudy.presenter.BasePresenter; public abstract class BaseActivity<T extends BasePresenter, V extends IBaseView> extends AppCompatActivity { //持有表示层 protected T presenter; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); //由activity选择一个表示层 presenter = createPresenter(); if(presenter == null) throw new NullPointerException("presenter is null。"); presenter.attachView((V) this); } @Override protected void onDestroy() { super.onDestroy(); presenter.detachView(); } protected abstract T createPresenter(); }
加入灵活网络切换框架:
背景:
上面MVP框架已经搭建好了,接下来这里加入一个跟MVP木有关系的框架,对于实际项目架构来说还是很重要的,下面先来描述一下要写这个框架的一个背景原因:
对于App来说,我们在商用时都会用各种成熟的三方框架,而且这些三方框架可能随着时代的变迁流行度可能也会变,咱们挼一下目前觉见的一些框架:
网络访问:retrofit、 okhttp、volley、xutils. . .
数据库:greendao、 room、 realm. . .
图片加载:imageloader、glide、picasso. . .
地图API:百度百图、腾讯地图、高德地图. . .
.....
这里就拿网络访问三方框架来说,目前基本上可能都是用retrofit+okhttp来对我们的网络访问进行封装,而假如哪一天这俩框架不行了需要将项目中所有的网络框架都改成volley,如果没有一个灵活切换的机制是不是得手动将所有的网络框架手动替换一下volley的实现?此时这种方式切换起来成本是很高的,所以在APP架构时如果能提前就将这种灵活度给考虑进去的话,那可以很从容的应对这种切换框架的需求的,所以接下来则打造一个满足这种切换背景的灵活框架,有了这个思路之后在实际工作中如果架构app时就可以借鉴一下。
思路:
这个实现思路就是在APP及框架使用中间加了一个能为我们动态切换的一个隔离层既可,用个示例图简单展示一下:
具体实现:
先来建立一个隔离的module层:
好先来定义好通用的网络接口,这里只定义一个就成了,重点是了解整个框架的构建过程,实际可以根据自己的业务去扩展就成了:
这里以返回JSON的格式为例,具体其它格式可以基于它进行扩展的,接下来则来实现一下这个回调,这里以Json转对像为例:
package com.android.isolation_processor.httpprocessor; import com.google.gson.Gson; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; /** * 回调接口的json版本的实现类 * 用于把网络返回的json字符串转让换成对象(Result就是用户接收数据的类型,不用T来表示泛型增加可读性) * //ResponceData就是Result */ public abstract class HttpCallback<Result> implements ICallback { @Override public void onSuccess(String result) {//result就是网络回来的数据 //result把转换成用户需要的对象 Gson gson = new Gson(); //需要得到用户输入的对象对应的字节码是什么样的 //得到用户接收数据的对象对应的class Class<?> clz = analysisClassInfo(this); Result objResult = (Result) gson.fromJson(result, clz); //把已经转好的对象,交给用户 onSuccess(objResult); } public abstract void onSuccess(Result result); private Class<?> analysisClassInfo(Object object) { //getGenericSuperclass可以得到包含原始类型,参数化类型,数组,类型变量,基本数据 Type genType = object.getClass().getGenericSuperclass(); //获取参数化类型 Type[] params = ((ParameterizedType) genType).getActualTypeArguments(); return (Class<?>) params[0]; } @Override public void onFailure(String e) { } }
上面的代码比较简单就不多解释了,也就是将JSON转换成我们要获取的那个对象,然后这里加几个依赖:
好,接下来则写一个工具类来使用一下这个网络框架,这里面都是用的抽象接口,以便在表示层可以进行具体网络框架的切换,代码也比较简单,直接贴出:
package com.android.isolation_processor.httpprocessor; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.Map; public class HttpHelper implements IHttpProcessor { private static HttpHelper instance; public static HttpHelper obtain() { synchronized (HttpHelper.class) { if (instance == null) { instance = new HttpHelper(); } } return instance; } private HttpHelper() { } private static IHttpProcessor mIHttpProcessor; //定义一个API,由应用层来决定用哪一个网络框架的实现 public static void init(IHttpProcessor iHttpProcessor) { mIHttpProcessor = iHttpProcessor; } @Override public void post(String url, Map<String, Object> params, ICallback callback) { //url:http://www.aaa.bbb/index //params:user=jett&pwd=123 //将其组拼到一块:http://www.aaa.bbb/index?&user=jett&pwd=123 String finalUrl = appendParams(url, params); mIHttpProcessor.post(finalUrl, params, callback); } public static String appendParams(String url, Map<String, Object> params) { if (params == null || params.isEmpty()) { return url; } StringBuilder urlBuilder = new StringBuilder(url); if (urlBuilder.indexOf("?") <= 0) { urlBuilder.append("?"); } else { if (!urlBuilder.toString().endsWith("?")) { urlBuilder.append("&"); } } for (Map.Entry<String, Object> entry : params.entrySet()) { urlBuilder.append("&" + entry.getKey()) .append("=") .append(encode(entry.getValue().toString())); } return urlBuilder.toString(); } private static String encode(String str) { try { return URLEncoder.encode(str, "utf-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); throw new RuntimeException(); }
}
}
好,接下来咱们来实现几个具体的网络请求,这里实现四个:Okhttp、Volley、XUtils,代码就不一一解释了,基本上都很熟了:
package com.android.isolation_processor.httpprocessor; import android.os.Handler; import java.io.IOException; import java.util.Map; import okhttp3.Call; import okhttp3.Callback; import okhttp3.FormBody; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; public class OkHttpProcessor implements IHttpProcessor { private OkHttpClient mOkHttpClient; private Handler myHandler; public OkHttpProcessor() { mOkHttpClient = new OkHttpClient(); myHandler = new Handler(); } private RequestBody appendBody(Map<String, Object> params) { FormBody.Builder body = new FormBody.Builder(); if (params == null || params.isEmpty()) { return body.build(); } for (Map.Entry<String, Object> entry : params.entrySet()) { body.add(entry.getKey(), entry.getValue().toString()); } return body.build(); } @Override public void post(String url, Map<String, Object> params, final ICallback callback) { RequestBody requestBody = appendBody(params); Request request = new Request.Builder() .url(url) .post(requestBody) .build(); mOkHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { final String result = response.body().string(); if (response.isSuccessful()) { myHandler.post(new Runnable() { @Override public void run() { callback.onSuccess(result); } }); } } }); } }
package com.android.isolation_processor.httpprocessor; import android.content.Context; import com.android.volley.Request; import com.android.volley.RequestQueue; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.StringRequest; import com.android.volley.toolbox.Volley; import java.util.Map; public class VolleyProcessor implements IHttpProcessor { private static RequestQueue mQueue = null; public VolleyProcessor(Context context) { mQueue = Volley.newRequestQueue(context); } @Override public void post(String url, Map<String, Object> params, final ICallback callback) { StringRequest stringRequest = new StringRequest( Request.Method.POST, url, new Response.Listener<String>() { @Override public void onResponse(String response) { callback.onSuccess(response); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { } } ); mQueue.add(stringRequest); } }
package com.android.isolation_processor.httpprocessor; import android.app.Application; import org.xutils.common.Callback; import org.xutils.http.RequestParams; import org.xutils.x; import java.util.Map; public class XUtilsProcessor implements IHttpProcessor { public XUtilsProcessor(Application app) { x.Ext.init(app); } @Override public void post(String url, Map<String, Object> params, final ICallback callback) { RequestParams requestParams = new RequestParams(url); x.http().post(requestParams, new Callback.CommonCallback<String>() { @Override public void onSuccess(String result) { callback.onSuccess(result); } @Override public void onError(Throwable ex, boolean isOnCallback) { } @Override public void onCancelled(CancelledException cex) { } @Override public void onFinished() { } }); } }
接下来咱们就可以来调用这个隔离层了:
然后咱们在Model层中来写一个测试代码验证一下:
其中得要JSON转换的一个对象:
package com.android.mvparcstudy.bean; public class ResponceData { /** * result : null * reason : 当前可请求的次数不足 * error_code : 10012 * resultcode : 112 */ private String result; private String reason; private int error_code; private String resultcode; public void setResult(String result) { this.result = result; } public void setReason(String reason) { this.reason = reason; } public void setError_code(int error_code) { this.error_code = error_code; } public void setResultcode(String resultcode) { this.resultcode = resultcode; } public String getResult() { return result; } public String getReason() { return reason; } public int getError_code() { return error_code; } public String getResultcode() { return resultcode; } }
添加一下网络权限:
好运行,看能不正常请求下来:
妥妥的, 此时咱们再来切换一下网络框架,超简单:
之后如果有类似要动态切的需求就可以用这套框架来搭建,还是挺灵活的。