Uber最近开源了他们的移动端框架RIBs,RIBs是一个跨平台框架,支持着很多Uber的移动应用。RIBs这个名字,取自Router、Interactor、Builder的缩写。
早在2016年,Uber就在Engineering the Architecture Behind Uber’s New Rider App一文中介绍了他们重构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这个库,RxLifecycle需要我们拿到RxActivity的引用,但在Interactor里引用Activity不是好的实践。没有Android的Context引用的话,我们可以把Interactor当做一个纯Java类进行单元测试,效率会比较高。另外RxLifecycle的作者也在Why Not RxLifecycle?一文中阐述了RxLifecycle存在的问题,并建议我们不要使用。
一个简洁清晰的处理是用CompositeDisposable把RxJava请求存起来,在Interactor生命周期结束时统一释放。
Uber的工程师可能觉得这么做不优雅,开发了一个AutoDispose来处理这个问题。
//AutoDispose库的使用
myObservable
.doStuff()
.as(autoDisposable(this)) // 一行代码解决内存溢出问题
.subscribe(s -> ...);
复制代码
AutoDispose库的原理和RxLifecycle大同小异,但在RxLifecycle的基础上做了改进,例如它不需要传递一个RxActivity上下文,取而代之的是一个LifecycleScopeProvider接口。下面是Interactor里的相关代码,这段逻辑其实就是AutoDispose库的使用,不多做解释了。
public abstract class Interactor<P, R extends Router>
implements LifecycleScopeProvider<InteractorEvent> {
private static final Function<InteractorEvent, InteractorEvent> LIFECYCLE_MAP_FUNCTION =
new Function<InteractorEvent, InteractorEvent>() {
@Override
public InteractorEvent apply(InteractorEvent interactorEvent) {
switch (interactorEvent) {
case ACTIVE:
return INACTIVE;
default:
throw new LifecycleEndedException();
}
}
};
private final BehaviorRelay<InteractorEvent> behaviorRelay = BehaviorRelay.create();
private final Relay<InteractorEvent> lifecycleRelay = behaviorRelay.toSerialized();
/** @return an observable of this controller's lifecycle events. */
@Override
public Observable<InteractorEvent> lifecycle() {
return lifecycleRelay.hide();
}
@Override
public Function<InteractorEvent, InteractorEvent> correspondingEvents() {
return LIFECYCLE_MAP_FUNCTION;
}
@Override
public InteractorEvent peekLifecycle() {
return behaviorRelay.getValue();
}
复制代码
3.组件间如何通信
一般无论MVVM模式还是VIPER模式,我们都需要处理父组件与子组件的通信问题,子组件间的平行调用问题。
同样有很多种方法可以解决,RIBs的通信图示
我们着重看一下Interactor的调用,从图中看出,父子组件的通信是通过接口以及Observable streams的方式。/**
* 在子组件定义接口
*/
interface LoggedOutPresenter {
Observable<Pair<String, String>> 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<RootInteractor>,
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 List<Router> children = 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也提供了很多基础库以及插件供开发者提高效率,改天有时间再详细分析一下它提供的插件以及基础库。