ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

Modularization of Android Applications with explicit initialization

--

Modularization articles cycle:

  1. Modularization of Android Applications in 2021
  2. Modularization of Android Applications with explicit initialization
  3. Modularization of Android Applications with lazy initialization

This article is a continuation of the previous one with common thoughts about our approach to splitting a monolithic Android-application code base into modules. It is strongly recommended to see through it before reading this one.

To remind there we had three base interfaces: ComponentHolder, BaseAPI, and BaseDependencies put in Module-injector to be implemented in all feature-modules.

These interfaces look like this:

Now I suggest you look at the implementation of one of the ComponentHolder, which involves manual management of the component lifecycle:

This object consists of four parts:

  • Nullable variable purchaseComponentHolder for keeping component's link.
  • The init() function which receives an interface with dependencies of this module as a parameter and initializes the variable with a component.
  • The get() function which can be used by other components after initialization of current to retrieve the module API.
  • The reset() function which resets the component's link when it is not needed anymore.

In other modules, this object will be approximately the same. This structure allows you to store a reference to a component in a single place. When the link is null, the garbage collector will be able to remove the entire component from memory.

The code of the PurchaseFeatureApi and PurchaseFeatureDependencies classes is quite simple and can be viewed in the repository.

Inside the module, the ComponentHolder can include the Dagger component directly:

Here we only care about the initAndGet() function that ComponentHolder uses. Once again Dagger is only here for example. It can be any DI framework or even manual dependencies injection for simple modules instead.

Finally, the modules are glued together inside the app as follows:

Here, the first provideScannerFeatureDependencies() function is used to populate the ScannerFeatureDependencies interface, which is used inside the second provideFeatureScanner() function to initialize the feature's ComponentHolder.

In this case, it turns out that the app module is aware of all the others. However, none of the components are duplicated, and they all remain in a single instance.

If the app-module contains only modules glue code, then its reassembly does not greatly affect the speed of assembly as a whole. When the Dagger AppModule grows, it can be logically divided into separate modules.

It remains to consider in more detail how the components are reset.

As mentioned above, each module’s ComponentHolder has a reset() function that nullifies the reference to the component inside it.

If the module lifetime should be associated with the UI, you can call reset() inside the corresponding function of some type of Lifecycle Observer (which can also be a Presenter or directly an Activity, as in our example):

It seems that everything is fine and the component can be removed by the garbage collector. But the reference to the component leaks out of the module via the get() function. A detailed explanation of the leak can be found here.

That is why DI framework in the app-module needs to try to initialize and get a reference to the component each time instead of caching it — for example, when using Singleton in Dagger. The init() function checks whether the component has been initialized, therefore the same reference will be returned before calling reset().

The second important condition for correctly releasing a reference to a component is the use of Provider<T> (in terms of Dagger) at the injection site:

Gluing UI

We learned how to form modules, extract their APIs and dependencies, and glue them into a single application. It should be clear with the introduction of ordinary classes in the API now. However, there is still the question of navigation between UI components such as Activity, fragments, or individual Views.

To start an Activity, you can put it in an API like this:

You can use it to pass the context of the current Activity inside the module, which itself will form and launch the necessary Intent:

In the case of fragments, you can either pass the FragmentManager inside the module, then create and run the needed fragment inside, or put a function in the API that returns the instantiated fragment. This approach will allow you to use any tool for navigation, whether it is Navigation Component, Cicerone or manual work with FragmentManager.

It seems that now you know everything to start splitting an application into modules. Finally, I just want to share a few life hacks that we found useful for ourselves.

Life hacks for working with modules

Core-UI

We reuse some common UI components among applications, as well as a common set of styles and themes that correspond to the company UI guidelines. For this, we use the internal UiKit library. In a project, the common theme can be customized and then applied at the Application level.

We noticed that with the allocation of modules, theme.xml files in which the same root theme is customized, e.g. in Example-modules to look like the real application, began to be duplicated.

Therefore, we decided to allocate a core-ui module with themes, styles, and colors that apply to the entire project. It connects the UiKit as a transitive dependency (api instead of implementation) and connects to all Feature-modules containing the UI. This module itself does not contain any code at all.

Core-native

To share common code between platforms in the company, we use native C++ libraries. JNI-wrappers that are called from Java code are needed for working with these. Those are building and linking together with the native libraries coming as part of the antivirus SDK.

All this code is rarely changing but takes a significant amount of build time, especially if the ABI is not restricted for the local build.

Therefore, we have now started putting these components and the thin wrappers above them into a separate module, which will rarely be rebuilt and which will shorten build time.

Verbosity

There were complaints from colleagues who started adopting our scheme of modules that sometimes it needs to write too much code for modules linking. But this is the price of manually managing a component’s lifecycle. The same can be said of any other architecture that makes code cleaner by introducing more abstractions and classes. Boilerplate code can be simplified e.g. by introducing an Android-studio template for new modules creation.

Another possible solution is to wait for a third article in this cycle, where another way of gluing modules will be shown.

That is all. I hope that this structure of a multi-module project, as well as tips for organizing modules, will help you in dealing with the monolith. Let me remind you that the example application with the structure of our multi-module architecture can be found here.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Responses (1)

Write a response