Multi-Modularity and Dagger 2. Yandex Lecture

    When your application is built on a multi-modular architecture, you have to devote a lot of time to ensure that all communications between modules are correctly written in the code. Half of this work can be entrusted to the Dagger 2 framework. The leader of the Yandex.Maps group for Android, Vladimir Tagakov Noxa spoke about the pros and cons of multi- modularity and convenient DI organization inside modules using Dagger 2.

    - My name is Vladimir, I am developing Yandex.Maps and today I will tell you about modularity and the second Dagger.

    I understood the longest part when I studied it myself, the fastest. The second part, over which I sat for several weeks, I will tell you very quickly and concisely.

    Why are we in Maps started a difficult process of dividing into modules? We just wanted to increase the speed of assembly, everyone knows about it.

    The second point of the goal is to reduce the code engagement. I hooked from Wikipedia. This means that we wanted to reduce interconnections between modules, so that the modules are separate and can be used outside the application. The initial formulation of the problem: other Yandex projects should be able to use part of the functionality of the Maps exactly as we have. And to develop this functionality, we were engaged in the development of the project.

    I want to throw burning sneakers towards [k] apt, which slows down the speed of assembly. I don't hate him much, but I love him a lot. He allows me to use Dagger.

    The main disadvantage of the separation process into modules is, paradoxically, slowing down the assembly speed. Especially at the very beginning, when you take out the first two modules, Common and some of your features, the overall build speed of the project drops, no matter how hard you try. At the end, when less and less code remains in your main module, the build speed will increase. Still, this does not mean that everything is very bad, there are ways to get around this and even have a profit from the first module.

    The second drawback is that it is difficult to divide the code into modules. Who tried, knows that you start to draw some kind of dependencies, some kind of classics, and everything ends with the fact that you copied all your main module into another module and started again. Therefore, you need to clearly understand the moment when you need to stop and break the connection using some kind of abstraction. The disadvantage is more abstractions. More abstractions - harder to design - more abstractions.

    It's hard to add new gradle modules. Why? For example, a developer comes, takes a new feature into development, immediately does well, makes a separate module. What is the problem? He should keep in mind all the available code that is in the main module in order to, if anything, reuse and render it in Common. Because the process of carrying out a module in Common is constant until your main App module turns into a thin layer.

    Modules, modules, modules ... Gradle modules, Dagger modules, interface modules - horror.

    The report will consist of three parts: small, large and complex. First, about the difference between Implementation and API in AGP. Android Gradle Plugin 3.0 appeared relatively recently. How was it before him?

    Here is a typical project of a healthy developer, consisting of three modules: the App module, which is the main one, is assembled and installed into the application, and two Feature modules.

    Immediately talk about the arrows. This is a big pain, everyone draws in the direction in which he is comfortable to draw. For me they mean that the arrow goes from Core to Feature. So, Feature knows about Core, can use classes from Core. As you can see, there are no arrows between Core and App, so the App doesn’t seem to use Core. Core is not a Common Module, it is, everything depends on it, it is separate, there is little code in it. While we will not consider it.

    Our core module has changed, we need to redo it somehow. We change the code in it. Yellow - change code.

    After rebuilding the project. It is clear that after changing a module, it will have to be recompiled, recompiled. Okay.

    After going to the Feature module, which depends on it. It’s also understandable, his addiction was rebuilt, and you need to update yourself. Who knows what has changed there.

    And here comes the most unpleasant. The App module is being assembled, although it is not clear why. I know for sure that I don’t use Core at all, and why the App is being rebuilt is unclear. And it is very big, because at the very beginning of the journey, and this is a very big pain.

    In addition, if several features depend on Core, many modules, then the whole world is reassembled, this is very long.

    Let's move on to the new version of AGP and replace, as the instruction says, all compile with the API, and not with Implementation, as you thought. Nothing changes. Identical schemes. What is the new way to specify dependencies Implementation? Imagine the same scheme using only this keyword, without an API? It will look like this.

    Here in the implementation it is clear that there is a connection between Core and App. Here we can clearly understand that we don’t need it, we want to get rid of it, so we just remove it. It becomes all kind of simpler.

    Now almost everything is good, even more than. If we change some API in Core, add a new class, a new public or package private method, Core and Feature will be rebuilt. If you change the implementation inside the method or add a private method, then theoretically the Reassembly Feature should not do anything at all, because nothing has changed.

    Let's go further. It so happened that many depend on our Core. Probably, Core is some kind of Network or user data processing. Because this is a Network, everything changes quite often, everything is rebuilt, and we got the same pain that we diligently ran away from.
    Let's look at two ways how to deal with it.

    We can take out from our Core module in a separate module only API interfaces, its API, which we use. And in a separate module we can take out the implementation of these interfaces.

    You can look at the connection on the screen. Core Impl will not be available for ficherov. That is, there will be no connection between the features and the Core implementation. And the module highlighted in yellow will only provide factories that will provide some unknown implementations of your interfaces.

    After this conversion, I want to note that the Core API, due to the fact that the API keyword is, will be available to all features transitively.

    After these transformations, we change something in the implementation that you do most often, and only the module with factories will be rebuilt, it is very light, small, you can not even consider how long it takes.

    Another option does not always work. For example, if this is some kind of Network, then I can hardly imagine how this can happen, but if this is some sort of user login screen, then it may well be.

    We can make Sample, the same full root module as App, and collect only one feature in it, it will be very fast, and it can be quickly and iteratively developed. At the end of the presentation, I will show how long a regular sample assembly and assembly takes.

    With the first part finished. What modules are there?

    Modules are of three types. Common, of course, should be as easy as possible, and it should not be some features, but only the functionality that is used by all. For us in our team this is especially important. If we provide our Feature modules to other applications, we will force them to drag Common anyway. If he is very fat, then no one will love us.

    If you have a smaller project, then with Common you can feel more free, you also do not have to be very zealous.

    The next type of module is Standalone. The most simple and intuitive module that contains a specific feature: a screen, a user script, and so on. It should be as independent as possible, and for it most often you can make a sample App and develop it in it. The Sample App is very important at the beginning of the split process, because everything is still building slowly and you want to get profit as soon as possible. At the end, when everything is broken into modules, you can reassemble everything, it will be fast. Because it will not reassemble once again.

    Celebrity modules. I myself came up with the word. The point is that he is very well known to everyone, and many depend on him. Same Network. I have already told you, if you often rebuild it, how can you avoid the fact that everything is being rebuilt. There is another way that can be applied to small projects, for which it is not worth the goal to give everything out as a separate dependency, a separate artifact.

    What does it look like? We repeat that from Celebrity you take out the API, take out its implementation, and now watch your hands, pay attention to the arrows from Feature to Celebrity. It happens. The API of your module got into Common, the implementation remained in it itself, and the factory that provides the implementation of this API appeared in your main module. If someone watched Mobius, then Denis Neklyudov talked about it. Very similar scheme.

    We use Dagger in the project, we like it, and we wanted to get as much benefit from this in the context of different modules.

    We wanted each module to have an independent dependency graph, to have a specific root component from which you can do anything, we wanted to have your own generated code for each Gradle module. We did not want the generated code to creep into the main one. We wanted as much compile-time validation as possible. We suffer from [k] apt, at least some profit should get from what gives Dagger. And with all this, we did not want to force anyone to use the Dagger. Neither the person who realizes the fresh feature module is separate, nor the one who then consumes it, our colleagues, who ask for some features for themselves.

    How to organize a separate dependency graph inside our feature module?

    You can try using Subcomponent, and it will even work. But this has quite a few flaws. You can see that in Subcomponent it is not clear exactly what dependencies it uses from the Component. To understand this, you will have to rebuild the project for a long time and painfully, look at what Dagger swears at and add it.
    In addition, the subcomponents are designed in such a way that they force others to use Dagger, and it will not be possible to start all this with your customers and yourself if you decide to opt out of some module.

    One of the most disgusting things is that when using Subcomponent, all dependencies are pulled into the main module. Dagger is designed so that the subcomponents are generated by the nested class of their framing parent components. Maybe someone looked at the gene code and its size, on their gene components? We have 20 thousand lines in it. Since the subcomponents are always nested classes for components, it turns out that the subcomponents of the subcomponents are also nested, and the entire gene code falls into the main module, this twenty thousand-fold file that needs to be compiled, and it needs to be refactored, the Studio begins to slow down - pain.

    But there is a solution. You can simply use Component.

    In Dagger, you can specify dependencies for a component. This is shown in the code, and shown in the picture. Dependencies, where you specify Provision methods, factory methods that show exactly what entities your component depends on. He wants to get them at the time of creation.
    Before, I always thought that only other components could be specified in these dependencies, and for some reason, the documentation says so.

    Now I understand what it means to use the component interface, but earlier I thought it was just a component. In fact, you need to use an interface that is composed according to the rules for creating an interface for a component. In short, just Provision-methods, when you just have getters for some dependencies. Also sample code is in the Dagger documentation.

    OtherComponent is also written there, and it is confusing, because in fact, you can not only push components there.

    How would we like to use this business in reality?

    In reality, there is a Feature-module, it has an API package, which is visible, is close to the root of all packages, and there it is indicated that there is an entry point - FeatureActivity. It is not necessary to use typealias, just to be clear. This may be a fragment, maybe a ViewController - it does not matter. And there are his dependencies, FeatureDeps, where it is indicated that he needs a context, some kind of Network-service, from Common some kind of thing that he wants to receive from the App, and any client is obliged to satisfy it. When he does, it will work.

    How do we use all this in the feature module? Here I use Activity, this is optional. We as usual create our own root Dagger component and use the magic method findComponentDependencies, it is very similar to Dagger for Android, but we cannot use it first of all because we don’t want to drag subcomponents. Otherwise, all the logic we can learn from them.

    At first I tried to tell how it works, but you can see it in the sample project on Friday. How do you need to use your library clients in your main module?

    First of all, it's just typealias. In fact, it has a different name, but for brevity so. MapOfDepth for the Dependency interface class gives you its implementation. In the App, we say that we are able to perform dependencies in the same way as in Dagger for Android, and it is very important that the component inherits this interface, automatically receives Provision-methods. Dagger from this moment begins to force us to provide this dependence. Until you provide it, it will not compile. This is the main convenience: you decide to make a feature, expand your component with this interface - everything, until you do everything else, it will not just compile, but will produce clear error messages. The module is simple, the point is that it binds your component to the interface implementation. About the same as in Dagger for Android.

    We turn to the results.

    I checked on our mainframe and on my local laptop, before turning off all that is possible. If we add a public method to Feature, then the build time is significantly different. Here I show the differences in the case where I am bilzhu sample-project. It is 16 seconds. Or when I collect all the cards - it means two minutes to sit and wait at every, even minimal change. Therefore, we develop many features and will develop them in sample projects. On the mainframer time is comparable.

    Another important result. Before the Feature module was selected, it looked like this: there were 28 seconds on the mainframe, now it's 49 seconds. We selected the first module, and already received a slowdown of the assembly almost twice.

    And one more option is a simple incremental build of our module, not features like the previous one. 28 seconds was before the module was selected. When they identified a code that does not need to be re-compiled each time, and [k] apt, which is not necessary to be carried out each time, they won three seconds. God knows that, but I hope that with each new module, time will only decrease.

    Here are useful links to articles: API against implementation , article with measurements of build time , sample module . The presentation will be available . Thank.

    Also popular now: