Android 开发进入「死丢丢」的时代后,引用三方库在 Gradle 的支持下变得十分轻松。各种高手写的开源框架,极大程度降低了新手入行(坑)的门槛,「一周开发一款 App 并上线」也不再遥不可及。
关于快速开发,笔者本人的意见是不一定什么功能都自己写,但框架最好是自己搭。虽然网上有很多非常成熟好用的完整框架,但直接「拿来主义」的话可能有 2 点不妥之处——
框架提供的功能你未必都用得到。比如你只写一个纯阅读类型的应用(不带大数据收藏功能),那么你就用不到本地数据库,这样完整框架里有关数据库的内容,就给白白浪费了。
高手也有疏忽时,即便技术大牛,也不敢保证自己写的代码没有任何 bug,在任何使用场景都健壮坚挺。如果某天突然发现完整框架有什么 bug 或者局限,自己又没能力解决,到头来只能重构大块内容甚至整个项目,这代价就非常大了。
综上,笔者更倾向新手「站在巨人的肩膀上搭积木」,用高手写的不同功能库,自己动手搭属于自己的快速开发框架。而且在搭框架的过程中,你能不知不觉中学到很多进阶知识,对自己的成长也很有利。
限于水平和篇幅,笔者只用老牌轮子Volley做例子,搭一个仅涉及网络请求和图片加载的MVP框架。当下最流行的原生框架应属RxJava + Retrofit + OkHttp + Dagger,如果你想了解得更多,推荐下面几篇文章——
给 Android 开发者的 RxJava 详解
RxJava 与 Retrofit 结合的最佳实践
Dagger 2 从入门到放弃再到恍然大悟
MVP + Dagger2 + Retrofit 实现更清晰的架构
当然,这些库本质和 Volley 一样,都是去实现具体功能的轮子,而MVP 的架构是不变的,所以下文的内容对它们同样适用。
动手开始
打开 Android Studio,新建一个项目MvpFrameTest。再在项目根目录右键 new 一个 Module,选择第二项 Android Library ,取名MVP。
这时你会看到你的项目下面多了一个叫 mvp 的Module(和 app 一样是加粗显示的),不过角标是一个书架而非手机。这代表此模块是一个依赖库,而非独立运行的应用,我们今天主要的代码都写在它里面。
1.png
导入依赖
点开mvp 下面的 build.gradle文件(别错搞成 app 下面的了哦),在dependency节点下面导入我们要用的轮子——
compile'com.android.support:design:25.3.1'compile'com.android.volley:volley:1.0.0'compile'com.google.code.gson:gson:2.7'
这里我希望内容尽量简洁一点,因此只导入设计适配(包含RecyclerView以及各种 Material Design 控件)、Volley(包含网络请求与图片加载)和Gson(包含Json 解析)三个库。语句后面的版本号仅供参考,因为当你看到这篇文章时,建议使用的版本号可能又变了。
下面点开app 下面的 build.gradle文件,同样在dependency节点下面添加依赖——
compile project(path:':mvp')
点击提示行里面的Sync Now,一会儿任务完成,从此以后你在 mvp 里面依赖的库(包括自己写的各种类),app 就都可以用了。
建议把整体性的功能诸如网络请求、图片加载等写在 mvp 里,具体性的实现诸如 UI 配色、访问地址写在 app 里,这样你的框架使用起来才更加灵活。
如果你对 Gradle 还不了解,推荐一篇文——
给 Android 初学者的 Gradle 知识普及
MVP
下面开始写自己的东西了,由于我们想要的是 MVP 设计模式,首先应该完成通用的 M、V 和 P。对 MVP 不了解的推荐一篇文——
浅谈 MVP in Android
找到 mvp 下面的com.example.mvp包,如图所示
2.png
在里面新建两个接口(Interface),分别取名BaseView和BaseModel。
publicinterfaceBaseView{voidshowLoading();voidhideLoading();voidshowError();}
BaseView 里面我们定义了三个抽象方法,分别用于显示加载、隐藏加载和显示加载失败的内容。这些方法最终会交给你的视图(也就是 Activity 或者 Fragment)去实现。
publicinterfaceBaseModel{}
BaseModel 里面目前可以什么都不写。如果你参与一个团队开发,接口和数据有比较统一的格式,那可以在此做一些规范工作。
OK,M 和 V 都有了,再新建一个抽象类,取名BasePresenter。
publicabstractclassBasePresenter{protectedM mModel;protectedWeakReference mViewRef;protectedvoidonAttach(M model, V view){ mModel = model; mViewRef =newWeakReference<>(view); }protectedVgetView(){returnisViewAttached() ? mViewRef.get() :null; }protectedbooleanisViewAttached(){returnnull!= mViewRef &&null!= mViewRef.get(); }protectedvoidonDetach(){if(null!= mViewRef) { mViewRef.clear(); mViewRef =null; } }}
首先声明了两个泛型 M 和 V,M 对应要处理的 Model,V 则对应负责展示的View。由于 V 一般比较大,这里采用了弱引用的写法,避免内存泄漏。
isViewAttached()用于检测 V 是否已关联 P,为真则让getView()返回对应的 V,否则返回 null。另外两个方法负责 V 和 P 的关联与解关联,很简单。
等等,你这不都是具体方法么,为啥还要弄成抽象类?待会自见分晓。
应用入口
新建一个MyApp类,继承 Application,用于获取应用全局的上下文。
publicclassMyAppextendsApplication{privatestaticMyApp instance;publicstaticMyAppgetInstance(){returninstance; }@OverridepublicvoidonCreate(){super.onCreate(); instance =this; }}
这个类是你整个应用的入口,一些你希望在应用一跑起来就立即完成的工作(比如初始化一些三方库,包括 SDK),可以写入它的 onCreate() 方法。
切记不要用 instance = new MyApp() 一类的赋值去获取实例,这样你得到的只是一个普通的 Java 类,不会具备任何 Application 的功能!
完成以后别忘了去app 模块的 AndroidManifest.xml,在 Application 节点下添加一行——
android:name="com.example.mvp.MyApp"
网络请求
前面已经说过,网络请求这类整体功能的封装应写入框架,这样应用调用起来就很方便。这里用的请求库是Volley,不够了解的请看这篇文——
Android Volley 完全解析
这是一个系列文,共四篇,新手建议看完前三篇。
新建一个RequestManager类,用于管理网络请求。
publicclassRequestManager{privateRequestQueuequeue;privatestaticvolatileRequestManager instance;privateRequestManager(){queue= Volley.newRequestQueue(MyApp.getInstance()); }publicstaticRequestManagergetInstance(){if(instance == null) { synchronized (RequestManager.class) {if(instance == null) { instance =newRequestManager(); } } }returninstance; }publicRequestQueuegetRequestQueue(){returnqueue; }}
这里定义了一个请求队列的对象,在构造器里实例化,对象和构造器均设为私有,只暴露两个 get 方法。因为请求队列一个便够(多了很浪费资源哦),这里采用了双重校验锁单例模式的写法。不了解单例模式请看——
Android 设计模式之单例模式
下面定制我们的专属网络请求,网上大多数 API 返回数据都是 Json 对象,可以通过 Gson 很轻松的把它们转换成 Java 对象。新建一个MyRequest类,继承 Volley 里面的 Request 类。
publicclassMyRequestextendsRequest{privateGson mGSon;privateClass mClass;privateResponse.Listener mListener;publicMyRequest(String url, Class clazz,
Response.Listener listener, Response.ErrorListener errorListener){this(Request.Method.GET, url, clazz, listener, errorListener); }publicMyRequest(intmethod, String url, Class clazz, Response.Listener listener, Response.ErrorListener errorListener){super(method, url, errorListener); mGSon =newGson(); mClass = clazz; mListener = listener; }@OverrideprotectedResponseparseNetworkResponse(NetworkResponse response){try{ String json =newString(response.data, HttpHeaderParser.parseCharset(response.headers));returnResponse.success(mGSon.fromJson(json, mClass), HttpHeaderParser.parseCacheHeaders(response)); }catch(UnsupportedEncodingException e) {returnResponse.error(newParseError(e)); } }@OverrideprotectedvoiddeliverResponse(T response){ mListener.onResponse(response); }}
代码看着不少,其实很好理解。首先我们想要的 Java 对象不确定,所以用一个泛型 T 去描述,并指定为与 Request 类的泛型相同。
构造器是重写自父类,里面实例化了马上要讲到的 Gson,然后重载了一个不带请求类型的,此时默认请求类型为 GET。
接下来就是重写 Request 类的parseNetworkResponse()和 ** deliverResponse()** 方法,前者用于解析请求到的响应(也就是返回数据),后者用于将响应传递给回调接口mListener。解析时我们采用了Gson,它会强制我们处理UnsupportedEncodingException,最终返回的便是我们想要的 Java 对象。对 Gson 不了解请看——
你真的会用 Gson 吗?Gson 使用指南
这是一个系列文,共四篇,新手可以只看第一篇。
现在去处理响应,首先新建一个接口MyListener——
publicinterfaceMyListener{voidonSuccess(T result);voidonError(String errorMsg); }
这是一个回调,成功时携带泛型描述的 Java 对象,失败时则携带错误信息。
然后补充前面的 RequestManager,添加发送 GET 和 POST 请求的封装。
publicvoidsendGet(String url, Class clazz,finalMyListener listener){ MyRequest request =newMyRequest<>(url, clazz,newResponse.Listener() {@OverridepublicvoidonResponse(T response){ listener.onSuccess(response); } },newResponse.ErrorListener() {@OverridepublicvoidonErrorResponse(VolleyError error){ listener.onError(error.getMessage()); } }); addToRequestQueue(request); }publicvoidsendPost(String url, Class clazz,finalHashMap map,finalMyListener listener){ MyRequest request =newMyRequest(Request.Method.POST, url, clazz,newResponse.Listener() {@OverridepublicvoidonResponse(T response){ listener.onSuccess(response); } },newResponse.ErrorListener() {@OverridepublicvoidonErrorResponse(VolleyError error){ listener.onError(error.getMessage()); } }) {@OverrideprotectedMapgetParams()throwsAuthFailureError{returnmap; } }; addToRequestQueue(request); }publicvoidaddToRequestQueue(Request req){ getRequestQueue().add(req); }
网络请求搞定!这里很明显看出 Volley 的局限,就是不支持 POST 大数据,因此不适合上传文件(下载文件倒是可以通过 DownloadManager 实现)。如果你的项目有上传文件需求,应该转战 Retrofit 或 OkHttp。
图片加载
这里只用 Volley 自带的 ImageLoader 模块实现图片加载。该模块性能不错,但功能不如 Glide 一类的专业图片加载框架丰富,大家可根据需求自行选择合适的轮子。新手推荐看下面这篇文——
Android开源项目推荐之「图片加载到底哪家强」
新建一个ImageUtil类,用于封装图片加载。
publicclassImageUtil{publicstaticvoidloadImage(String url, ImageView iv,intplaceHolder,interrorHolder){ ImageLoader loader =newImageLoader( RequestManager.getInstance().getRequestQueue(),newBitmapCache());if(ivinstanceofNetworkImageView) { ((NetworkImageView) iv).setDefaultImageResId(placeHolder); ((NetworkImageView) iv).setErrorImageResId(errorHolder); ((NetworkImageView) iv).setImageUrl(url, loader); }else{ ImageLoader.ImageListener listener = ImageLoader.getImageListener(iv, placeHolder, errorHolder); loader.get(url, listener); } }privatestaticclassBitmapCacheimplementsImageLoader.ImageCache{privateLruCache cache;privatefinalintmaxSize =10*1024*1024;//缓存大小设为10MBitmapCache() { cache =newLruCache(maxSize) {@OverrideprotectedintsizeOf(String key, Bitmap value){returnvalue.getByteCount() /1024; } }; }@OverridepublicBitmapgetBitmap(String url){returncache.get(url); }@OverridepublicvoidputBitmap(String url, Bitmap bitmap){ cache.put(url, bitmap); } }}
首先写了一个内部类BitmapCache(因为工具类对外方法是静态的,所以它也应是静态),实现 Volley 的 ImageCache 接口并重写方法。这里采用了 LruCache 实现图片缓存,不了解请看这篇文——
Android高效加载大图、多图解决方案,有效避免程序OOM
然后暴露一个loadImage()方法给外部调用。Volley 带有一个 继承自 ImageView 的控件NetworkImageView,并有一套专属的加载流程,因此在 loadImage() 方法里,针对它和原生 ImageView 做了区分。
OK,图片加载也搞定了。回首一看我们已写了不少类和接口,整理一下吧,如下图示。这已经是一个还算像样的 MVP 快速开发框架了。
3.png
补充润色
继续添加轮子。我们都知道 MVP 的优点,但它也是有不少坑的——
类爆炸,这也是 MVP 最受诟病之处。严格的 MVP 写法下,每写 1 个页面(不算适配器和实体),要为之创建 8 个类。
P 应当具备和 V 相似的生命周期,但在众多 V 里一个个调用 onAttach() 和 onDetach() 一个个关联解关联,显然是重复劳动。
有些 V 的展现内容是共通的,比如进度条、空白页。
另外实际开发中我们还有一些需求,简单列举 2 个——
View 加载控件和数据的逻辑有时会很多,混杂一起阅读相当不方便。
应用要求单击返回键只弹出提示警告,双击才是回到桌面。
现在我们就来解决它们。
首先在 util 目录下新建两个类,分别取名ToastUtil和ReflectUtil。
publicclassToastUtil{privatestaticToast toast;publicstaticvoidshowToast(String text){if(toast ==null) { toast = Toast.makeText(MyApp.getInstance(), text, Toast.LENGTH_SHORT); }else{ toast.setText(text); } toast.show(); }}
该类用于显示一段土司(原生的接口有不妥之处,连续点击会连续土司)。
publicclassReflectUtil{publicstaticTgetT(Object o,inti){try{return((Class) ((ParameterizedType) (o.getClass().getGenericSuperclass())).getActualTypeArguments()[i]).newInstance(); }catch(Exception e) { e.printStackTrace(); }returnnull; }}
该类则用于反射获取指定泛型。
然后在 base 目录下新建两个抽象类BaseActivity和BaseMvpActivity,前者继承 AppCompatActivity,并实现我们写的 BaseView;后者继承前者。
publicabstractclassBaseActivityextendsAppCompatActivityimplementsBaseView{@OverrideprotectedvoidonCreate(@Nullable Bundle savedInstanceState){super.onCreate(savedInstanceState); setContentView(getLayoutId()); initView(); }protectedabstractintgetLayoutId();protectedabstractvoidinitView();@OverridepublicvoidshowLoading(){ }@OverridepublicvoidhideLoading(){ }@OverridepublicvoidshowError(){ }@OverridepublicbooleanonKeyDown(intkeyCode, KeyEvent event){returncheckBackAction() ||super.onKeyDown(keyCode, event); }//双击退出相关privatebooleanmFlag =false;privatelongmTimeout = -1;privatebooleancheckBackAction(){longtime =3000L;//判定时间设为3秒booleanflag = mFlag; mFlag =true;booleantimeout = (mTimeout == -1|| (System.currentTimeMillis() - mTimeout) > time);if(mFlag && (mFlag != flag || timeout)) { mTimeout = System.currentTimeMillis(); ToastUtil.showToast("再点击一次回到桌面");returntrue; }return!mFlag; }}
有时我们的活动只是一个静态的容器(比如欢迎页),这时其实是没必要使用 MVP 的。所以把包括 UI 的逻辑(双击退出)封装在此。BaseView 里面的方法也在此重写,简明起见,就不具体实现了。
另外为了提升可读性,BaseActivity 添加了两个抽象方法getLayoutId()和initView()。子类在重写时,将前者的返回值改为布局 ID,在后者中进行初始化(findViewById、setOnClickListener)即可。如果子类不在 onCreate() 方法里干其它事,重写 onCreate() 一步也可以省略。
皮埃斯:如果你用了 ButterKnife、Dagger 等依赖注入框架,初始化和解绑(去 onDestory() 方法)工作同样可以在这个 BaseActivity 里完成。
有意思的是如果你在子类里用了 Android Studio 一款关于 ButterKnife 的助手插件(人气很高的说),它依然会很「认真负责」的帮你重写 onCreate() 和 onDestory()…… 只有自己动手咔嚓掉了。
publicabstractclassBaseMvpActivityextendsBaseActivity{protectedT mPresenter;protectedM mModel;@OverrideprotectedvoidonCreate(@Nullable Bundle savedInstanceState){super.onCreate(savedInstanceState); mPresenter = ReflectUtil.getT(this,0); mModel = ReflectUtil.getT(this,1); mPresenter.onAttach(mModel,this); }@OverrideprotectedvoidonStart(){super.onStart(); loadData(); }protectedabstractvoidloadData();@OverrideprotectedvoidonDestroy(){super.onDestroy(); mPresenter.onDetach(); }}
遇到动态的,有数据请求和处理的页面,再让 MVP 出马。这个 BaseMvpActivity 继承了 BaseActivity,因此包含了里面全部功能,同时又添加了一个抽象方法loadData(),有关数据交互的方法写在里面即可。
举一反三,如果要让碎片也能选择性使用 MVP,你应该能写出对应的 BaseFragment 和 BaseMvpFragment 来了吧?
最后在 base 下创建接口MvpListener,用于数据从 M 到 V 的层间传递。
publicinterfaceMvpListener{voidonSuccess(T result);voidonError(String errorMsg);}
好了,属于你的简易 MVP 快速开发框架已经搭建完成,撒花庆祝一下吧。
4.png
开车上路
现在就在 app 模块中写个「知乎日报」测试测试,顺便也学习一下 MVP 杜绝类爆炸的使用姿势。简明起见,只用一个 RecyclerView 请求今天的内容(图片 + 标题),不再涉及详情。
首先创建知乎日报的实体类DailyBean。推荐用Postman做请求,然后用 Android Studio 的插件Gson Format自动生成。
publicclassDailyBean{privateString date;privateList stories;publicStringgetDate(){returndate; }publicvoidsetDate(String date){this.date = date; }publicListgetStories(){returnstories; }publicvoidsetStories(List stories){this.stories = stories; }publicstaticclassStoriesBean{privateinttype;privateintid;privateString ga_prefix;privateString title;privatebooleanmultipic;privateList images;publicintgetType(){returntype; }publicvoidsetType(inttype){this.type = type; }publicintgetId(){returnid; }publicvoidsetId(intid){this.id = id; }publicStringgetGa_prefix(){returnga_prefix; }publicvoidsetGa_prefix(String ga_prefix){this.ga_prefix = ga_prefix; }publicStringgetTitle(){returntitle; }publicvoidsetTitle(String title){this.title = title; }publicbooleanisMultipic(){returnmultipic; }publicvoidsetMultipic(booleanmultipic){this.multipic = multipic; }publicListgetImages(){returnimages; }publicvoidsetImages(List images){this.images = images; } }}
然后创建一个契约接口DailyContract,这是 Google 推荐的类爆炸解决方案(不过笔者此处并没严格按照官方要求去执行)——
publicinterfaceDailyContract{interfaceDailyModelextendsBaseModel{voidloadDaily(String url, MvpListener> listener); }interfaceDailyViewextendsBaseView{voidsetData(List beanList); }abstractclassDailyPresenterextendsBasePresenter{protectedabstractvoidloadData(String url); }}
接口里同时承载了 Daily 这个模块的 M,V 和 P(现在明白为何一开始要把 BasePresenter 弄成抽象类了吧),并且定义了方法规则。
下面开始具体实现这三层,首先是 P 层,创建一个DailyPresenterImpl类,让它继承契约里面的 DailyPresenter。
publicclassDailyPresenterImplextendsDailyContract.DailyPresenter{@OverridepublicvoidloadData(String url){finalDailyContract.DailyView mView = getView();if(mView ==null) {return; } mView.showLoading(); mModel.loadDaily(url,newMvpListener>() {@OverridepublicvoidonSuccess(List result){ mView.hideLoading(); mView.setData(result); }@OverridepublicvoidonError(String errorMsg){ mView.hideLoading(); mView.showError(); } }); }}
逻辑很简单,首先拿到契约里 DailyView 的实例 mView,做非空判断,然后调用 showLoading() 方法显示加载进度条。
此后调用 mModel(也就是契约里 DailyModel 的实例)的 loadDaily() 方法,出结果后告知 mView,首先关闭进度条。成功则执行 setData() 展示数据,失败则执行 showError() 展示错误信息。
创建DailyModelImpl类,继承契约里的 DailyModel。
publicclassDailyModelImplimplementsDailyContract.DailyModel{@OverridepublicvoidloadDaily(String url,finalMvpListener> listener){ RequestManager.getInstance().sendGet(url, DailyBean.class,newMyListener() {@OverridepublicvoidonSuccess(DailyBean result){ listener.onSuccess(result.getStories()); }@OverridepublicvoidonError(String errorMsg){ listener.onError(errorMsg); } }); }}
这里具体实现 loadDaily() 方法去请求数据,具体途径当然是之前我们封装的网络请求类。成功则执行 MvpListener 的成功回调,失败则执行失败回调。
创建我们用于展示的条目布局文件item_daily。
这里我没添加分割线,其实也不推荐直接在 item 里加分割线。
这里插播 2 个小知识——
在层级相同时,FrameLayout 的性能略高于 LinearLayout,LinearLayout 又略高于RelativeLayout。对应的百分比布局同理。
约束布局能保证布局层级始终为 1,如果你的 item 很复杂,有必要考虑一下它。如果你不习惯拖拖拽拽,可以先写 XML 再转换。
创建知乎日报的适配器DailyAdapter。这里我用了 RecyclerView,因为它的依赖已经包含在了 mvp 里,app 里就不用再重复声明了。
publicclassDailyAdapterextendsRecyclerView.Adapter{privateContext context;privateList beanList;publicDailyAdapter(Context context){this.context = context; beanList =newArrayList<>(); }publicvoidsetBeanList(List list){this.beanList.addAll(list); notifyDataSetChanged(); }@OverridepublicDailyHolderonCreateViewHolder(ViewGroup parent,intviewType){returnnewDailyHolder(LayoutInflater.from(context) .inflate(R.layout.item_daily, parent,false)); }@OverridepublicvoidonBindViewHolder(DailyHolder holder,intposition){ DailyBean.StoriesBean bean = beanList.get(position); holder.tv.setText(bean.getTitle()); ImageUtil.loadImage(bean.getImages().get(0), holder.iv, R.mipmap.ic_launcher_round, R.mipmap.ic_launcher_round); }@OverridepublicintgetItemCount(){returnbeanList.size(); }staticclassDailyHolderextendsRecyclerView.ViewHolder{ TextView tv; NetworkImageView iv; DailyHolder(View itemView) {super(itemView); tv = (TextView) itemView.findViewById(R.id.item_daily_tv); iv = (NetworkImageView) itemView.findViewById(R.id.item_daily_iv); } }}
简单起见我们只加载当天的全部内容。onBindViewHolder() 方法里面用到了之前封装的图片工具,占位图就简单用小机器人代替了。
创建主界面的布局文件activity_main。
创建一个日期工具类DateUtil,封装日期格式化流程。
聪明如你,应该知道这个类是放 app 更好,还是放 mvp 更好吧?
publicclassDateUtil{privatestaticfinalLocale LOCALE = Locale.CHINA;publicstaticStringformat(Date date, String s){returnnewSimpleDateFormat(s, LOCALE).format(date); }}
养成好习惯,创建一个类Api,统一管理访问接口。
publicclassApi{publicstaticfinalString DAILY_HISTORY ="http://news.at.zhihu.com/api/4/news/before/";}
最后写展示用的类MainActivity,也就是 MVP 的 V 层。继承 BaseMvpActivity 并实现契约里的 DailyView。
publicclassMainActivityextendsBaseMvpActivityimplementsDailyContract.DailyView{privateDailyAdapter adapter;@OverrideprotectedintgetLayoutId(){returnR.layout.activity_main; }@OverrideprotectedvoidinitView(){ adapter =newDailyAdapter(this); RecyclerView rcv = (RecyclerView) findViewById(R.id.ac_main_rcv); rcv.setLayoutManager(newLinearLayoutManager(this)); rcv.setHasFixedSize(true); rcv.setAdapter(adapter); }@OverrideprotectedvoidloadData(){ mPresenter.loadData(Api.DAILY_HISTORY + DateUtil.format(newDate(),"yyyyMMdd")); }@OverridepublicvoidsetData(List beanList){ adapter.setBeanList(beanList); }}
由于无须在活动创建时做其它事,onCreate() 方法可以不重写了。其它 4 个重写方法依次负责布局文件,初始化控件,请求和展示数据,一目了然。
最后别忘了在AndroidManifest里添加网络访问权限——
OK,可以跑应用了~~
1.gif
实际效果比 gif 更好,Volley 做纯阅读应用还是比较给力的。
再看一看我们搭好框架后真正写的代码(笔者做了归类整理)——
5.png
除去适配器和实体类,一个页面我们只写了 4 个类,有效解决了类爆炸;如果是类似欢迎页那样不涉及交互的,那直接继承 BaseActivity 即可,不再用 MVP 模式写了,这样一个页面只须写 1 个类。
本文结束,欢迎指教 and 拍砖~~
作者:彼岸sakura
链接:https://www.jianshu.com/p/965e67222454
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。