其实早就想记录关于MVC、MVP、MVVM架构的演变细节了,所以接下来借此机会准备对于这块的东东详细进行一个梳理,就自己实际工作而言还是习惯用MVC传统的代码习惯,毕境习惯了它N多年了有点改不过来,而对于MVP的代码风格在目前的项目上基本上已经越来越普及了,而往往项目中MVC和MVP风格都并存在的,所以习惯MVP风格的代码风格也是当务之急的,最终会打造目前公司所用的MVP的非常精简的企业级实现框架,好下面从点滴开始。
从MVC风格开始:在正式学习MVP之前,还是对于人人皆知的MVC风格代码进行一个复习,这里以一个加载本地图片列表功能为例,之后会用MVP进行改造的,先看下效果:
由于比较简单,代码贴出来如下:
packagecom.android.mvparcstudy; importandroidx.appcompat.app.AppCompatActivity; importandroid.os.Bundle; importandroid.widget.ListView; importcom.android.mvparcstudy.adapter.MyAdapter; importcom.android.mvparcstudy.bean.Shoe; importjava.util.ArrayList; importjava.util.List; public class MainActivity extendsAppCompatActivity { privateListView listView; @Override protected voidonCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView =findViewById(R.id.listView); loadData(); } private voidloadData() { 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>
packagecom.android.mvparcstudy.bean; /*** 实体 */ public classShoe { public inticon; public int sales;//销量 public int inventory;//库存 public Shoe(int icon, int sales, intinventory) { this.icon =icon; this.sales =sales; this.inventory =inventory; } publicShoe() { } public intgetIcon() { returnicon; } public void setIcon(inticon) { this.icon =icon; } public intgetSales() { returnsales; } public void setSales(intsales) { this.sales =sales; } public intgetInventory() { returninventory; } public void setInventory(intinventory) { this.inventory =inventory; } }
packagecom.android.mvparcstudy.adapter; importandroid.content.Context; importandroid.view.LayoutInflater; importandroid.view.View; importandroid.view.ViewGroup; importandroid.widget.BaseAdapter; importandroid.widget.ImageView; importandroid.widget.TextView; importcom.android.mvparcstudy.R; importcom.android.mvparcstudy.bean.Shoe; importjava.util.List; public class MyAdapter extendsBaseAdapter { privateLayoutInflater inflater; private List<Shoe>shoes; public MyAdapter(Context context, List<Shoe>girs) { inflater =LayoutInflater.from(context); this.shoes =girs; } @Override public intgetCount() { returnshoes.size(); } @Override public Object getItem(intposition) { returnshoes.get(position); } @Override public long getItemId(intposition) { returnposition; } @Override public View getView(intposition, 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); returnview; } }
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的实现类:
packagecom.android.mvparcstudy.model;
importcom.android.mvparcstudy.R;
importcom.android.mvparcstudy.bean.Shoe;
importjava.util.ArrayList;
importjava.util.List;
public class MainModel implementsIMainModel {
@Override
public voidloadData(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:
packagecom.android.mvparcstudy.presenter; importcom.android.mvparcstudy.bean.Shoe; importcom.android.mvparcstudy.model.IMainModel; importcom.android.mvparcstudy.model.MainModel; importcom.android.mvparcstudy.view.IMainView; importjava.util.List; public classMainPresenter { //持有左边(VIEW) IMainView iMainView; //持有右边(MODEL) IMainModel iMainModel = newMainModel(); publicMainPresenter(IMainView mainView) { this.iMainView =mainView; } //执行UI逻辑 public voidfetch() { if (iMainModel != null && iMainView != null) { iMainModel.loadData(newIMainModel.OnLoadListener() { @Override public void onComplete(List<Shoe>shoes) { //再交给view iMainView.showListView(shoes); } }); } } }
应用于Activity上:
此时则在Activity将P进行初始化并处理回调,如下:
packagecom.android.mvparcstudy; importandroid.os.Bundle; importandroid.widget.ListView; importandroidx.appcompat.app.AppCompatActivity; importcom.android.mvparcstudy.adapter.MyAdapter; importcom.android.mvparcstudy.bean.Shoe; importcom.android.mvparcstudy.presenter.MainPresenter; importcom.android.mvparcstudy.view.IMainView; importjava.util.List; public class MainActivity extends AppCompatActivity implementsIMainView { privateListView listView; privateMainPresenter mainPresenter; @Override protected voidonCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView =findViewById(R.id.listView); mainPresenter = new MainPresenter(this); loadData(); } private voidloadData() { 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就很有必要的:
packagecom.android.mvparcstudy.presenter; importcom.android.mvparcstudy.view.IMainView; importjava.lang.ref.WeakReference; public class BasePresenter<T extends IMainView>{ //持有左边(VIEW) WeakReference<T>iMainView; public voidattachView(T view) { this.iMainView = new WeakReference<>(view); } public voiddetachView() { if (iMainView != null) { iMainView.clear(); iMainView = null; } } }
嗯,但是呢,这个Base还是有点问题:
所以,对于这个UI接口还得抽一个Base出来:
目前通用的UI接口一般就是显示与隐藏对话框,这个根据实际具体的业务需要可以再次进行扩展。
抽取一个BaseActivity:
对于Activity中创建P层的代码其实也可以往上抽,这样更加的精简,如下:
packagecom.android.mvparcstudy.view; importandroid.os.Bundle; importandroidx.annotation.Nullable; importandroidx.appcompat.app.AppCompatActivity; importcom.android.mvparcstudy.presenter.BasePresenter; public abstract class BaseActivity<T extends BasePresenter, V extends IBaseView> extendsAppCompatActivity { //持有表示层 protectedT presenter; @Override protected voidonCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); //由activity选择一个表示层 presenter =createPresenter(); if(presenter == null) throw new NullPointerException("presenter is null。"); presenter.attachView((V) this); } @Override protected voidonDestroy() { super.onDestroy(); presenter.detachView(); } protected abstractT 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转对像为例:
packagecom.android.isolation_processor.httpprocessor; importcom.google.gson.Gson; importjava.lang.reflect.ParameterizedType; importjava.lang.reflect.Type; /*** 回调接口的json版本的实现类 * 用于把网络返回的json字符串转让换成对象(Result就是用户接收数据的类型,不用T来表示泛型增加可读性) * //ResponceData就是Result */ public abstract class HttpCallback<Result> implementsICallback { @Override public void onSuccess(String result) {//result就是网络回来的数据 //result把转换成用户需要的对象 Gson gson = newGson(); //需要得到用户输入的对象对应的字节码是什么样的 //得到用户接收数据的对象对应的class Class<?> clz = analysisClassInfo(this); Result objResult =(Result) gson.fromJson(result, clz); //把已经转好的对象,交给用户 onSuccess(objResult); } public abstract voidonSuccess(Result result); private Class<?>analysisClassInfo(Object object) { //getGenericSuperclass可以得到包含原始类型,参数化类型,数组,类型变量,基本数据 Type genType =object.getClass().getGenericSuperclass(); //获取参数化类型 Type[] params =((ParameterizedType) genType).getActualTypeArguments(); return (Class<?>) params[0]; } @Override public voidonFailure(String e) { } }
上面的代码比较简单就不多解释了,也就是将JSON转换成我们要获取的那个对象,然后这里加几个依赖:
好,接下来则写一个工具类来使用一下这个网络框架,这里面都是用的抽象接口,以便在表示层可以进行具体网络框架的切换,代码也比较简单,直接贴出:
packagecom.android.isolation_processor.httpprocessor; importjava.io.UnsupportedEncodingException; importjava.net.URLEncoder; importjava.util.Map; public class HttpHelper implementsIHttpProcessor { private staticHttpHelper instance; public staticHttpHelper obtain() { synchronized (HttpHelper.class) { if (instance == null) { instance = newHttpHelper(); } } returninstance; } privateHttpHelper() { } private staticIHttpProcessor mIHttpProcessor; //定义一个API,由应用层来决定用哪一个网络框架的实现 public static voidinit(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()) { returnurl; } StringBuilder urlBuilder = newStringBuilder(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())); } returnurlBuilder.toString(); } private staticString encode(String str) { try{ return URLEncoder.encode(str, "utf-8"); } catch(UnsupportedEncodingException e) { e.printStackTrace(); throw newRuntimeException(); }
}
}
好,接下来咱们来实现几个具体的网络请求,这里实现四个:Okhttp、Volley、XUtils,代码就不一一解释了,基本上都很熟了:
packagecom.android.isolation_processor.httpprocessor; importandroid.os.Handler; importjava.io.IOException; importjava.util.Map; importokhttp3.Call; importokhttp3.Callback; importokhttp3.FormBody; importokhttp3.OkHttpClient; importokhttp3.Request; importokhttp3.RequestBody; importokhttp3.Response; public class OkHttpProcessor implementsIHttpProcessor { privateOkHttpClient mOkHttpClient; privateHandler myHandler; publicOkHttpProcessor() { mOkHttpClient = newOkHttpClient(); myHandler = newHandler(); } private RequestBody appendBody(Map<String, Object>params) { FormBody.Builder body = newFormBody.Builder(); if (params == null ||params.isEmpty()) { returnbody.build(); } for (Map.Entry<String, Object>entry : params.entrySet()) { body.add(entry.getKey(), entry.getValue().toString()); } returnbody.build(); } @Override public void post(String url, Map<String, Object> params, finalICallback callback) { RequestBody requestBody =appendBody(params); Request request = newRequest.Builder() .url(url) .post(requestBody) .build(); mOkHttpClient.newCall(request).enqueue(newCallback() { @Override public voidonFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throwsIOException { final String result =response.body().string(); if(response.isSuccessful()) { myHandler.post(newRunnable() { @Override public voidrun() { callback.onSuccess(result); } }); } } }); } }
packagecom.android.isolation_processor.httpprocessor; importandroid.content.Context; importcom.android.volley.Request; importcom.android.volley.RequestQueue; importcom.android.volley.Response; importcom.android.volley.VolleyError; importcom.android.volley.toolbox.StringRequest; importcom.android.volley.toolbox.Volley; importjava.util.Map; public class VolleyProcessor implementsIHttpProcessor { private static RequestQueue mQueue = null; publicVolleyProcessor(Context context) { mQueue =Volley.newRequestQueue(context); } @Override public void post(String url, Map<String, Object> params, finalICallback callback) { StringRequest stringRequest = newStringRequest( Request.Method.POST, url, new Response.Listener<String>() { @Override public voidonResponse(String response) { callback.onSuccess(response); } }, newResponse.ErrorListener() { @Override public voidonErrorResponse(VolleyError error) { } } ); mQueue.add(stringRequest); } }
packagecom.android.isolation_processor.httpprocessor; importandroid.app.Application; importorg.xutils.common.Callback; importorg.xutils.http.RequestParams; importorg.xutils.x; importjava.util.Map; public class XUtilsProcessor implementsIHttpProcessor { publicXUtilsProcessor(Application app) { x.Ext.init(app); } @Override public void post(String url, Map<String, Object> params, finalICallback callback) { RequestParams requestParams = newRequestParams(url); x.http().post(requestParams, new Callback.CommonCallback<String>() { @Override public voidonSuccess(String result) { callback.onSuccess(result); } @Override public void onError(Throwable ex, booleanisOnCallback) { } @Override public voidonCancelled(CancelledException cex) { } @Override public voidonFinished() { } }); } }
接下来咱们就可以来调用这个隔离层了:
然后咱们在Model层中来写一个测试代码验证一下:
其中得要JSON转换的一个对象:
packagecom.android.mvparcstudy.bean; public classResponceData { /*** result : null * reason : 当前可请求的次数不足 * error_code : 10012 * resultcode : 112 */ privateString result; privateString reason; private interror_code; privateString resultcode; public voidsetResult(String result) { this.result =result; } public voidsetReason(String reason) { this.reason =reason; } public void setError_code(interror_code) { this.error_code =error_code; } public voidsetResultcode(String resultcode) { this.resultcode =resultcode; } publicString getResult() { returnresult; } publicString getReason() { returnreason; } public intgetError_code() { returnerror_code; } publicString getResultcode() { returnresultcode; } }
添加一下网络权限:
好运行,看能不正常请求下来:
妥妥的, 此时咱们再来切换一下网络框架,超简单:
之后如果有类似要动态切的需求就可以用这套框架来搭建,还是挺灵活的。