前言
写这个项目的初衷主要是为了熟悉 mvp 这种架构设计模式以及一些主流的第三方框架在项目中的使用方法。主要使用到的技术有:MVP + RxJava2 + Retrofit + ButterKnife + Glide + EventBus + Androidx + Room 等等。主要的功能我就不多介绍了,大家登录 WanAndroid 网站 或者下载本项目跑一下就可以看到了。我会尽量从零开始介绍这样一个项目是怎么一步一步搭建起来的,最好下载项目源码再结合本文的介绍能比较快速的理解,源码的 github 地址我贴在文章底部。好了,废话不多说,我们开始吧!
框架搭建以及依赖添加
WanAndroid 客户端使用 mvp 架构搭建,那么首先我们来看一下怎么搭建 mvp 的基础架构。
先创建一个空项目,名字就叫 WanAndroid, 然后创建一个 basemvp 的包,这个包里面的文件如图所示:
我来解释一下各个文件的作用吧!
BaseActivity : 这是所有 Activity 的父类,我们创建的 Activity 都需要继承自这个抽象父类。之所以抽象出一个公共的父类,也是为了减少我们编写一下重复的代码。来,看一下 BaseActivity 的 onCreate() 方法。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(getXMLId());
ButterKnife.bind(this);
if (useEventBus()) {
EventBus.getDefault().register(this);
}
mInjectPresenters = new ArrayList<>();
Field[] fields = this.getClass().getDeclaredFields(); // 解释1。
for (Field field : fields) {
//获取变量上面的注解类型
InjectPresenter injectPresenter = field.getAnnotation(InjectPresenter.class);
if (injectPresenter != null) {
try {
Class<? extends BasePresenter> type = (Class<? extends BasePresenter>) field.getType();
BasePresenter mInjectPresenter = type.newInstance();
mInjectPresenter.attach(this); // P 绑定 V 层。
field.setAccessible(true);
field.set(this, mInjectPresenter);
mInjectPresenters.add(mInjectPresenter);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}catch (ClassCastException e){
e.printStackTrace();
throw new RuntimeException("SubClass must extends Class:BasePresenter");
}
}
}
init(savedInstanceState);
}
这里先绑定 ButterKnife, 使用 ButterKnife 主要是减少我们编写很多的 findViewById() 等代码。然后注册 EventBus, 从解释1开始就比较关键了,先获取到实现类的所有成员变量,然后判断哪个成员变量被 InjectPresenter 注解修饰,之后利用反射实例化该成员变量,然后调用 mInjectPresenter.attach(this) 绑定 view。这里就将 View 层和对应的 Presenter 层绑定起来了。注意除了绑定,我们还需要在 onDestroy() 中进行解绑操作。
BaseFragment :这是所有 Fragment 的父类,作用和 BaseActivity 类似,BaseFragment 的 onCreateView() 方法和 BaseActivity 的 onCreate() 方法所做的操作时一样的,代码我就不贴了。
BaseModel : 这是所有 model 的父类。是一个空的抽象类。
BasePresenter : 这是所有 Presenter 的父类,它是一个泛型类,我们看一下它的 attach 方法吧。
@Override
public void attach(IBaseView view) {
mReferenceView = new SoftReference<>(view);
//使用动态代理做统一的逻辑判断 aop 思想 解释1.
mProxyView = (V) Proxy.newProxyInstance(view.getClass().getClassLoader(), view.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] objects) throws Throwable {
if (mReferenceView == null || mReferenceView.get() == null) {
return null;
}
return method.invoke(mReferenceView.get(), objects);
}
});
//通过获得泛型类的父类,拿到泛型的接口类实例,通过反射来实例化 model 解释2.
ParameterizedType type = (ParameterizedType) this.getClass().getGenericSuperclass();
if (type != null) {
Type[] types = type.getActualTypeArguments();
try {
mModel = (M) ((Class<?>) types[1]).newInstance();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
还记得我们在 onCreate() 方法中调了 mInjectPresenter.attach(this) 吗?这里传进来的就是具体的 view (就是我们的 Activity 或者 Fragment),然后看解释1,我们通过动态代理的方法做了统一的判空处理,这样就不需要在每一个具体使用的地方做判空处理了。
然后看解释2, 我们获取到具体实现类的父类,然后获取父类的第二个泛型的接口类实例,然后实例化该对象,也就是我们的 具体 model 的实现类了。
其他的几个类文件我就不解释了,看看代码就很好理解,可能我解释的不是很清楚,看源码应该能理解的更清楚一些吧!
接下来讲讲依赖的添加,以往我们都是在 module 中的 build.gradle 中添加依赖,但是这样不太好管理各种依赖的版本,于是我们可以创建一个单独文件来统一管理依赖和依赖的版本号。
首先在 根目录(也就是project层)创建一个 dependencies.gradle 文件,然后在 project 的 build.gradle 的最上面添加一行:
apply from: "dependencies.gradle"
之后我们就可以在 module 中的 build.gradle 中像这样添加依赖了。
implementation rootProject.androidSupportLibs
implementation rootProject.extraSupportLibs
implementation rootProject.ext.networkLibs
implementation rootProject.ext.rxJavaLibs
annotationProcessor rootProject.ext.annotationsProcessorLibs
详细的大家可以参考源码。
讲完了框架的搭建和添加依赖,接下来讲讲主体功能的实现。
主要的功能包括:引导页,首页,项目,体系,导航,登录,注册,收藏,黑夜模式(暂未实现),头像上传(暂未实现)。
那接下来就从功能角度讲讲具体实现以及用到的一些框架和 mvp 是怎么通过 Presenter 将 view 层和 model 层隔离开的。
引导页
首先 app 尽量就是一个引导页:LoadingActivity, 在这个 activity 里面我们需要去动态申请一些权限,如拍照,读写文件权限。在权限申请完之后我们 sleep 1500ms 再进入到主界面中,这样有过渡的效果,不至于一下子就跳到主界面。
首页
进入 MainActivity, 我们使用 BottomNavigationView 来实现底部导航栏的效果,再使用 DrawerLayout 来实现滑动菜单的效果,这些都比较简单,代码看源码就好,我都做了相应的注释,我们看看效果吧!
这里需要说明一下,BottomNavigationView 底部的四个导航栏分别对应的是四个 Fragment,默认显示的是首页的这个 Fragment, 也就是 HomeFragment。可以看到 HomeFragment 分为了两个部分,上面的轮播图和下面的列表,轮播图主要使用的是一个第三方框架:com.youth.banner:banner ,列表的实现使用的就是 RecyclerView 了。
我们都知道 RecyclerView 的 Adapter 写起来会有点麻烦,特别是要实现一些特殊效果,这里使用了一个特别好用的 Adapter 的第三方框架:BaseRecyclerViewAdapterHelper
另外,我们的数据都是通过网络获取的,那有可能出现数据获取失败,正在获取中等不同的状态,我们需要显示不同的 ui 界面,针对不同的状态显示不同的 ui, 这里也是使用了一个第三方的框架:MultipleStatusView:com.classic.common:multiple-status-view
如果代码有哪里看不到的可以先学习一下我上面说到的框架,另外源码中有必要的我都加了一些注释。
介绍完了 ui 层,下面我们讲讲数据是怎么获取并显示在界面上的。
在 HomeFragment 中定义了一个成员变量:mHomePresenter, 该成员变量使用了 @InjectPresenter 注解进行修饰,我们在上面讲了,使用这个注解修饰之后,在 BaseFragment 中就能够实例化该对象并将 mHomePresenter 和 HomeFragment 的对象绑定起来,那我们看看 HomePresenter 的代码:
public class HomePresenter extends BasePresenter<HomeContract.View, HomeModel> implements HomeContract.Presenter {
private static final String TAG = "HomePresenter";
@Override
public void initData(Context context) {
/***
* 初始化banner数据
*/
getModel().getBannerData(context).subscribe(listBaseBean ->
getView().showBanner(listBaseBean.getData()), throwable ->
AppLog.debug(TAG, "get banner data error: " + throwable.getMessage()));
getModel().getArticlesData(context, 0).subscribe(articleListBaseBean ->
getView().showArticleList(articleListBaseBean.getData().getDatas()), throwable ->
AppLog.debug(TAG, "get articles data error: " + throwable.getMessage()));
}
}
它继承自BasePresenter,第二个泛型传的是 HomeModel,我们不妨也一起看看 HomeModel 的代码,这样结合起来就能够很容易理解 RxJava2 和 Retrofit 是怎么结合使用的。HomeModel 的代码如下:
public class HomeModel extends BaseModel implements HomeContract.Model {
@Override
public Observable<BaseBean<List<BannerBean>>> getBannerData(Context context) {
return HttpHelper.getInstance(context)
.getRetrofitClient()
.builder(HomeApi.class)
.getHomeBannerData()
.subscribeOn(Schedulers.io()) // 操作在子线程中进行。
.observeOn(AndroidSchedulers.mainThread()); // 返回的是在主线程中。
}
@Override
public Observable<BaseBean<ArticleList>> getArticlesData(Context context, int pageNo) {
return HttpHelper
.getInstance(context)
.getRetrofitClient()
.builder(HomeApi.class)
.getArticlesData(pageNo)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
}
我们通过一个 HttpHelper 的工具类获取到 RetrofitClient 对象,然后通过建造者模式传入一个 HomeApi.class
接口类,然后调用该接口类的 getHomeBannerData() 方法获取到首页 Banner 的数据,返回的是一个 Observable 对象。接着我们在 HomePresenter 的 initData() 方法中调用 HomeModel 的 getBannerData() 方法,在调用 subscribe() 方法进行注册,这样请求完成之后就会回调到 subscribe() 方法的匿名内部类中,这里是使用了 lambda 表达式简化了匿名内部类的写法,之后调用 getView().showBanner() 就可以将数据返回到 HomeFragment 中了。
这样我们就完成了一个数据请求-回调的过程,在 HomeModel 中获取数据,通过 HomePresenter 进行中转,最终调到 HomeFragment 中,通过 Retrofit 和 RxJava2 的很好结合,我们的代码逻辑结构非常的清晰。
其他模块的数据请求-回调也是和这类似的,我就不重复解释其他模块的数据请过程了,这里如果有不了解 RxJava2 的使用的话,可以参考这个系列文章:
给初学者的RxJava2.0教程(一) : https://www.jianshu.com/p/464fa025229e
项目
讲完了首页之后,我们接着看项目这个模块的效果和实现,首先来看看效果:
我们可以通过左右滑动查看不同分类下面的详细文章,其实类似的效果我们在很多 app 中都可以看到,像微博,蜻蜓fm等。所以这是一个很经典的实现效果。那究竟是怎么实现的呢?
答案其实就是 TabLayout + ViewPager 的组合。关于这两者结合使用网上已经有很多很好的文章了,我就不再赘述了。也可以参考源码的实现,我都加了注释。
体系
先看看效果吧!
体系这个模块的实现其实比较简单,首先是一个 RecyclerView 显示各个知识体系的数据,然后 RecyclerView 子项的点击事件又是一个 TabLayout + ViewPager 的组合。
导航
我们再来看看导航页面的效果。
怎么样?效果还是挺不错的吧!其实使用到的是 google 爸爸提供的的一个流式布局容器:FlexboxLayout。
我们在 RecyclerView 的子项中添加一个 TextView 和 FlexboxLayout,然后在 Adapter 中获取到 FlexboxLayout,然后给该容器添加多个 TextView, 就可以实现这种效果了。可以看看 Adapter 的 convert() 代码:
@Override
protected void convert(@NonNull BaseViewHolder helper, NavigationBean item) {
helper.setText(R.id.item_navigation_title, item.getName());
FlexboxLayout flexboxLayout = helper.getView(R.id.item_navigation_fbl);
for (int i = 0; i < item.getArticles().size(); i++) {
final NavigationBean.NavigationItem navigationItem = item.getArticles().get(i);
TextView textView = createFlexItemTextView(flexboxLayout);
textView.setText(navigationItem.getTitle());
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mNavigationFragment != null) {
mNavigationFragment.showNavigationDetail(navigationItem);
}
}
});
flexboxLayout.addView(textView);
}
}
private TextView createFlexItemTextView(FlexboxLayout flexboxLayout) {
return (TextView) LayoutInflater.from(flexboxLayout.getContext()).inflate(R.layout.navigation_recyclerview_child_item, flexboxLayout, false);
}
哦,忘了说了,xml 的布局大部分使用的都是 ConstraintLayout 约束布局,这也是谷歌推荐的布局容器,减少多层布局的嵌套,提升响应性能。
好了,讲完了这几大模块之后,我们还有登录注册和收藏没讲。
登录和注册
这里我讲讲思路就好,首先做了一些简单的逻辑判断用于验证输入的账号和密码是否为空,两次输入的密码是否一致,然后获取到用户密码之后一路往下调,登录成功或者失败都会给我们一个回调反馈,我们再根据反馈做页面的处理就好。在登录成功之后,我们将用户的姓名保存在数据库中,这里我使用的是 Room,这也是 google 推出的一个 ORM(对象关系映射)框架,是 google 架构组件中的一员。这里多说一句,google 推出的架构组件真的非常好用,像 lifecycles, livedata, viewmodel 等,使用 mvvm + google 架构组件开发 app 也是很棒的一种体验。不妨尝试一下。
在本地保存了用户姓名之后,我们重复进入退出app,仍然能够保存当前已经登录的用户,在用户退出登录后清除 User 表的数据。
关于收藏我就不多说了,收藏功能需要在用户已经登录的前提下才能使用,但是这里有个bug,我已经登录成功之后,但是调收藏的接口还是返回给我没有登录,呃…还没解决这个问题。
结束
好了,这个项目主要是我拿来练手,学习一下一些框架的使用,它还是有很多很多不完善的地方。这里特别感谢一下鸿洋大神提供的 api,也感谢开源,感谢这么多好用的第三方框架和其他优秀作者开源的 WanAndroid 客户端源码。非常感谢。
源码 github 地址:https://github.com/AlongLing/WanAndroid