写一个 WanAndroid 客户端吧!

前言

写这个项目的初衷主要是为了熟悉 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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值