搭建自己的 Android MVP 快速开发框架

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),分别取名BaseViewBaseModel

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 目录下新建两个类,分别取名ToastUtilReflectUtil

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 目录下新建两个抽象类BaseActivityBaseMvpActivity,前者继承 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

來源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 4
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
使用android studio 运行,下面是一个简单的文档,这个代码是一个demo 一、Activity的使用 1、SNActivity 框架最基本的activity,可调用$(SNManager)进行操作activity,具体用法请参考文档或代码 2、SNNavigationSlidingActivity 包含SNActivity的功能,继承于com.jeremyfeinstein.slidingmenu.lib.app.SlidingActivity 支持导航条和左滑视图的Activity 加载导航条: loadNavBar(int height,int background_color_id) loadNavBarResId(int height_id,int background_id) 加载左侧视图: /** * load left view * @param left_id left layout id * @param offset_value offset value * @param shadow_width_value shadow width value * @param shadow_drawable_id shadow drawable style * @param fade fade value */ loadLeft(int left_id, int offset_value, int shadow_width_value, int shadow_drawable_id, float fade) /** * load left view * @param left_id left layout id * @param offset_id offset id * @param shadow_width_id shadow width id * @param shadow_drawable_id shadow drawable id * @param fade fade value */ loadLeftResId(int left_id, int offset_id, int shadow_width_id, int shadow_drawable_id, float fade) 二、SNElement的使用 View的伪装对象,支持所有View的功能,详细功能可参考文档或代码 手动伪装:$.create $.id $.findView 注入伪装:$.setContent(view class or layout id,inject class); 获取原型:elem.toView(); 三、注入 1、视图注入 A、创建注入类,属性名称必须和layout中的id对应,如果不对应请加入标签@SNInjectView class DemoInject{ @SNInjectView(id=R.id.tvTest) public SNElement test; } B、实例化注入对象 DemoInject di=new DemoInject(); C、调用$.inject或者$.setContent注入 $.inject(di); D、注入成功后即可调用对象 String text=di.test.text(); 2、依赖注入 A、需要绑定注入对象,建议写到Application中的onCreate SNBindInjectManager.instance().bind(ITest.class, Test.class); B、与视图注入不同的是属性必须添加标签@SNIOC,注入的对象(Test)必须包含只有一个SNManager参数的构造函数,且必须实现注入者 public class Test implements ITest{ SNManager $; public Test(SNManager _$){ this.$=_$; }; } class DemoInject{ @SNIOC public ITest test; } C、调用$.inject或者$.setContent注入 同视图注入 D、注入成功后即可调用对象 di.test.xxx(); 四、fragment的使用 1、SNFragment 2、SNLazyFragment 五、控件的使用 1、SNFragmentScrollable 2、SNPercentLinearLayout、SNPercentRelativeLayout 3、SNScrollable 4、SNSlipNavigation 5、XList 6、slidingtab
Android有很多快速开发框架可供选择。其中两个常用的框架是AndroidFire和KJFrameForAndroidAndroidFire是一个新闻阅读App框架,基于Material Design、MVP、RxJava、Retrofit和Glide。它涵盖了当前Android开发最常用的主流框架,并且可以帮助开发快速开发一个App。 KJFrameForAndroid(也叫KJLibrary)是一个Android的ORM和IOC框架。它封装了Android中的Bitmap和Http操作,使其更加简单易用。KJFrameForAndroid的设计思想是通过封装Android原生SDK中复杂的操作,简化Android应用级开发,实现快速而又安全的App开发。它包含了五个主要模块:UILibrary、UtilsLibrary、HttpLibrary、BitmapLibrary和DBLibrary。 这些框架都提供了一些常用的功能和工具,可以帮助开发者高效地开发Android应用。开发者可以根据自己的需求和喜好选择适合自己的快速开发框架。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Android 常用开发框架](https://blog.csdn.net/nnmmbb/article/details/126161671)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [Android 快速开发框架:推荐10个框架](https://blog.csdn.net/u011394397/article/details/117841185)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值