前言
前一段时间在微博上看到了一个面试题,要求一定时间内开发一个简易的 Gank.io 客户端,虽说笔者并无求职意向,但作为练手感觉也很不错,就尝试了一下。
GitHub Repo: unixzii / Android-Proficiency-Exercise
建议大家对照代码阅读本文!!
App 运行截图如下:
题目要求:
- 可以调用 API 获取数据
- 异步加载图片并缓存
- 下拉刷新,上拉加载更多
- 可以将数据缓存到数据库以供离线浏览
其实乍一看是一个非常简单的小项目,我在 5 个小时内快速写出了一个版本,并向原 Repo 发起了 PR,但是这一版的代码仅仅是完成功能,也就是实现题目要求的界面和功能,但是性能和代码并不是最优状态。今天又花了近 4 个小时进行了一次重构,目前来说是一个比较完美的状态了。
开发模式
笔者是 iOS 开发者,Android 仅仅是作为业余爱好来研究,但应用开发归根到底都是相通的,唯一不同的就是 API 及平台差异。
这个 app 我采用了很常见的 MVP 开发模式。将应用分为了三大模块,分别是:
- Main(包括 Toolbar、TabLayout、Fragment Container),这部分负责协调初始化子模块,并加载标签。
- Entity(Main 选中标签所对应的内容界面),这部分被 Main 所复用,用于加载展示数据。
- WebView,这部分我很简单地实现了一下,主要是用来展示文章的。
对于每一个模块,我都写了一个 Contract 契约类,它用来规定 Presenter 与 View 之间的交互,契约类中包括 Presenter
和 View
两个内部接口,这样就非常有利于模块的多人协作开发,各部分相互独立,各部分开发时不需了解其它部分的实现,只需调用接口中方法即可。
View & Presenter
我们来看 Entity 模块的 Contract 类:
public interface EntityContract {
interface View {
int STATE_LOADING_IDLE = 0;
int STATE_LOADING_REFRESHING = 1;
int STATE_LOADING_RESERVING = 2;
void setLoading(int state);
void addEntities(List<Entity> entities);
void clearEntities();
void showNetworkError();
void runOnUiThread(Runnable runnable);
}
abstract class Presenter extends BasePresenter<View> {
abstract void setCategory(String categoryName);
abstract String getCategory();
abstract void refresh();
abstract void reserve();
}
}
View 和 Presenter 所提供的功能一目了然,这里的 BasePresenter
是一个范型抽象类,其中实现了 attaching 和 detaching 时的相关处理,并提供了获取 View 的便利方法。
这里的 View 我才用 Fragment 来实现,列表选用了 RecyclerView
,事实上列表控件也属于一个小的 MVC 组件,Adapter 作为 Model,那么我们如何细化这部分的实现呢?整个 app 的 Model 层我写的比较简单,基本就是 Bean 类,完全可以直接交给 View 层作为一个 ViewModel,而 View 层的接口也十分简明,除了一些状态接口外,剩下的就是操作这些 ViewModel 的接口,我们可以向 View 中添加 ViewModels 也可以清空。对于已经添加进视图的 ViewModels,View 就负责展示这些数据即可,无需与 Presenter 进行再次交互。
那么 Presenter 的职责也很分明,就是负责加载数据,处理数据,做缓存处理等工作的。创建 Presenter 的时候我们无须操心 Context 的问题,我创建了一个 Application 单例,Presenter 没有要进行 UI 操作的必要,因此使用 Application Context 就可以满足数据库、磁盘、网络等操作。创建 Presenter 的时候我们可以配置好所需的 Retrofit、OkHttpClient、数据库助手类等对象,当然这里我们也可以使用单例,然后用 Dagger2 注入进去。
然后我们来看看缓存的处理:
在 Presenter 被 attach 到 View 上时,我们进行缓存的加载,因为这时 View 一定是空状态,所以我们执行的加载逻辑:
Presenter 缓存是一个存在于 Presenter 类内部的数组列表,是一级缓存,Fragment destroyView 后,Presenter 类并不会销毁,因此在 Fragment 再次 createView 的时候,我们可以直接拿出其中缓存的数据供 View 显示。
如果 Presenter 中没有缓存,则尝试从数据库读取缓存数据(作为二级缓存),如果数据库也不存在缓存数据才进行网络请求。
网络 & 缓存
网络请求方面采用了 Retrofit + RxJava + Gson,这方面的文章其实很多,本文不想多赘述。这里谈谈缓存,现在的缓存无非分为两大种:
- 数据库缓存
- Response 缓存
数据库缓存稍微麻烦一些,但是好处也是显而易见的,数据读写更加灵活,可控性更强;Response 缓存就是将服务器的响应(JSON、XML、BLOB、Protobuf 等)用 Hash 算法进行存储,下次请求相同的 API 时就能直接拿到,好处就是实现简单,加载逻辑不需要做区分处理,每次都按照网络请求来处理即可,但是缺点就是不够灵活,可扩展性差。
我采用了数据库存储,简单地封装了 Android 中原生的数据库操作类,然后写了一个 DAO 来隐藏 SQL 实现细节,因为有些干货数据中含有图片数组,因此一张表肯定是不够的,我们需要构建第二张表来保存图片 URL 数据。由于我们不会直接操作这些图片 URL 数据,因此这张图片表的操作可以与干货表一同被封装到一个 DAO 中,这样一来 Presenter 对数据库的操作也变得十分简单了。
最终整个 Gank.io App 的架构如下,非常简洁清晰:
总结
整个应用开发下来也遇到了很多小问题,虽然应用需求十分简单,但是能熟练地在有限时间内开发完这样一个小应用也并非一件十分简单的事,它要求我们站在一个更高地层面去设计整个应用的架构、业务逻辑等。在长时间与 API 和细节功能打交道的同时,偶尔也需要做一做这样的小软件,每次都会有不同的收获。