Uber recently open-source their mobile framework RIBs, which is a cross-platform framework that supports many of Uber’s mobile applications. The name RIBs comes from the abbreviation for Router, Interactor, and Builder.

As early as 2016, Uber introduced the Architecture and technology they used to reconstruct Uber App in Engineering the Architecture Behind Uber’s New Rider App. As can be seen from the source code, The RIBs is an implementation of the VIPER pattern and makes many improvements on it.

You should understand the VIPER model before reading this article. If not, Google it.

The article constitutes

This article will be divided into three parts. Part one introduces the basic components of the RIBs framework. Part 2 describes the problems that the framework needs to solve, and how to do this. Part 3 describes the characteristics of RIBs.

1. Basic composition of RIBs
2. Main problem solving
  • How does RIBs handle the life cycle
  • How does RIBs solve RxJava memory leaks caused by the Android life cycle
  • How do components communicate with each other
  • How do you handle decoupling between components
3. The characteristics of the RIBs
  • The Router tree
  • Single application Activity
  • Easy unit testing

The basic composition of RIBs

The components of RIBs consist of Router, Interactor, Builder, Presenter, and View. According to Uber’s design, Presenter and View are not required for UI-independent business scenarios. In addition to Builder, several other components are existing in VIPER mode.

We can easily generate initial code using the plug-in they provide, as shown below is an example of template code generated using the IntellJ plug-in.

Router

The routes for the RIBs, like the other VIPER designs, are used for page jumps.

The differences are as follows: 1. The Router for the RIBs maintains a list of routers for submodules and is responsible for adding submodule views to the View tree. A Router does not communicate with an Interactor, as can be seen from the diagram above.

The Router class relies on Interactor, which in the diagram calls the Router to jump to it. The Router also calls Interactor in the following two scenarios:

1. HandleBackPress, which handles the entity key rollback event 2. Pass savedInstanceState to the submodule

Interactor

The RIBs interactive is used to get data, either from a server or from a database, much like other ViPers. It relies on Presenter and Router. As you can see from the architecture diagram, Interactor passes the data Model to Presenter, who interacts with the View and displays it on the View. Presenter handles point-to-point calls to the View, calling Interactor to retrieve data or process logic.

Builder

The Builder for RIBs is something not found in The VIPER design pattern. It is used to initialize components such as interactors and routers, and to define dependencies.

As you can see, the Builder relies on the View and Router to create the Interactor in the build method. How the components are put together and initialized is always a problem, and writing this part of the code in the Activity is obviously redundant. It is not logical to use a Builder class to create a View, Router, or Interactor.

The View and Presenter

The design of these two parts is also interesting. In MVP, we think of an Activity as a View, and we have an IView interface, and an IPresenter interface. If you follow the interface oriented principle, the VIPER framework might have four interfaces, as shown below:

Interactor calls Presenter, which in turn calls View. There are three methods that express similar meanings, such as requestLogin() in Interactor. Presenter’s updateLoginStatus() and View’s showLoginSuccess(). Although it is a different duty, it is too cumbersome.

The Routers, interactors, and Views of RIBs do not need to define interfaces, but inherit directly from the base class. Presenter is the only interface that needs to be defined. In Interactor, Presenter is defined, and View implements Presenter. Then, Rxbinding is used to bind the control, and Presenter calls View unidirectionally.

Several major problems have been solved

1. How does RIBs handle the life cycle

In MVP mode, we need to have various lifecycle methods in Presenter, and in MVVM, we need to handle the lifecycle in ViewModel. VIPER needs to handle the life cycle in Interactor. Simply put, it maps the Activity or Fragment’s lifecycle callbacks to the related methods in the Interactor.

There are a number of ways to do this, the most primitive of which is to rely on Interactor in an Activity and call the Interactor related methods within each lifecycle method.

@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();
  }
Copy the code

An alternative approach is to use the Google-provided LifeCycle component, annotate the method in the Interactor base class and add a listener via 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() {}Copy the code

Uber uses the first approach, which fetches the Router in the RibActivity base class and dispatches it to each component in a lifecycle callback.

2. How does RIBs solve RxJava memory leaks caused by the Android life cycle

Another life-cycle issue is how to deal with memory leaks that RxJava might cause.

RxLifecycle requires that we get a reference to an RxActivity, but referencing an Activity in an Interactor is not a good practice. Without an Android Context reference, it is more efficient to unit test Interactor as if it were a pure Java class. Also the author of RxLifecycle is also at Why Not RxLifecycle? This article explains the problems with RxLifecycle and advises us not to use it.

A neat and clean process is to store RxJava requests with CompositeDisposable and release them uniformly at the end of the Interactor life cycle.

Uber’s engineers, perhaps feeling inelegant, developed an AutoDispose to deal with the problem.

// The AutoDispose library uses myObservable.dostuff ().as(autoDisposable(this)) // a line of code to solve the memory overflow problem. Subscribe (s ->...) ;Copy the code

The AutoDispose library works in much the same way as RxLifecycle but improves upon RxLifecycle, for example it does not need to pass an RxActivity context and instead has a LifecycleScopeProvider interface. The following is the relevant code in Interactor, this piece of logic is actually the use of AutoDispose library, there is no more explanation.

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
      
        lifecycle() { return lifecycleRelay.hide(); } @Override public Function
       
         correspondingEvents() { return LIFECYCLE_MAP_FUNCTION; } @Override public InteractorEvent peekLifecycle() { return behaviorRelay.getValue(); }
       ,>
      Copy the code

3. How do components communicate with each other

In general, whether in MVVM mode or VIPER mode, we need to deal with the communication between parent and child components, parallel calls between child components.

There are also many ways to do this, the communication diagram for RIBs

/ / interface LoggedOutPresenter {Observable<Pair<String, String>> playerNames(); } /** * implement the interface in the parent component, And for child components into sub-components call * / class LoggedOutListener implements LoggedOutInteractor. The Listener {@ Override public void RequestLogin (UserName playerOne, UserName playerTwo) {// Switch to logged in. Let's just ignore UserNamefornow. getRouter().detachLoggedOut(); getRouter().attachLoggedIn(playerOne, playerTwo); }}Copy the code

For the parent component to call the child component, Uber recommends Observable Streams. The parent component exposes the Observable data stream to the Interactor of the child component, and the child component responds when the data changes.

4. How to handle decoupling between components

RIBs handles dependencies on Views, routers, and interactors in Builder. Here is an example of teaching code

@dagger.Module public abstract Static class Module {// Provide static interface for child component to communicate with parent component LoggedOutInteractor.Listener loggedOutListener(RootInteractor rootInteractor) {returnrootInteractor.new LoggedOutListener(); } / / Presenter instance @ RootScope @ Binds the abstract RootInteractor. RootPresenter Presenter (RootView view); @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 {

  }
Copy the code

In addition to initializing components, The Builder is also responsible for dependency injection. Instances of sub-interactors are generated in the Builder.

3. The characteristics of the RIBs

  • Business logic drives app, not View driver
  • There is only one Activity in the entire application
  • Easy unit testing

The Router base class for RIBs maintains a List of subrouters. Because the Router tree is maintained, we can find the routers of any subcomponent layer by layer in the root Router. It is also because of the Router tree that single activities are possible.

private final List<Router> children = new CopyOnWriteArrayList<>(); //dispatch child protected voiddispatchDetach() {
    checkForMainThread();

    getInteractor().dispatchDetach();
    willDetach();

    for(Router child : children) { detachChild(child); }}Copy the code

The reasons for single activities are explained in the RIBs documentation, and more Acitivity leads to more state globally and less robust code. The specific scenario may need to be explored, but Android’s use of activities as pages does cause some problems. Activities do not have a clear hierarchy and logical structure like the Router tree.

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.

As for unit testing, because the responsibilities of the RIBs components are very clear, it is very easy to unit test for full coverage of both routers and interactors.

conclusion

The RIBs framework is a very short and lean framework with very little code and few classes. As a concrete realization of VIPER mode, it can be seen from the design that Uber engineers have been thoughtful and solved the problems encountered in development in a logical way. It’s not hard to write a VIPER framework, it’s hard to solve a problem in a beautiful way, and Uber’s engineers are very good at this. While RIBs also provides many basic libraries and plugins for developers to be more productive, there will be time to examine this in more detail later on.