How we implemented the RIBs architecture. Report Yandex.Taxi

Hi, my name is Alexei Valyakin, I am writing applications for Android. A few months ago, I spoke at a meeting of the Yandex.Taxi team with mobile developers. My report was devoted to the transition to the architecture of RIBs in Taxis (RIB stands for the top three Router, Interactor, Builder). Here is the video, and under the cut - summary:





- It's time to jump a little on a train with a hype. This is a classic theme about architecture in Android.



Seriously, today I want to tell you about how and why we implemented this architecture, what difficulties we encountered and how it can help you.







When I joined the company, our team consisted of four people. Already at that moment we had a huge number of difficulties. The project was old, it started in 2012. There were collected quite a few technical problems, one of which is a wrongly built CI, great variability in approaches, tests that do not cover everything. And in general, there were a large number of difficulties and merge conflicts.



In two years, we have grown to 12 people, which means that we have increased the parallelization of the development of features. Therefore, there are even more merge conflicts, and with a lot of coherence of the code, you understand what this may lead to. At some point, we just started to sink, and somehow we had to figure it out. Part of these problems was solved by point refactoring, part by the library of components, which is worth talking about in a separate report.







What do all developers want? Beautiful architecture, development flexibility, ease of adding features, and, of course, reducing the complexity of merges - because basically they cause some bugs that can pop up at the release stage, when features in isolation are tested and work well. And when they got hold of and hit the release - hop, everything fell apart. This is an approximate picture that we wanted to come to.







How can you go to her? It is clear that there are a lot of options on how to do something well. I will talk about the main approaches and their disadvantages. Of course, there are other solutions.







Classic MVP. What challenges do we face in classic MVP? If we look at the example of our project, we get that there is MVP Activity, MVP Fragment, MVP View. And it turns out very great variability in what needs to be added. In some cases, you think you need to add a view and a fragment. Then it turns out that adding some small feature with which the manager comes to you is quite difficult, because it is generally located in a separate MVP Activity.



The second problem that MVP has is related to the fact that the router begs. You want to connect children flexibly and so that you have some kind of essence for this. Therefore, usually MVPs come to some kind of self-made router or something else. And the view driven approach is a pretty big minus. In very many MVP patterns, it is the presenter that is injected into the view, this already makes it less passive and violates clean architecture.







Viper is better. He has such an entity as a router, he is more abstracted, and yet he has a number of minuses. It still has view driven logic, it has the required presenter layer through which business logic passes, and this is not always true. The View layer is also required, you cannot get rid of it.



The main problem is that this architecture came to us from the iOS world, so it needs to be adapted in a certain way for Android. I saw that there are some adaptations, and some of them even nothing, but there are disadvantages.







It is clear that in the world of architecture there is no silver bullet, each architecture has its pros and cons. RIBs also have cons. In general, Uber introduced this architecture for the most part at the concept level. They have quite a few open classes, there are no complicated examples. There are some simple tutorials that you can go through. And when switching to any architecture, a large amount of refactoring follows, which you need to do, but not only RIBs have this minus.







What does RIBs architecture consist of? She uses Dagger components. Her main class, Builder, brings together this entire component, which consists of the following parts: Router, Interactor. Presenter (View) - a separate layer, sometimes it can be present, sometimes absent. At the same time, Presenter (View) can be merged into one class, or divided if you have a false presentation logic.



What else is cool here? Since Presenter (View) are optional, you add new screens in much the same way as new business features. Your structure is more clean and understandable. The child does not know anything about the parent, and the parent knows about the children. Let's see how this looks like an example of a simplified structure.







You always have some kind of root. This is the root RIB. It decides what to include in itself, depending on the state of your application: it is either an authorized or unauthorized state. Let's look at an example of our application. Maybe you are on the order or not on the order.



As an example, another cool RIB feature. You can create a RIB as a modal screen and then connect it in principle from any RIB. Since the RIB does not know anything about parents, any parent can provide the dependencies that are necessary for the child RIB.







The structure of modules may look something like this. At the moment, we were just thinking about breaking our application into modules. He was alone with us. In fact, everything is implemented quite classically. You have some kind of Common module, it can be divided into even smaller modules depending on what you need. You have some kind of core API, maybe network, databases, etc. And in our coordinate system, a specific RIB is a separate module, it includes all Common, etc., what it needs, including child RIBs.



If some things need to be combined between several RIBs, there are examples with Shared feature classes that stand out simply in separate modules.







What are the advantages of RIBs? Ease of testing, high code isolation, single activity approach, no pain with fragments (whoever works will understand), and uniformity. This is a cross-platform architecture, there is an approach for both iOS and Android. And if you have two teams, this is a big plus, because they will speak the same language.



This is an important point. Want a little life hack about implementing RIBs? Suppose you transfer dependencies to yourself, then you begin to add the extension functions of the heirs and understand that all of this is not enough for you, you need to adapt it for yourself. In the end, you just take and transfer them to your classes. And there is another way - when you immediately transfer them to your classes, without wasting time on the first option, and adapt it for yourself.



It's time to take a look at the code, at how it all looks.







They have a convenient plugin that allows you to generate the classes that are needed for RIB, without wasting time creating them. He creates four main classes - Builder, Interactor, Router and View, which I will talk about in more detail on the following slides. It also generates tests. Naturally, he will not write them for you, and you will have to write them yourself, but nevertheless, it is quite nice. Now we are thinking about creating a plugin that will simplify the creation of new modules with RIBs. This plugin would immediately connect all the necessary dependencies, and it would take less time to configure the module.







So, Builder is a classic glue code component, the main task of which is to assemble everything together, assemble a Dagger component and View. Usually View is going to just call the constructor, nothing complicated there. In some cases, it may be inflate.







The second part, which is in Builder, is about addictions, that is, about how the child gets any addictions from the outside.







It has a Parent Component interface that defines the dependencies that it needs. Thus, in the Builder of the child component, all the dependencies that it needs from above are provided.







Interactor is essentially the most important class that is business logic. Only injections are allowed into it. This is practically the most important thing that is being tested. It receives an event from the UI layer using Stream RX events. Presenter is an interface that defines the methods that my event provides.



What else is convenient for RIBs? By the fact that on the Interactor and Presenter layer you can organize the interaction that you like. It can be MVP, and MVVM, and MVVI. Here everyone is free to choose what he likes. A subscription to Presenter events might look something like this.







And here is how the processing of these events might look.







Router - a class that is responsible for connecting children. He has no business logic; he himself does not cause children to connect. This makes Interactor in such a concept. In fact, here I give a simplified example of how this happens. In fact, Builder simply calls the Build method, which collects the child RIB and connects the child directly using the attach child, as well as adding a view. Most often, this logic can be encapsulated in a separate transition, you can configure animations - it all depends on your needs.







View is as passive as possible in this architecture. She doesn’t inject anything into herself, she knows almost nothing. In the simplest cases, it can implement the Presenter interface if you have no difficulty presenting it. In more complex cases, this logic is divided into two classes. That is, you have a separate class Presenter, which just maps business data - for example, in the model view.



Here is an example of how Interactor receives UI events. Take a look at Rx stream.







You can’t just build a new architecture. When you do this, especially in a large project, certain difficulties begin. You need to understand that we have a huge project: about 20 Activity, if not more, and about 60 fragments. All this logic was very fragmented. It was necessary to somehow merge all this together.







First of all, you need to merge everything into a single navigation point, first make a god object - a certain Activity Router, where you will also manage the fragment stack, because you will have a lot of old code. Nobody will let you implement a new architecture all day and stop a business. In doing so, you will need to make friends with a stack of RIBs. RIBs, of course, also have a stack - it is accessible from under the hood. But what is important here? Quite a lot of code will have to be completed by ourselves. Uber does not support screen rotation, so it doesn’t so much about restoring state. Therefore, the first thing I had to do when I started to study this architecture was to add an heir over the router, which supports restoring the hierarchy of RIBs and the entire state of the application.



You will need to support Feature toggling. Not a single large project can do without it. Now one of our developers is developing a concept. If someone watched Mobius 2016, we talked about Plugin Factory on it, which allows you to dynamically connect and disconnect certain blocks of logic - not necessarily pieces with screens. It can act, for example, depending on the experiments that come from the server. Everything is done abstractedly and interaction is simplified.



RIBs workflow is also an interesting concept that you may need. This is when you have several RIBs that do not know anything about each other, are approximately at the same level, but at the same time you need to start the process with the data at the input, and at the output at the end you have to put everything together.



And, for example, modal screens. We have a super-custom design, so there are almost no classic dialogs left. Everything is self-written, we have to implement everything ourselves.



What can you get using RIBs? Isolation of the code, clear simple architecture, easy way to modularization, getting rid of fragments, Single activity approach and the convenience of parallel development of features.



References:

- github.com/uber/RIBs

- github.com/uber/RIBs/tree/master/android/tutorials

- habr.com/en/company/livetyping/blog/320452

- youtu.be/Q5cTT0M0YXg

- github.com/xzaleksey/Role-Playing-System-V2

- github.com/xzaleksey/DeezerSample



The last link leads to my pet project. Six months ago, I started developing on RIBs to try this architecture before introducing it to us. And there are more or less real cases that can help you. I experimented a lot, so there are controversial things. But in general, you can see how it works there. And maybe you will take something for yourself from there.



We also think about then allocating all this into a separate library, as we did in our time with the component library. There will be such Yandex-rib. But this is in the future. I told you everything, thanks.



All Articles