Uber Cross-Platform Mobile Architecture RIBs

Original author: Uber
  • Transfer
December 20, 2016, guys from Uber Engineering published an article about a new architecture (here is a translation of this article in Habré). I present to your attention the translation of the main part of the documentation.

What is RIBs architecture all about?


RIBs is a cross-platform architectural framework from Uber. It was designed for large mobile applications with a large number of nested states.

When developing this structure, Uber engineers adhered to the following principles:

  • Support for collaboration between people developing on different platforms: the vast majority of complex parts of Uber applications are similar on iOS and Android. RIBs provide common development patterns on Android and iOS. When using RIBs, engineers on both iOS and Android can share one co-developed architecture for their functions.
  • Minimizing global states and solutions: global state changes can lead to unpredictable behavior and can make it impossible to know what the changes in the code lead to. The RIBs-based architecture encourages encapsulated states in the deep hierarchy of well-isolated RIBs, thus avoiding problems with global states.
  • Testability and isolation: classes must be simple in order to write unit tests and also have a reason to be isolated (refer to SRP ). Individual RIB classes have different responsibilities (for example, routing, business logic, presentation logic, creating other RIB classes). In addition, the logic of the parent RIB is mainly separated from the logic of the child RIB. This makes it easy to test RIB classes and reduce dependency between system components.
  • Tools for productive development: borrowing nontrivial architectural patterns can lead to problems with the growth of the application, if there are no reliable tools to support the architecture. The RIBs architecture comes with IDE tools for code generation, static analysis, and runtime integration, which increases the productivity of developers in large and small teams.
  • The principle of open-closed: developers, if possible, should add new features without changing the existing code. When using RIBs, the implementation of this rule can be seen in a number of places. For example, you can attach or create a complex child RIB that requires dependencies on its parent RIB, with little or no change in the parent RIB.
  • Structuring around business logic: the structure of the business logic of the application should not strictly reflect the structure of the user interface. For example, to facilitate animation and view performance, the view hierarchy may be smaller than the RIB hierarchy. Or, a single RIB function can control the appearance of three views that are displayed in different places in the user interface.
  • Exact Contracts: Requirements must be advertised using contracts that are checked at compile time. A class should not compile if its own dependencies as well as guest dependencies are not satisfied. The RIBs architecture uses ReactiveX to represent guest dependencies, type-safe dependency injection ( DI ) systems to represent class dependencies, and many other DI capabilities to help create data invariants.

Constituent elements of RIBs


If you have previously worked with the VIPER architecture , then the classes that are part of the RIB will look familiar to you. RIBs usually consist of the following elements, each of which is implemented in its class:



Interactor


Interactor contains business logic. In this class, a subscription to Rx notifications occurs, decisions are made on state changes, data storage and attachment of child RIBs.

All operations performed in Interactor should be limited to its life cycle. Uber has created a toolkit to ensure that business logic is executed only with active interaction. This prevents Interactors from being deactivated, but Rx subscriptions still work and cause unwanted updates to the business logic or state of the user interface.

Router


Router tracks events from Interactor and converts these events into attaching and detaching child RIBs. Router exists for three simple reasons:

  • Router exists as a passive object, which simplifies testing complex Interactor logic without the need to create stubs for child Interactors or in any other way take care of their existence.
  • Routers create an additional level of abstraction between parent and child Interactors. This makes synchronous communication between Interactors a bit more complicated and encourages the use of Rx communication instead of direct communication between RIBs.
  • Routers contain simple and repetitive routing logic that would otherwise be implemented in Interactors. Transferring this template code to the Routers helps Interactors to be small and more focused on the core business logic of the RIB.

Builder


Builder is needed to create instances for all classes included in the RIB, as well as create instances of Builders for child RIBs.

Highlighting class creation logic in Builder adds support for stub creation in iOS and makes the rest of the RIB code insensitive to the details of the DI implementation. Builder is the only part of the RIB that needs to be aware of the DI system used in the project. By implementing another Builder, you can reuse the rest of the RIB code in a project using a different DI mechanism.

Presenter


Presenter is a stateless class that translates a business model into a presentation model and vice versa. It can be used to facilitate the testing of view model transformations. However, often this translation is so trivial that it does not justify the creation of a separate class Presenter. If Presenter is not done, the translation of the view models becomes the responsibility of the View (Controller) or Interactor.

View (Controller)


View creates and updates the user interface. It includes the creation and arrangement of interface components, the processing of user interaction, the filling of user interface components with data, and animation. View is designed to be as “stupid” (passive) as possible. They simply display information. In general, they do not contain any code for which unit tests should be written.

Component


Component is used to manage RIB dependencies. It helps Builder create instances of other classes that make up the RIB. The component provides access to the external dependencies needed to create the RIB, as well as its own dependencies created by the RIB itself, and control access to them from other RIBs. A parent RIB component is typically embedded in a child RIB Builder to give the child RIB access to the dependencies of the parent RIB.

Condition management


The state of the application is mainly controlled and represented by the RIBs that are currently connected to the RIB tree. For example, as a user navigates through different states in a simplified application for joint travel, the application appends and separates the following RIBs:



RIBs only make state decisions within their competence. For example, LoggedIn RIB only decides to transition between states such as Request and OnTrip. It does not make any decisions about how the system should behave when we are on the OnTrip screen.

Not all states can be saved by adding or deleting an RIB. For example, when user profile settings change, the RIB is not associated or disconnected. As a rule, we save this state inside streams of immutable models, which re-send values ​​when parts change. For example, a user name can be stored in a ProfileDataStream file that is within the scope of LoggedIn. Only network responses have write access to this stream. We pass an interface that provides read access to these threads down the DI column.

There is nothing in RIBs that is the ultimate truth for the state of RIB. This contrasts with the fact that more masterful frameworks, such as React, are already provided out of the box. In the context of each RIB, you can choose templates that facilitate a unidirectional data flow, or you can let the state of business logic and the state of presentation temporarily deviate from the norm to take advantage of effective animation frameworks for the platform.

Interaction between RIBs


When Interactor makes a decision related to business logic, it may need to inform the other RIB about events, such as the completion and sending of data. RIB framework does not include any single way to transfer data between RIBs. However, this method was created in order to facilitate some common patterns.

As a rule, if the link goes down to the child RIB, then we pass this information as events in the Rx stream. Or, the data can be included as a parameter in the build () method of the child RIB, in which case this parameter becomes an invariant for the lifetime of the child.



If the link goes up the RIB tree to the parent RIB Interactor, then this link is made through the listener interface, since the parent RIB may have a longer life cycle than the child RIB. A parent RIB, or some object on its DI graph, implements the listener interface and places it on its DI graph, so that its child RIBs can call it. Using this template to transfer data up instead of parent RIBs directly subscribing to their child RIBs Rx streams has several advantages. It prevents memory leaks, allows writing, testing and maintaining parent RIBs without knowing which child RIBs are attached to them, and also reduces the amount of fuss required to attach / detach a child RIB.



Rib toolkit


To ensure the smooth implementation of the RIB architecture in applications, Uber engineers have created a toolkit to simplify the use of RIB and the use of invariants created by implementing the RIB architecture. The source code of this toolkit was partially opened and is mentioned in the examples (see right part - comment. Per.).

The toolkit, which is currently open source, includes:


Tools where Uber plans to open source code in the future:

  • Static analyzer to prevent various memory leaks in the RIB
  • RIB integration with memory leak detection during program execution
  • (Android) annotation processors to simplify testing
  • (Android) Static analyzer RxJava, which provides RIBs with views that cannot be changed from the main stream (views)

PS


We in sports.ru really liked the approach of Uber engineers, because we have many times faced with all the architectural problems that the article described. Despite reasonableness, RIB has a number of drawbacks, for example, a rather high threshold for entering the architecture. We will examine in more detail the pros and cons of the architecture in the following articles; at least two of them are planned - for iOS and for Android. For those who want to dive into the RIB right now, there is a column on the wiki page that has lessons in English. From myself, I note that the architecture was clearly born in long technical discussions and gathered in itself the best practices of building architectures for mobile applications that are currently available. And finally, a bit of PR - we are in sports.ruwe also love technical discussions, often hold technical workshops for colleagues, regularly study new technologies and, in general, we have a cool atmosphere. So if you want to be part of our team - welcome !

Also popular now: