Uber最近开源了他们的移动端框架,RIBs是一个跨平台框架,支持着很多Uber的移动应用。RIBs这个名字,取自Router、Interactor、Builder的缩写。
早在2016年,Uber就在一文中介绍了他们重构Uber app所采用的架构和技术,从源码我们能看出,RIBs就是VIPER模式的一个实现,并在VIPER的基础上做了不少改进。
阅读本文前需要了解VIPER模式,如之前不了解,可谷歌一下。
文章构成
文章将会分成三部分,第一部分介绍RIBs框架的基本组成。第二部分阐述框架需要解决的问题,以及RIBs怎么解决这些问题。第三部简述RIBs的特点。
1.RIBs的基本构成
2.主要问题的解决
- RIBs如何处理生命周期
- RIBs如何解决Android生命周期引起的RxJava内存泄漏
- 组件间如何通信
- 如何处理组件间的解耦
3.RIBs的特点
- Router树
- 单Activity应用
- 易于单元测试
RIBs的基本构成
RIBs的组件主要由Router、Interactor、Builder、Presenter、View组成,按Uber的设计,Presenter和View不是必须的,应对UI无关的业务场景。除了Builder,其它几个都是VIPER模式有的组件。
我们可以很容易地用他们提供的插件生成初始代码,下图是用IntellJ插件生成的模板代码示例。
Router
RIBs的路由,和别的VIPER设计相同的是,都用于页面的跳转。
不同的是: 1.RIBs的Router维护了一个子模块的Router列表,同时负责把子模块的View添加到视图树上。 2.Router不和Presenter通信,而是和Interactor通信,从上面的架构图能看出来。
Router类依赖Interactor,架构图里的Interactor会调用Router,来实现跳转。而Router也会调用Interactor,但场景不多,有以下两个:
1.handleBackPress,处理实体键的回退事件 2.向子模块传递savedInstanceState
Interactor
RIBs的交互器用于获取数据,从服务器或者从数据库中,和别的VIPER大同小异。它依赖Presenter和Router,从架构图中也能看出,Interactor会把数据Model传给Presenter,Presenter再跟View交互,显示到View上。而Presenter会处理View的点击调用,调用Interactor获取数据或处理逻辑。
Builder
RIBs的Builder是VIPER设计模式里没有的东西,用于初始化Interactor、Router等组件,并且定义依赖关系。
可以看出,Builder依赖View、Router,在build方法中创建Interactor。各组件如何组合起来,如何初始化一直是个问题,这部分代码写在Activity里明显会造成冗余。在View、Router、Interactor其中一个里负责创建也不符合它们的职责,用一个Builder类来负责创建符合逻辑。
View和Presenter
这两部分的设计也很有意思。一般在MVP里,我们会把Activity当做View,会有一个IView的接口,以及一个IPresenter的接口。如果按照面向接口的原则,VIPER框架可能有4个接口,如下图所示:
这同时也带来一个接口过多的问题,造成接口方法冗余,例如Interactor调用Presenter,Presenter接着调用View,这三个接口内会有三个表达含义相似的方法,如Interactor内requestLogin(),Presenter里updateLoginStatus(),View里会有一个showLoginSuccess()。尽管是不同职责,未免过于累赘。
RIBs的Router、Interactor、View都无需定义接口,直接继承基类。Presenter是唯一需要定义的接口,在Interactor内定义Presenter接口,View实现Presenter接口,然后通过Rxbinding绑定控件,Presenter单向调用View。
几个主要问题的解决
1.RIBs如何处理生命周期
如果采用MVP模式,我们需要在Presenter里有各种生命周期的方法,如果采用MVVM,我们需要在ViewModel里面处理生命周期。VIPER则需要在Interactor里处理生命周期。简单来说,就是把Activity或者Fragment的生命周期回调,映射到Interactor里的相关方法。
有很多方法能达到这个目的,最原始的一种,是在Activity里依赖Interactor,在每一个生命周期方法内,调用Interactor的相关方法。
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); interactor.onCreate(); } @Override protected void onResume() { super.onResume(); interactor.onResume(); } @Override protected void onDestroy() { super.onDestroy(); interactor.onDestroy(); }复制代码
另一种方法是使用Google提供的LifeCycle组件,在Interactor基类里注解方法,然后通过getLifecycle().addObserver(Interactor)添加监听。
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE) @CallSuper public void onCreate() { mCompositeDisposable = new CompositeDisposable(); } @OnLifecycleEvent(Lifecycle.Event.ON_START) @CallSuper public void onStart() { } @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @CallSuper public void onResume() { }复制代码
Uber采用的是第一种,在RibActivity基类里获取到router,在生命周期回调里dispatch到各个组件。
2.RIBs如何解决Android生命周期引起的RxJava内存泄漏
另一个跟生命周期息息相关的问题,就是如何解决RxJava可能会导致的内存泄漏问题。
一般我们会用这个库,RxLifecycle需要我们拿到RxActivity的引用,但在Interactor里引用Activity不是好的实践。没有Android的Context引用的话,我们可以把Interactor当做一个纯Java类进行单元测试,效率会比较高。另外RxLifecycle的作者也在一文中阐述了RxLifecycle存在的问题,并建议我们不要使用。
一个简洁清晰的处理是用CompositeDisposable把RxJava请求存起来,在Interactor生命周期结束时统一释放。
Uber的工程师可能觉得这么做不优雅,开发了一个来处理这个问题。
//AutoDispose库的使用myObservable .doStuff() .as(autoDisposable(this)) // 一行代码解决内存溢出问题 .subscribe(s -> ...);复制代码
AutoDispose库的原理和RxLifecycle大同小异,但在RxLifecycle的基础上做了改进,例如它不需要传递一个RxActivity上下文,取而代之的是一个LifecycleScopeProvider接口。下面是Interactor里的相关代码,这段逻辑其实就是AutoDispose库的使用,不多做解释了。
public abstract class Interactor implements LifecycleScopeProvider{ private static final Function LIFECYCLE_MAP_FUNCTION = new Function () { @Override public InteractorEvent apply(InteractorEvent interactorEvent) { switch (interactorEvent) { case ACTIVE: return INACTIVE; default: throw new LifecycleEndedException(); } } }; private final BehaviorRelay behaviorRelay = BehaviorRelay.create(); private final Relay lifecycleRelay = behaviorRelay.toSerialized(); /** @return an observable of this controller's lifecycle events. */ @Override public Observable lifecycle() { return lifecycleRelay.hide(); } @Override public Function correspondingEvents() { return LIFECYCLE_MAP_FUNCTION; } @Override public InteractorEvent peekLifecycle() { return behaviorRelay.getValue(); }复制代码
3.组件间如何通信
一般无论MVVM模式还是VIPER模式,我们都需要处理父组件与子组件的通信问题,子组件间的平行调用问题。
同样有很多种方法可以解决,RIBs的通信图示
我们着重看一下Interactor的调用,从图中看出,父子组件的通信是通过接口以及Observable streams的方式。/** * 在子组件定义接口 */ interface LoggedOutPresenter { Observable> playerNames(); }/** * 在父组件实现接口,并注入到子组件中供子组件调用 */class LoggedOutListener implements LoggedOutInteractor.Listener { @Override public void requestLogin(UserName playerOne, UserName playerTwo) { // Switch to logged in. Let’s just ignore userName for now. getRouter().detachLoggedOut(); getRouter().attachLoggedIn(playerOne, playerTwo); } }复制代码
对于父组件调用子组件,Uber更推荐Observable streams的方式,父组件将observable data stream暴露给子组件的Interactor,当数据变化时,子组件做出响应。
4.如何处理组件间的解耦
RIBs在Builder处理View、Router、Interactor的依赖问题。以下是教学代码的一个例子
@dagger.Module public abstract static class Module { //提供子组件跟父组件通信的接口实例 @RootScope @Provides static LoggedOutInteractor.Listener loggedOutListener(RootInteractor rootInteractor) { return rootInteractor.new LoggedOutListener(); } //提供Presenter实例 @RootScope @Binds abstract RootInteractor.RootPresenter presenter(RootView view); //提供Router实例 @RootScope @Provides static RootRouter router(Component component, RootView view, RootInteractor interactor) { return new RootRouter( view, interactor, component, new LoggedOutBuilder(component), new LoggedInBuilder(component)); } } @RootScope @dagger.Component(modules = Module.class, dependencies = ParentComponent.class) interface Component extends InteractorBaseComponent, LoggedOutBuilder.ParentComponent, LoggedInBuilder.ParentComponent, BuilderComponent { @dagger.Component.Builder interface Builder { @BindsInstance Builder interactor(RootInteractor interactor); @BindsInstance Builder view(RootView view); Builder parentComponent(ParentComponent component); Component build(); } } interface BuilderComponent { RootRouter rootRouter(); } @Scope @Retention(CLASS) @interface RootScope { }复制代码
Builder出了用于初始化各个组件外,还负责依赖注入,子Interactor的接口实例就是在Builder生成的。
3.RIBs的特点
- 业务逻辑驱动app,而不是View驱动
- 整个应用只有一个Activity
- 易于单元测试
RIBs的Router基类里维护了一个保存子Router的List,由于维护了Router树,在根Router里我们能一层层往下找到任何一个子组件的Router。也是因为有了Router树,单Activity成为可能。
private final Listchildren = new CopyOnWriteArrayList<>(); //dispatch 子组件 protected void dispatchDetach() { checkForMainThread(); getInteractor().dispatchDetach(); willDetach(); for (Router child : children) { detachChild(child); } }复制代码
RIBs文档中解释了单Activity的原因,多Acitivity会导致在全局中有更多的状态,代码会不稳健。具体的情景可能还得探讨,Android使用Activity作为页面确实会导致一些问题,Activity并不像Router树有着清晰的层次和逻辑结构。
It contains a single RootActivity and a RootRib. All future code will be written nested under RootRib. RIB apps should avoid containing more than one activity since using multiple activities forces more state to exist inside a global scope. This reduces your ability to depend on invariants and increases the chances you'll accidentally break other code when making changes.
至于单元测试,由于RIBs各组件的职责非常清晰,对Router和Interactor进行单元测试全覆盖是非常容易的事。
总结
RIBs框架的代码量非常小,类不多,是一个短小精悍的框架。作为VIPER模式的一个具体实现,从设计上能看出Uber的工程师深思熟虑,并且用符合逻辑的方式去解决了开发中遇到的问题。写一个VIPER框架并不难,用优美的方式去解决问题才是难点,Uber的工程师在这方面做得非常好。同时RIBs也提供了很多基础库以及插件供开发者提高效率,改天有时间再详细分析一下它提供的插件以及基础库。