📜 ⬆️ ⬇️

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. 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 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 with two buttons:


By clicking on these buttons we get to the feature of the 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.

First way
Let us set all dependencies on the features of the Shopping Cart 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 .

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:


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 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 ) @PerFeature public abstract class FeatureComponent { // ... } public interface FeatureDependencies { SomeDependency someDependency(); } @Component( modules = AnotherFeatureModule.class ) @PerFeature public abstract class AnotherFeatureComponent implements FeatureDependencies { // ... } 


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:
 public interface PurchaseFeatureDependencies { HttpClientApi httpClient(); } 

Or the external dependency interface features of the Scanner:
 public interface ScannerFeatureDependencies { 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:
 public interface PurchaseFeatureApi { 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:
 public interface ScannerFeatureApi { ScannerStarter scannerStarter(); } 

And immediately bring the interface and implementation of ScannerStarter :
 public interface ScannerStarter { void start(Context context); } @PerFeature public class ScannerStarterImpl implements ScannerStarter { @Inject public ScannerStarterImpl() { } @Override public void start(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   public abstract class ScannerFeatureComponent implements ScannerFeatureApi { private static volatile ScannerFeatureComponent sScannerFeatureComponent; //   public static ScannerFeatureApi initAndGet( ScannerFeatureDependencies scannerFeatureDependencies) { if (sScannerFeatureComponent == null) { synchronized (ScannerFeatureComponent.class) { if (sScannerFeatureComponent == null) { sScannerFeatureComponent = DaggerScannerFeatureComponent.builder() .scannerFeatureDependencies(scannerFeatureDependencies) .build(); } } } return sScannerFeatureComponent; } //           public static ScannerFeatureComponent get() { if (sScannerFeatureComponent == null) { throw new RuntimeException( "You must call 'initAndGet(ScannerFeatureDependenciesComponent scannerFeatureDependenciesComponent)' method" ); } return sScannerFeatureComponent; } //    (   ) public void resetComponent() { sScannerFeatureComponent = null; } public abstract void inject(ScannerActivity scannerActivity); //         Moxy public abstract 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 example packages:

And now let's talk carefully about each item.

First of all, we see four large blocks: Application , API , Impl and Utils . In the API , Impl and Utils, you can notice that all modules start 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 on core-impl and feature-impl .
Modules in core-impl are also completely independent. Their only dependency is the api-module . For example, take a look at the core.db-impl module build.gradle :
 // 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:
 public interface ScannerFeatureDependencies { // core-db-api DbClientApi dbClient(); // core-network-api HttpClientApi httpClient(); // core-utils SomeUtils someUtils(); // feature-purchase-api PurchaseInteractor 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 Add-on Scanner, add it here:


A small architectural retreat
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:


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 does not matter) that the user pressed such a button, and what to launch under it is a matter of the Consumer Anti-theft feature (a specific application, a specific app).

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:
 public interface ScannerFeatureDependencies { // core-db-api DbClientApi dbClient(); // core-network-api HttpClientApi httpClient(); // core-utils SomeUtils someUtils(); // feature-purchase-api PurchaseInteractor 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 }) @PerFeature interface ScannerFeatureDependenciesComponent extends ScannerFeatureDependencies { } 

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


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:
 public class FeatureProxyInjector { // another... public static 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 must also satisfy all external dependencies : feature-scanner-impl . But since this is an example, we can substitute dummy classes.
How will it look like:
 //     ScannerFeatureDependencies public class ScannerFeatureDependenciesFake implements ScannerFeatureDependencies { @Override public DbClientApi dbClient() { return new DbClientFake(); } @Override public HttpClientApi httpClient() { return new HttpClientFake(); } @Override public SomeUtils someUtils() { return CoreUtilsComponent.get().someUtils(); } @Override public PurchaseInteractor purchaseInteractor() { return new PurchaseInteractorFake(); } } //  -  Application-   public class ScannerExampleApplication extends Application { @Override public void onCreate() { 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"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.scanner_example"> <application android: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"> <!--   --> <activity android:name="com.example.scanner.presentation.view.ScannerActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android: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 is migrating to the common module , to the core module and back, and so on 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:


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 own selected 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. But after all app , in fact, unites api- and impl-modules , and therefore app knows about all.
In this case, you can create a special module : adapterwhich is exactly the connecting point 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 dealing with 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!

Source: https://habr.com/ru/post/422555/


All Articles