Android Architecture Component 和架构升级在铭师堂的实践

前言

升学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 中,咱们抽象出了一些基础类, 例如 BasePresenterActivityBaseActivity,这段代码多是这样的学习

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 架构组件。

Jetpack

Android Jetpack 是 Google 在今年的 IO 大会上,根据去年 IO 大会上发布的 Android Architecture Component 进一步发布的内容,针对咱们的问题,咱们关注的主要是架构组件。

Lifecycle

咱们使用了 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

咱们使用 ViewModel 来解决自建 MVP 架构中 presenter的生命周期问题。

这里的 ViewModel 和 MVVM 的 ViewModel 并非一回事,简单理解,其实 ViewModel 仍然是 Presenter。固然,是一个自动管理者生命周期的 PresenterViewModel 的官网简介就是

Manage UI-related data in a lifecycle-conscious way
复制代码

从文档里面咱们能够看到 ViewModel 的基本用法:

image

image

从官网的这张图咱们也能够看到,ViewModel 会随着 view 对象的 onDestory 执行 onCleared 方法销毁

image

咱们把数据的逻辑存储在 ViewModel 中,在 Activity 生命周期发生变化的时候,咱们能够从 ViewModel 中获取数据进行 UI 的恢复。 在 ViewMdoel 中,咱们也让它承担了一些单纯的逻辑操做的职责。

在文档中咱们看到的 ViewModel 初始化方式是

ViewModelProviders.of(this).get(ModelClass::class.java)
复制代码

在开发中, 咱们也常常须要把上个 Activity 传过来的数据传给 ViewModel , 这时候咱们能够利用 ViewModelProvider。Factory 进行初始化。

咱们在团队内的约定是,为了较复杂逻辑的抽象,咱们不限制 ActivityViewModel 的对应关系。一个 Activity 中能够持有多个 ViewModel 对象。可是,在不少逻辑不算很复杂的页面,可能仍然只是一个 Activity 须要一个ViewModel 就够了,因此咱们也写封装了一个对应的基础类。

image

其中:

  • arguments() 为咱们传给 ViewModel 的参数,放在 Bundle 对象里面。使用这个类的同窗只须要关心他传什么值,不须要关心 Factory 的使用方法

  • viewModelClass() 返回的是 ViewModel 的 Class 对象

ViewModel 的初始化以下图:

image

在利用 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 以后,咱们梳理了咱们的数据流向图

image

这里咱们能够看到,数据的传递方向看实际上是一个单向数据流。不会有数据从 UI 层到逻辑层互相扔来扔去的状况。即便代码多了,咱们也只须要关注单向的数据变化就能轻松了解到逻辑。代码也更加容易维护。

类比一下,咱们也能够发现,这个架构,和前端 React + ReduxFlux 架构也十分类似。

image

实际上,在 Jetpack 的源码中,咱们也能够看见相似 StoreDispatcher 的概念。虽然在业务代码的结构咱们仍然和 MVP 没有很大差别,可是从总体的角度看,咱们的架构更像是 Flux

这里,咱们就很方便的解决了自建 MVP 中,使人头疼的生命周期问题。也不须要担忧数据返回的时候 View 已经销毁了。由于这时候 LiveData 已经不会再执行 observer 的回调。

LiveData和数据相关的架构

Paging的使用

Jetpack 中,还要一个使人眼前一亮的组件就是 Paging。在最新迭代的图片选择组件中,咱们也使用了 Paging 做为列表分页加载的载体。

Paging 将相册选择的逻辑抽象成了几个部分:

数据
  • PagedList 一个继承了 AbstractList 的 List 子类, 包括了数据源获取的数据
  • DataSource 数据源的概念,分别提供了 PageKeyedDataSourceItemKeyedDataSourcePositionalDataSource, 在数据源中,咱们能够定义咱们本身的数据加载逻辑。
UI
  • UI 部分 paging 提供了一个新的 PagedListAdapter, 在实例化这个 Adapter 的时候,咱们须要提供一个本身实现的 DiffUtil.ItemCallback 或者 AsyncDifferConfig

在相册选择中,咱们每页读取必定量的图片,避免一次性加载全部本地图片可能出现的卡顿

image

配置相对应的配置

image

到这里咱们就实现了一个很优雅的列表分页加载,咱们能够画出 Paging 简单的架构图

image

在通常状况下,咱们最原始的方式,列表 UI 所在的部分,是须要知道数据的来源等逻辑部分。Paging实际是抽象了列表分页加载这个行为的 Presenter 层及其下游处理。这种模式,业务的编写者,能够把 UI 部分的代码模板化, 只须要关心业务逻辑,而且把业务逻辑中的数据获取写在 DataSource 中,使分页加载的操做解耦程度更高。

总结

经过实践,咱们总结了 Android Jetpack 组件的一些优势:

  • 官方出品,值得在第一时间使用,而且能够保证稳定性
  • 解决了自建 MVP 架构关于生命周期难以控制,接口复杂等致使的 部分代码很差维护的问题
  • 架构比较清晰,不会出现由于理解差别写出风格不一样的代码

同时咱们也有一些本身的思考,思考如何去把架构升级这件事作的更好:

  • 咱们须要整理出现有架构的不足,新的架构升级终究是为了解决痛点问题,不是单纯为了追求新技术而升级架构。
  • 架构升级的过程,应该尽可能减小对原有架构的侵入性,若是能实现无感知的替换则会更好,某些细节部分能够进行封装,让其余业务线的同窗只关注业务的处理过程。

以上咱们介绍了升学e网通客户端的架构升级,以及 Android Jetpack 在咱们团队内的实践。目前,文中介绍的部分都已经上线,部份内容已经通过了几个版本的迭代,没有出现明显的线上 crash

远景

在初步进行架构的升级以后,在客户端稳定性的前提下,咱们团队将会进一步尝试架构的升级。其中包括:

  • DI 的引入:架构在逐步的完善过程当中,会分出不少的代码层,例如 数据库、网络、复杂的逻辑处理层。这些对象目前在咱们的代码中都是单例类。单例同时也意味着生命周期很差管理,咱们须要一个依赖注入库帮助咱们管理对象。目前,咱们正准备针对kotlin 的 koin 进行尝试
  • 其余jetpack组件的尝试:例如 NavigationWorkManager
  • Paging 的进一步使用:Paging 在咱们的客户端目前没有大量使用,咱们在日后将会尝试和现有的三方 RecyclerView 组件结合,在网络请求的场景下使用它来作分页加载逻辑

做者

  • 烧麦, 铭师堂 Android 开发工程师

审稿

  • pighead, 铭师堂 Android 开发工程师
相关文章
相关标签/搜索