升学e网通是杭州铭师堂旗下的一款在线教育产品,集助学、助考、和升学为一体,是国内最领先的高中生综合指导系统,专为高中同窗打造的提供视频学习、助学备考、志愿填报、升学报考等服务的平台。在客户端的高速业务迭代下,咱们对Android客户端的架构进行了一次升级。咱们将用这篇文章将咱们最近几个月的技术工做进行分享。前端
早年,咱们采用了大多数客户端采用的 MVP 架构。可是随着业务代码的逐步增长,咱们遇到了下面几个头疼的问题。java
生命周期的不可控android
在咱们早期 MVP 的架构中,view 层就是 Actiivity、Fragment 等承载视图的部分,这部分通常都会有本身的生命周期,在 view 层对象中,会持有一个 Presenter 的对象实例。可是咱们没有办法保证 presenter 层对象的生命周期和 view 层保持一致。好比团队的同窗很早在 v 层的destroy中写了以下代码数据库
@Override
public void onDestroy() {
this = null;
}
复制代码
咱们这里暂时不讨论这个作法是否有必要或者是否正确,可是这里确实在 view 层对象置空后出现了 presenter 层对 view 层的调用,会发生不可预料的错误。 例如,咱们在 presenter 层加入了最经典的 Retrofit + Rxjava 的代码。当弱网状况下,网络请求没有返回,回退界面,若是当前的 Activity 对象被销毁,而 presenter 内的网络回调完成并调用了 view 层的方法刷新 UI,就会出现 crash(NullPointException)bash
因此咱们每次都须要在网络请求的时候对 Rxjava 的 Flowable
对象添加订阅,在 v 层对象的生命周期中调用取消订阅。网络
大体的代码以下:架构
addSubscribe(myApi.requestNetwork(requestModel)
.compose()
.subscribeWith(new MySubscriber<MyBean>() {
@Override
public void onFail(int errorCode, String msg) {
// todo something
}
@Override
public void onNext() {
// todo something
}
}));
复制代码
在团队人员增长的时候,若是在新同窗入职的时候不强调这个规则的时候,很容易就会出现线上的 NullPointException
异常ide
基础对象难以维护post
在 mvp 中,咱们抽象出了一些基础类, 例如 BasePresenterActivity
和 BaseActivity
,这段代码多是这样的学习
public abstract class BasePresenterActivity<T extends BasePresenter> extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
// todo something
}
this.setContentView(getLayout());
// todo something
}
}
复制代码
在 onCreate
中,咱们能够看到有很多代码逻辑,在将来的开发中,咱们可能须要其余的类似功能的 Activity, 或者在某些 Fragment 中,咱们须要相似的逻辑。可是,新上手的同窗可能只想关心我须要复制哪些 Activity 相关的逻辑,或者只想关心和生命周期相关的逻辑,这时候,Activity 和生命周期的逻辑就耦合在了一块儿,终究会变得难以维护。
MVP接口过多,影响可维护性
咱们使用 MVP 的初衷是为了代码分层解耦,利于阅读和维护,可是在代码量增长后却发现,view 层和 presenter 层经过接口来交互,致使接口中定义的方法愈来愈多,若是修改一个地方的逻辑,可能须要顺着好多个文件来找被影响的方法并修改。
整理一下 MVP 的数据流向,能够发现 MVP 实际上是双向的数据流。view 能够把数据传给 presenter, presenter 也能够把数据带给 view。逻辑复杂了以后及其不方便
团队同窗对MVP的理解不一致
MVP 虽然基本的原理很简单,只是 MVC 的一个改进和变种。可是网上其实也有不少的 MVP 写法。在团队内部,对因而否应该保证 presenter 层只拥有纯 Java/Kotlin 代码,而不出现 Android 的相关包,也有过各自的意见。
综合以上 MVP架构 遇到的问题,升级一套新的架构,让业务代码抽象程度更高,开发更简便,代码更利于维护,迫在眉睫。因而咱们开始关注 Google 官方出的 Jetpack 架构组件。
Android Jetpack
是 Google 在今年的 IO 大会上,根据去年 IO 大会上发布的 Android Architecture Component
进一步发布的内容,针对咱们的问题,咱们关注的主要是架构组件。
咱们使用了 Lifecycle
来重构咱们的基础 Activity
类,将 lifecycle 相关的内容和具体逻辑分类
abstract class BaseActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(bindLayout())
lifecycle.addObserver(BaseActivityLifecycle(this))
}
/** * Activity 的 Layout questionId */
abstract fun bindLayout(): Int
}
复制代码
BaseActivityLifecycle
的代码以下:
class BaseActivityLifecycle(val context: Context) : LifecycleObserver {
private val value:String? = null
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreate() {
// todo something
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onStart() {
// todo something
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResume()) {
// todo something
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
// todo something
}
}
复制代码
目前,Activity内部的 lifecycle 包含了 EventBus
和咱们本身的埋点库。咱们能够一目了然的看到咱们的基类 Activity 在每一个生命周期中有哪些三方库或者二方库须要初始化和销毁。若是某个同窗须要重构 BaseFragment 类,能够直接复用这个 lifecycle 的代码,也不用担忧本身写漏了什么 lifecycle 相关的初始化。
咱们使用 ViewModel
来解决自建 MVP 架构中 presenter的生命周期问题。
这里的 ViewModel
和 MVVM 的 ViewModel 并非一回事,简单理解,其实 ViewModel
仍然是 Presenter
。固然,是一个自动管理者生命周期的 Presenter
, ViewModel
的官网简介就是
Manage UI-related data in a lifecycle-conscious way
复制代码
从文档里面咱们能够看到 ViewModel
的基本用法:
从官网的这张图咱们也能够看到,ViewModel
会随着 view 对象的 onDestory
执行 onCleared
方法销毁
咱们把数据的逻辑存储在 ViewModel
中,在 Activity
生命周期发生变化的时候,咱们能够从 ViewModel
中获取数据进行 UI 的恢复。 在 ViewMdoel
中,咱们也让它承担了一些单纯的逻辑操做的职责。
在文档中咱们看到的 ViewModel
初始化方式是
ViewModelProviders.of(this).get(ModelClass::class.java)
复制代码
在开发中, 咱们也常常须要把上个 Activity 传过来的数据传给 ViewModel
, 这时候咱们能够利用 ViewModelProvider。Factory
进行初始化。
咱们在团队内的约定是,为了较复杂逻辑的抽象,咱们不限制 Activity
和 ViewModel
的对应关系。一个 Activity
中能够持有多个 ViewModel
对象。可是,在不少逻辑不算很复杂的页面,可能仍然只是一个 Activity
须要一个ViewModel
就够了,因此咱们也写封装了一个对应的基础类。
其中:
arguments() 为咱们传给 ViewModel
的参数,放在 Bundle
对象里面。使用这个类的同窗只须要关心他传什么值,不须要关心 Factory
的使用方法
viewModelClass() 返回的是 ViewModel
的 Class 对象
ViewModel
的初始化以下图:
在利用 Factory
初始化对象的时候,由于咱们使用了反射,因此在 proguard-rules.pro
中咱们要去掉相关类的混淆。
若是是你本身使用,须要添加
-keepclassmembers public class * extends android.arch.lifecycle.ViewModel {
public <init>(...);
}
复制代码
例如咱们上面封装的,则须要添加
-keepclassmembers public class * extends <your_package_name>.BaseViewModel {
public <init>(...);
}
复制代码
解决了生命周期的问题,那么咱们在 ViewModel
中获取了逻辑处理的结果,应该如何反馈给 UI 呢?咱们选择使用 LiveData
完成这些。
LiveData
是一个可观察数据的持有者,而且具备生命周期的感知。简单的 LiveData
用法以下:
在 ViewModel
中给 LiveData
赋值,
myLiveData?.post(value)
复制代码
在 view 中,对 LiveData
进行观察
mViewModel.myLiveData?.observer{v->
v?.let{
updateUI(it)
}
}
复制代码
关于 LiveData
更多的使用,咱们会在接下来的章节介绍
在拥有了 View
, ViewModel
, LiveData
以后,咱们梳理了咱们的数据流向图
这里咱们能够看到,数据的传递方向看实际上是一个单向数据流。不会有数据从 UI 层到逻辑层互相扔来扔去的状况。即便代码多了,咱们也只须要关注单向的数据变化就能轻松了解到逻辑。代码也更加容易维护。
类比一下,咱们也能够发现,这个架构,和前端 React
+ Redux
的 Flux
架构也十分类似。
实际上,在 Jetpack
的源码中,咱们也能够看见相似 Store
和 Dispatcher
的概念。虽然在业务代码的结构咱们仍然和 MVP 没有很大差别,可是从总体的角度看,咱们的架构更像是 Flux
这里,咱们就很方便的解决了自建 MVP 中,使人头疼的生命周期问题。也不须要担忧数据返回的时候 View
已经销毁了。由于这时候 LiveData
已经不会再执行 observer 的回调。
在 Jetpack
中,还要一个使人眼前一亮的组件就是 Paging
。在最新迭代的图片选择组件中,咱们也使用了 Paging
做为列表分页加载的载体。
Paging
将相册选择的逻辑抽象成了几个部分:
PagedList
一个继承了 AbstractList 的 List 子类, 包括了数据源获取的数据DataSource
数据源的概念,分别提供了 PageKeyedDataSource
、ItemKeyedDataSource
、PositionalDataSource
, 在数据源中,咱们能够定义咱们本身的数据加载逻辑。PagedListAdapter
, 在实例化这个 Adapter 的时候,咱们须要提供一个本身实现的 DiffUtil.ItemCallback
或者 AsyncDifferConfig
在相册选择中,咱们每页读取必定量的图片,避免一次性加载全部本地图片可能出现的卡顿
配置相对应的配置
到这里咱们就实现了一个很优雅的列表分页加载,咱们能够画出 Paging
简单的架构图
在通常状况下,咱们最原始的方式,列表 UI 所在的部分,是须要知道数据的来源等逻辑部分。Paging
实际是抽象了列表分页加载这个行为的 Presenter
层及其下游处理。这种模式,业务的编写者,能够把 UI 部分的代码模板化, 只须要关心业务逻辑,而且把业务逻辑中的数据获取写在 DataSource 中,使分页加载的操做解耦程度更高。
经过实践,咱们总结了 Android Jetpack
组件的一些优势:
同时咱们也有一些本身的思考,思考如何去把架构升级这件事作的更好:
以上咱们介绍了升学e网通客户端的架构升级,以及 Android Jetpack
在咱们团队内的实践。目前,文中介绍的部分都已经上线,部份内容已经通过了几个版本的迭代,没有出现明显的线上 crash
在初步进行架构的升级以后,在客户端稳定性的前提下,咱们团队将会进一步尝试架构的升级。其中包括:
koin
进行尝试jetpack
组件的尝试:例如 Navigation
和WorkManager