Multi-modality in Android in terms of architecture. From A to Z

    Hello!

    Not so long ago, we all realized that a mobile application is not just a thin client, but this is really a large number of very different logic that needs to be organized. That is why we imbued with the ideas of Clean architecture, felt what DI is, learned how to use Dagger 2, and now with closed eyes are able to break any feature into layers.

    But the world does not stand still, and with the solution of old problems new ones come. And the name of this new problem is monomodule. Usually you will learn about this problem when assembly time flies into space. This is exactly how many reports about the transition to multi-modulus ( one , two ) begin .
    But for some reason, all of this somehow forget that monomodularity beats strongly not only in terms of assembly time, but also in your architecture. Here is the answer to the questions. How big is your AppComponent? Do you occasionally encounter in the code that the feature A is for some reason tweaking the feature B's repository, although it doesn't seem to be like this, well, or should it be somehow more top-level? In general, features have some kind of contract? And how do you organize communication between features? Are there any rules?
    You feel that we have solved the problem with the layers, that is, vertically everything seems to be fine, but is something going horizontally wrong? And simply dividing into packages and control into reviews does not solve the problem.

    And a control question for the more experienced. When you moved to multi-modularity, didn't you have to shovel half of the application, always drag and drop code from one module to another, and live a decent amount of time with an uncollected project?

    In my article I want to tell you how I came to multi-modality precisely from an architectural point of view. What problems bothered me, and how I tried to solve them step by step. And at the end you will find an algorithm for switching from mono-modular to multi-modular without tears and pain.

    Answering the first question, how big is my AppComponent, I can admit - big, really big. And it constantly tormented me. How did that happen? First of all, it is because of such an organization DI. It is with DI that we begin.

    As I did DI before


    I think many people have formed in their heads something like this dependency scheme of components and corresponding scopes:


    What do we have here


    AppComponent , which absorbed absolutely all dependencies with the Singleton scoop . I think almost everybody has this component.

    FeatureComponents . Each feature was with its scoop and was a subcomponent of AppComponent or a major feature.
    Let's dwell on the features. First of all, what is a feature? I will try in my own words. Feature- it is a logically complete, maximally independent program module that solves a specific user problem, with clearly defined external dependencies, and which is relatively easy to use again in another program. Features can be big and small. Features may contain other features. And they can also use or launch other features through clearly marked external dependencies. If you take our application (Kaspersky Internet Security for Android), then features can be considered Anti-Virus, Anti-Theft, etc.

    ScreenComponents . A component for a specific screen, also with its own scopes and also a subcomponent of the corresponding feature component.

    Now the list of "why so"


    Why subcomponents?
    In component dependencies, I didn’t like first of all that a component could depend on several components at once, which, it seemed to me, could ultimately lead to a chaos of components and their dependencies. When you have a strict one-to-many relationship (component and its subcomponents), then it is safer and more obvious. In addition, by default, all dependencies of the parent are accessible to the subcomponent, which is also more convenient.

    Why for every feature your skoup?
    Because then I proceeded from the considerations that each feature is some kind of life-cycle of its own, which is not the same as the others, so it is logical to create your own scop. There is one more point for many meanings, which I will mention below.

    Since we are talking about Dagger 2 in the Clean section, I’ll also mention the moment the dependencies were delivered. Presenters, Interactors, Repositories and other dependency auxiliary classes were delivered through the constructor. In tests, we then substitute stubs or moks through the designer and calmly test our class.
    The closure of the dependency graph usually occurs in the activation, fragments, sometimes receivers and services, in general, in the root places from which the android can start something. The classic situation is when an activit is created for a feature, a feature component starts and lives in an activit, and there are three screens in the feature itself that are implemented in three fragments.

    So, everything seems logical. But as always, life makes its own adjustments.

    Life problems


    Example task


    Let's look at a simple example from our application. We have the Scanner feature and the Antitheft feature. In both features there is a cherished "Buy" button. Moreover, “Buy” is not just a request, but also a lot of different logic related to the purchase process. This is pure business logic with some dialogs for immediate purchase. That is, there is quite a separate feature - Purchase (Purchase). Thus, in two features we need to enable the third feature.
    From the point of view of ui and navigation, we have the following picture. The main screen starts up, on which there are two buttons:


    By clicking on these buttons we get to the feature of Scanner or Anti-Theft.
    Consider the feature of the Scanner:


    By clicking on “Start antivirus scanning”, some scanning work is done, by clicking on “Buy me” we just want to buy, that is, we pull the Shopping feature, well, and by “Help” we get on a simple screen with help.
    Antivirus feature looks almost the same.

    Potential solutions


    How do we implement this example in terms of DI? There are several options.

    First option


    The feature of the purchase to allocate an independent component , depending only on the AppComponent .


    But then we are faced with a problem: how to inject dependencies on two different graphs (components) into one class right away? Only through dirty crutches, which, of course, is to myself.

    Second option


    We select feature of purchase in the subcomponent depending on AppComponent. And to make the components of the Scanner and Anti-Virus subcomponents already from the Purchase component.


    But, as you understand, such situations can be quite a lot in applications. This means that the depth of dependencies of components can be truly enormous and complex. And such a graph will be more confusing than to make your application more slender and understandable.

    Third option


    We do not select the feature of the purchase in a separate component, but in a separate Dagger module . Further two ways are possible.

    The first way
    Let's set all dependencies to the features of Shopping Scoup Singleton and connect to the AppComponent .


    The option is popular, but it leads to bloating AppComponent . As a result, it expands in size, contains all the classes of the application, and the whole point of using Dagger is reduced only to more convenient delivery of dependencies to the classes - through the fields or the designer, and not through the singletons. In principle, this is DI, but we miss architectural moments, and it turns out that everyone knows about everyone.
    In general, at the beginning of the path, if you do not know where to include a class, to which feature, then it is easier to make it global. This is quite common when working with Legacy and trying to bring at least some kind of architecture there, plus you don’t know all the code well. And there really eyes run, and these actions are justified. The error is that when everything is more or less looming, no one wants to take on this AppComponent .

    The second way
    This is the reduction of all features to a single skoupu, for example PerFeature .


    Then we can connect the Dagger Shopping module to the necessary components easily and simply.
    It seems convenient. But architecturally it turns out not in isolation. The features of the Scanner and Anti-Vigor know absolutely everything about the Purchase feature, all its offal. By negligence, something may be involved. That is, Shopping features do not have a clear API, the border between features is blurry, and there is no clear contract. This is bad. Well, in multi-modular gredlovuyu will be hard then.

    Architectural pain


    Frankly, for a long time I used the third option. The first way . This was a necessary measure when we began to gradually transfer our legacy to normal rails. But, as I mentioned, with this approach, your features begin to mix up a bit. Everyone can know about everyone, about the implementation details and this is all. And the swelling of AppComponent clearly indicated that something needs to be done.
    By the way, with the unloading, it is AppComponent that the third option would help well . The second way . But here knowledge about implementations and mixing of features will not disappear anywhere. Well and clear business, reuse of features between applications would be rather uneasy business.

    Intermediate conclusions


    So, what do we want in the end? What problems do we want to solve? Let's go straight through the points, starting from the DI and moving on to the architecture:

    • Convenient DI mechanism, which allows using features within other features (in our example, we want to use the Purchases feature as part of Scanner and Anti-Theft), without co-costing and pain.
    • The finest AppComponent.
    • Features do not need to know about the implementation of other features.
    • Fichi should not be available by default to anyone, I want to have some kind of strict control mechanism.
    • It is possible to give a feature to another application with a minimum number of gestures.
    • Logical transition to multi-modularity and best practices in this transition.

    I specifically said about multi-modality only at the very end. We will reach it, we will not get ahead.

    "Life in a new way"


    Now we will try to gradually implement the wishes mentioned above.
    Go!

    DI Improvements


    Let's start with the same DI.

    Rejection of a large number of scopes


    As I wrote above, before my approach was this: for every feature, your scop. In fact, there are no special profits from this. Just get a large number of scopes and a certain amount of headache.
    Such a chain is quite enough: Singleton - PerFeature - PerScreen .

    Waiver of Subcomponents in favor of Component dependencies


    Already more interesting moment. With Subcomponents, you seem to have a more strict hierarchy, but at the same time, your hands are completely tied up and there is no possibility to at least somehow maneuver. In addition, AppComponent knows about all the features, and you also get a huge generated class DaggerAppComponent .
    With Component dependencies you get one super cool advantage. In component dependencies, you can specify not pure components, but pure interfaces (thanks to Denis and Volodya). Because of this, you can substitute any implementation of the interface, Dagger will eat everything. Even if this implementation is a component with the same script:
    @Component(
        dependencies = FeatureDependencies.class,
        modules = FeatureModule.class
    )
    @PerFeaturepublicabstractclassFeatureComponent{
        // ...
    }
    publicinterfaceFeatureDependencies{
        SomeDependency someDependency();
    }
    @Component(
        modules = AnotherFeatureModule.class
    )
    @PerFeaturepublicabstractclassAnotherFeatureComponentimplementsFeatureDependencies{
        // ...
    }
    


    From DI improvements to better architecture


    Let's repeat the definition of features. A feature is a logically complete, maximally independent program module that solves a specific user problem, with clearly defined external dependencies, and which is relatively easy to reuse in another program. One of the key expressions in the definition of a feature is “with clearly defined external dependencies”. Therefore, let's all that we want from the outside world for the features will be described in a special interface.
    Here, let’s say, the interface of external dependencies of the Purchase feature:
    publicinterfacePurchaseFeatureDependencies{
        HttpClientApi httpClient();
    }
    

    Or the external dependency interface features of the Scanner:
    publicinterfaceScannerFeatureDependencies{
        DbClientApi dbClient();
        HttpClientApi httpClient();
        SomeUtils someUtils();
        // Фиче Сканера нужна возможность осществлять покупкиPurchaseInteractor purchaseInteractor(); 
    }
    

    As already mentioned in the section on DI, dependencies can be implemented by anyone and in any way, these are pure interfaces, and our features are freed from this extra knowledge.

    Another important component of the “clean” feature is the presence of a clear api, according to which the outside world can refer to the feature.
    Here are the features of Shopping:
    publicinterfacePurchaseFeatureApi{
        PurchaseInteractor purchaseInteractor();
    }
    

    That is, the outside world can get PurchaseInteractor and through it try to make a purchase. Actually, above, we saw that the Scanner needed a PurchaseInteractor to make a purchase.

    But api features Scanner:
    publicinterfaceScannerFeatureApi{
        ScannerStarter scannerStarter();
    }
    

    And immediately bring the interface and implementation of ScannerStarter :
    publicinterfaceScannerStarter{
        voidstart(Context context);
    }
    @PerFeaturepublicclassScannerStarterImplimplementsScannerStarter{
        @InjectpublicScannerStarterImpl(){
        }
        @Overridepublicvoidstart(Context context){
            Class<?> cls = ScannerActivity.class;
            Intent intent = new Intent(context, cls);
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(intent);
        }
    }
    

    It's more interesting here. The fact is that the scanner and anti-virus are quite closed and isolated features. In my example, these features are launched on separate Activiti, with their own navigation, etc. That is, we simply need to start Activiti here. Activity dies - dies and feature. You can work on the principle of “Single Activity”, and then through the app, transfer, say, to the FragmentManager and any callback through which the feature reports that it has ended. There are many variations.
    We can also say that such features as Scanner and Anti-Theft, we are entitled to consider as independent applications. Unlike the features of Shopping, which is a feature-addition to something and by itself somehow can not really exist. Yes, it is independent, but it is a logical addition to other features.

    As you might guess, there must be some point that links the app, its implementation and the necessary features of dependence. This point is the Dagger component.
    An example of the components of the features of the Scanner:
    @Component(modules = {
        ScannerFeatureModule.class,
        ScreenNavigationModule.class
        // ScannerFeatureDependencies - api зависимостей фичи Сканера
    }, dependencies = ScannerFeatureDependencies.class)
    @PerFeature// ScannerFeatureApi - api фичи СканнераpublicabstractclassScannerFeatureComponentimplementsScannerFeatureApi{
        privatestaticvolatile ScannerFeatureComponent sScannerFeatureComponent;
        // классический синглтонpublicstatic ScannerFeatureApi initAndGet(
            ScannerFeatureDependencies scannerFeatureDependencies){
            if (sScannerFeatureComponent == null) {
                synchronized (ScannerFeatureComponent.class) {
                    if (sScannerFeatureComponent == null) {
                        sScannerFeatureComponent = DaggerScannerFeatureComponent.builder()
                            .scannerFeatureDependencies(scannerFeatureDependencies)
                            .build();
                    }
                }
            }
            return sScannerFeatureComponent;
        }
        // этот метод используется в модуле Скана для инжекта необходимых зависимостейpublicstatic ScannerFeatureComponent get(){
            if (sScannerFeatureComponent == null) {
                thrownew RuntimeException(
                    "You must call 'initAndGet(ScannerFeatureDependenciesComponent 
                     scannerFeatureDependenciesComponent)' method"
                );
            }
            return sScannerFeatureComponent;
        }
        // обнуление компонента фичи (когда активити Сканера умирает)publicvoidresetComponent(){
            sScannerFeatureComponent = null;
        }
        publicabstractvoidinject(ScannerActivity scannerActivity);
        // для удобной инициализации Презентеров для скармливания их в Moxypublicabstract ScannerScreenComponent scannerScreenComponent();
    }
    


    I think nothing new for you.

    Transition to multi-modularity


    So, we managed to clearly define the boundaries of the features through the API of its dependencies and the external API. We also figured out how to turn it all in Dagger. And now we come to the next logical and interesting step - the division into modules.
    Immediately open the test case - it will go easier.
    Let's look at the picture in general:

    And look at the structure of the packages of the example:

    And now let's talk carefully about each item.

    First of all, we see four large blocks: Application , API , Impl and Utils . In API , Impl and UtilsYou may notice that all modules begin either at core- or at feature- . Let's first talk about them.

    Core and feature separation


    I divide all modules into two categories: core- and feature- .
    In feature- , as you might guess, our features. In the core, there are such things as utilities, work with the network, database, etc. But there are no interface features. And the core is not a monolith. I am for splitting the core module into logical pieces and against loading features with some other interfaces.
    In the module name, we first write core or feature . Next in the module name is a logical name ( scanner , network , etc.).

    Now about four big blocks: Application, API, Impl and Utils


    API
    Each feature- or core-module is divided into API and Impl . The API is an external api through which you can access the feature or core. Only this, and nothing more:

    In addition, the api-module does not know anything about anyone, it is an absolutely isolated module.

    Utils
    The only exception to the rule above can be considered some kind of quite utility things that are meaningless to break into api and implementation.

    Impl
    Here we have a sub-division into core-impl and feature-impl .
    Modules in the core-implalso completely independent. Their only dependency is the api-module . For example, take a look at the build.gra module of the core-db-impl module :
    // bla-bla-bla
    dependencies {
        implementation project(':core-db-api')
        // bla-bla-bla
    }
    

    Now about feature-impl . There is already the lion's share of application logic. The modules of the feature-impl group may know about the modules of the API group or Utils , but they definitely don’t know anything about the other modules of the Impl group .
    As we remember, all external dependencies of a feature are accumulated in the external dependencies api. For example, for a scan feature, this api looks like this:
    publicinterfaceScannerFeatureDependencies{
        // core-db-apiDbClientApi dbClient();
        // core-network-apiHttpClientApi httpClient();
        // core-utilsSomeUtils someUtils();
        // feature-purchase-apiPurchaseInteractor purchaseInteractor();
    }
    

    Accordingly, the build.gradle feature-scanner-impl will be like this:
    // bla-bla-bla
    dependencies {
        implementation project(':core-utils')
        implementation project(':core-network-api')
        implementation project(':core-db-api')
        implementation project(':feature-purchase-api')
        implementation project(':feature-scanner-api')
        // bla-bla-bla
    }
    

    You may ask, why is api external dependencies not in the api module? The fact is that this is an implementation detail. That is, it is a specific implementation that needs some specific dependencies. For the Scanner, the dependencies are here:


    A small architectural digression
    Let's digest all of the above and clarify for ourselves some of the architectural aspects of feature -...- impl-modules and their dependencies on other modules.
    I met two of the most popular addiction patterns for a module:

    • The module can know about anyone. There are no rules. There is nothing to even comment on.
    • Modules only know about the core module . And in the core module all interfaces of all features are concentrated. This approach is not very appealing to me, since there is a risk of turning the core into another garbage bin. In addition, if we want to transfer our module to another application, we will need to copy these interfaces to another application, and also place it in the core . By itself, the blunt copy-paste of interfaces is not very attractive and reusable in the future, when the interfaces can be updated.

    In our example, I advocate for knowledge of api modules and api only (well, utils-groups). Fichi absolutely do not know anything about the implementation.

    But it turns out that features can know about other features (via api, of course) and run them. Do not end up with porridge?
    Fair remark. It's hard to work out some kind of super-clear rules. In everything there should be a measure. We have already touched on this issue a little bit, dividing the features into independent (Scanner and Anti-Theft) - completely independent and separate, and features “in context”, that is, always launched within something (Purchase) and usually implying business logic without ui. That is why the Scanner and Anti-Theft are aware of Purchases.
    Another example. Imagine that in Anti-Theft there is such a thing as wipe data, that is, absolutely all data cleared from the phone. There are a lot of business logic, ui, it is completely isolated. Therefore, it is logical to allocate wipe data into a separate feature. And then the fork. If wipe data is always launched only from Anti-Theft and is always present in Anti-Theft, then it is logical that Anti-Theft would know about wipe data and launch it on its own. And the accumulating module, the app, would then know only about Anti-Theft. But if wipe data can be run somewhere else or is not always present in Anti-Theft (that is, it can be different in different applications), then it is logical that Anti-Theft does not know about this feature and just say something external (via Router, through some kind of callback, it doesn't matter) that the user pressed such and such a button,

    Also there is an interesting question about transferring features to another application. If we, for example, want to transfer the Scanner to another application, then we must also transfer in addition to the modules : feature-scanner-api and : feature-scanner-impl and the modules on which the scanner depends ( : core-utils,: core-network- api,: core-db-api,: feature-purchase-api ).
    Yes, but! Firstly, all your api-modules are completely independent, and there are only interfaces and data models. No logic. And these modules are clearly logically separated, and : core-utils is usually a common module for all applications.
    Secondly, you can build api-modules in the form of aar and deliver them via maven to another application, or you can connect them in the form of a guitar sub-module. But you will have versioning, there will be control, there will be integrity.
    Thus, the reuse of the module (more precisely, the module-implementation) in another application looks much simpler, clearer and safer.

    Application


    It seems that we have a slender and clear picture with features, modules, their dependencies, and that’s all. Now we come to a climax - this is a combination of api and their implementations, the substitution of all the necessary dependencies, and so on, but now from the point of view of the graded modules. The point of connection is usually the app itself .
    By the way, in our example such point is still the feature-scanner-example . The above approach allows you to run each of its features as a separate application, which greatly saves assembly time during active development. Beauty!

    Consider, for a start, how everything through the app happens on the example of the already beloved Scanner.
    Quickly recall the feature:
    Api external dependencies Scanner is:
    publicinterfaceScannerFeatureDependencies{
        // core-db-apiDbClientApi dbClient();
        // core-network-apiHttpClientApi httpClient();
        // core-utilsSomeUtils someUtils();
        // feature-purchase-apiPurchaseInteractor purchaseInteractor();
    }
    

    Therefore : feature-scanner-impl depends on the following modules:
    // bla-bla-bla
    dependencies {
        implementation project(':core-utils')
        implementation project(':core-network-api')
        implementation project(':core-db-api')
        implementation project(':feature-purchase-api')
        implementation project(':feature-scanner-api')
        // bla-bla-bla
    }
    


    Based on this, we can create a Dagger component implementing the api external dependencies:
    @Component(dependencies = {
            CoreUtilsApi.class,
            CoreNetworkApi.class,
            CoreDbApi.class,
            PurchaseFeatureApi.class
        })
    @PerFeatureinterfaceScannerFeatureDependenciesComponentextendsScannerFeatureDependencies{ }
    

    I have placed this interface in ScannerFeatureComponent for convenience:
    @Component(modules = {
        ScannerFeatureModule.class,
        ScreenNavigationModule.class
    }, dependencies = ScannerFeatureDependencies.class)
    @PerFeature
    public abstractclassScannerFeatureComponentimplementsScannerFeatureApi{
        // bla-bla-bla@Component(dependencies = {
            CoreUtilsApi.class,
            CoreNetworkApi.class,
            CoreDbApi.class,
            PurchaseFeatureApi.class
        })
        @PerFeature
        interface ScannerFeatureDependenciesComponentextendsScannerFeatureDependencies { }
    }
    


    Now App. App knows about all the modules it needs ( core-, feature-, api, impl ):
    // bla-bla-bla
    dependencies {
        implementation project(':core-utils')
        implementation project(':core-db-api')
        implementation project(':core-db-impl')
        implementation project(':core-network-api')
        implementation project(':core-network-impl')
        implementation project(':feature-scanner-api')
        implementation project(':feature-scanner-impl')
        implementation project(':feature-antitheft-api')
        implementation project(':feature-antitheft-impl')
        implementation project(':feature-purchase-api')
        implementation project(':feature-purchase-impl')
        // bla-bla-bla
    }
    

    Next, create an auxiliary class. For example, FeatureProxyInjector . It will help to correctly initialize all the components, and it is through this class that we will turn to hardware. Let's see how the feature of the Scanner is initialized in us:
    publicclassFeatureProxyInjector{
        // another...publicstatic ScannerFeatureApi getFeatureScanner(){
            return ScannerFeatureComponent.initAndGet(
                DaggerScannerFeatureComponent_ScannerFeatureDependenciesComponent.builder()
                    .coreDbApi(CoreDbComponent.get())
                    .coreNetworkApi(CoreNetworkComponent.get())
                    .coreUtilsApi(CoreUtilsComponent.get())
                    .purchaseFeatureApi(featurePurchaseGet())
                    .build()
            );
        }
    }
    

    Outward, we return the features interface ( ScannerFeatureApi ), and inside we just initialize the entire dependency graph of implementation (via the ScannerFeatureComponent.initAndGet (...) method ).
    DaggerPurchaseComponent_PurchaseFeatureDependenciesComponent is the Dagger -generated implementation of the PurchaseFeatureDependenciesComponent , which we discussed above, where we substitute the implementation of the api modules into the builder.
    That's all the magic. Look again at the example .

    By the way, about example . In example, we also need to satisfy all external dependencies : feature-scanner-impl. But since this is an example, we can substitute dummy classes.
    How will it look like:
    // создаем вот такую реализацию ScannerFeatureDependenciespublicclassScannerFeatureDependenciesFakeimplementsScannerFeatureDependencies{
        @Overridepublic DbClientApi dbClient(){
            returnnew DbClientFake();
        }
        @Overridepublic HttpClientApi httpClient(){
            returnnew HttpClientFake();
        }
        @Overridepublic SomeUtils someUtils(){
            return CoreUtilsComponent.get().someUtils();
        }
        @Overridepublic PurchaseInteractor purchaseInteractor(){
            returnnew PurchaseInteractorFake();
        }
    }
    // и где-нибудь в Application-файле инициализируем графpublicclassScannerExampleApplicationextendsApplication{
        @OverridepublicvoidonCreate(){
            super.onCreate();
            ScannerFeatureComponent.initAndGet(
                // да, Даггер отлично съедает это =)new ScannerFeatureDependenciesFake()
            );
        }
    }
    

    And the very feature of the Scanner in example is run through the manifest so as not to fence off additional empty activations:
    <?xml version="1.0" encoding="utf-8"?><manifestxmlns:android="http://schemas.android.com/apk/res/android"package="com.example.scanner_example"><applicationandroid:name=".ScannerExampleApplication"android:allowBackup="true"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/AppTheme"><!--Вот она активити Сканера--><activityandroid:name="com.example.scanner.presentation.view.ScannerActivity"><intent-filter><actionandroid:name="android.intent.action.MAIN" /><categoryandroid:name="android.intent.category.LAUNCHER" /></intent-filter></activity></application></manifest>


    Algorithm of transition from monomodularity to multimodularity


    Life is a harsh thing. And the reality is that we all work with Legacy. If someone is sawing a new project right now, where you can refill everything at once, then I envy you, bro. But I have not, and that guy is also not so =).

    How to translate your application into multiple modules? I heard mostly about two options.
    The first. Splitting the application into modules here and now. True, your project may not be ready for a month or two =).
    Second. Try to pull features out gradually. But at the same time all sorts of dependencies of these features stretch. And here the most interesting begins. The code of dependencies can be pulled by another code, the whole thing migrates to the common module , to the core moduleback and forth in a circle. As a result, pulling one feature can entail working with a good half of the application. And again at the beginning of your project will not be collected a decent amount of time.

    I am in favor of a gradual transfer of the application to multi-modularity, since in parallel we still need to cut new features. The key idea is that if your module needs some of the dependencies, you should not immediately physically drag the code into the modules . Let's look at the module removal algorithm using the example of a scanner:

    • Create an apfich, put it in a new api-module. That is, to completely create a module : feature-scanner-api with all interfaces.
    • Create : feature-scanner-impl . In this module, physically transfer all the code related to the feature. Everything that your feature depends on, the studio will immediately highlight.
    • Identify external dependencies features. Create the appropriate interfaces. These interfaces are divided into logical api-modules. That is, in our example, create the modules : core-utils,: core-network-api,: core-db-api,: feature-purchase-api with the appropriate interfaces.
      I advise all the same to immediately invest in the name and meaning of the modules. It is clear that over time, interfaces and modules may be a little shuffled, collapsed, etc., this is normal.
    • Create external dependencies ( ScannerFeatureDependencies ). Depending : feature-scanner-impl register recently created api-modules.
    • Since in the app we have everything legacy, that's what we are doing. In the app, we include all modules created for the feature (api-module features, impl-module features, api-modules of external feature dependencies).
      Super important moment . Next, in the app, we create the implementation of all necessary feature dependency interfaces (Scanner in our example). These implementations will be rather just proxy from your dependencies to the current implementation of these dependencies in the project. When you initialize a feature, you substitute implementation data.
      Difficult words, want an example? So he is already there! In fact, something similar is already in the feature-scanner-example. Once again I will give it a little adapted code:
      // создаем вот такую реализацию ScannerFeatureDependencies в app-модулеpublicclassScannerFeatureDependenciesLegacyimplementsScannerFeatureDependencies{
          @Overridepublic DbClientApi dbClient(){
              returnnew DbClientLegacy();
          }
          @Overridepublic HttpClientApi httpClient(){
              // какое-то легаси// главное, что мы имплементируем наш апиreturn NetworkFabric.createHttpClientLegacy();
          }
          @Overridepublic SomeUtils someUtils(){
              returnnew SomeUtils();
          }
          @Overridepublic PurchaseInteractor purchaseInteractor(){
              returnnew PurchaseInteractorLegacy();
          }
      }
      // и где-нибудь инициализируем граф
      ScannerFeatureComponent.initAndGet(
          new ScannerFeatureDependenciesLegacy()
      );
      

      That is the main message here is this. Let all the external code necessary for the feature live in the app , as well as lived. And the feature itself will already work with it in a normal way, through api (meaning api dependencies and api-modules). In the future, the implementation will gradually move to the modules. But on the other hand, we will avoid an endless game with dragging and dropping from the module into the module the necessary external code for the feature. We can move in clear iterations!
    • Profit

    Here is a simple, but working algorithm that allows you to move to your goal step by step.

    Additional tips


    How big / small should features be?
    It all depends on the project, etc. But at the beginning of the transition to multi-modularity, I advise you to split up into large pieces. Further, if necessary, you will select from these modules more modules. But do not shrink. Do not do this: one / several classes = one module.

    Clean app-module
    When switching to a multi-module app , we will have a rather large one, and from there, your dedicated features will be twitching too. It is possible that in the course of the work you will have to make edits to it legacy, something to finish there, well, or you just have a release, and you are not up to cuts into modules. In this case, you want the app , and with it all legacy, to know about the selected features only through the API, no knowledge about the implementation. Butapp , in fact, combines api- and impl-modules , and therefore the app knows about all.
    In this case, you can create a special module : adapter , which will be the connecting point of api and impl, and then the app will only know about api. I think the idea is clear. You can see an example in the clean_app branch . I will add that with Moxy, or rather MoxyReflector, there are some problems when splitting into modules, because of which I had to create another additional module : stub-moxy-java . Light pinch of magic, so far without it.
    The only amendment. This will only work if your feature and corresponding dependencies have already been physically moved to other modules. If you learned a feature, but the dependencies still live in the app , as in the algorithm above, this will not work.

    Afterword


    The article turned out rather big. But I hope that it will really help you in the fight against mono-modularity, awareness of how it should be, and how to make friends with DI.
    If you are interested in plunging into a problem with assembly speed, how to measure everything, then I recommend the reports of Denis Neklyudov and Zhenya Suvorov (Mobius 2018 Piter, videos are not publicly available yet).
    About Gradle. The difference between api and implementation in the gradle was perfectly shown by Vova Tagakov . If you want to reduce the multi-modulus boilerplate, you can start here with this article .
    I would welcome comments, amendments, as well as likes! All clean code!

    Also popular now: