ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

Modularization of Android Applications with lazy initialization

--

Continue the series of the articles about modularization of Android Applications.

Modularization articles cycle:

  1. Modularization of Android Applications in 2021.
  2. Modularization of Android Applications with an explicit initialization.
  3. Modularization of Android Applications with lazy initialization. <- the current.

Just a reminder.

In the first article, we looked at the general principles of splitting an application into modules.

In the second article, we reviewed one of the approaches to gluing modules, with manual initialization and manual reset.

In the current article, we consider another approach to gluing modules, with lazy initialization and automatic reset.

In the first article, we described the process of splitting an application into modules, what a module should be responsible for and how modules should depend on one another. To understand how to split an application into modules or compose an application with multimodule architecture, read the first article. The current article provides an alternative approach to module gluing, described in the second article.

In the first article, we agreed that we will use the pattern Component Holder. Please refer to the first article to find out what that is.

In the second article, you can find one of the possible implementations of the Component Holder. Let us look at it.

The functions are as follows:

  1. init(), where you can put FeatureDependencies for the component. The function creates the component.
  2. get(), which returns FeatureApi.
  3. reset(), which must be called when the component is not needed anymore.

In the implementation of the Component Holder, a hard-reference to the component is stored. Calling reset() makes it null.

If you try to use this implementation of the Component Holder, questions might arise. For example:

  • If a component is used by several components and if one of the components calls reset(), then what will happen to the other components? The reference to the component being used is made null, but the component is still used by others. Perhaps we should add a reference counter to the Component Holder and reset() call will decrease the counter and make the reference null only if the counter is 0.
  • When should reset() be called? For components related to Activity/Fragment, we can call it when Activity/Fragment is completely destroyed. But what about common components (utility, core, shared features)? The developer who uses the module may never call reset(), just for the sake of safety. So, we will have components that persist forever. Moreover, the components will hold references to their dependencies.

Ok, we can add the reference counter in the Component Holder. And no one will be too scared to call reset(). But anyway, we have the risk that the developer just will not call reset().

Eventually, this approach with init()/reset() and a reference counter resembles the approach to object lifecycle management used in languages that include a garbage collector, e.g. Java.

Android uses Java Virtual Machine, and we might ask the question: can we implement the Component Holder without reset() and have the component freed automatically if it is not used anymore? In other words, if no one holds a reference to the component, it would be automatically destroyed by JVM. Yes, that is possible! The Component Holder with lazy initialization will help us achieve just that.

Component Holder with lazy initialization

Let us look at the interface of the Component Holder with lazy initialization.

Here, you see the property dependencyProvider, where you must put a provider of FeatureDependencies. Why the provider and not a FeatureDependencies object? That is because we do not want to store references to dependencies inside the Component Holder. Otherwise, the references will be held forever, because the Component Holder is a global object.

The function get() returns the FeatureApi of a module. Another module calls get() to get the module’s API.

Let us go deep into the implementation of the Component Holder. This code must be in every particular module, but in a real project, you should not duplicate it. Make a delegate. In the sample, you can find the code of the delegate. For convenience, below is the full code of the particular Component Holder with lazy initialization.

First of all, to allow the component to self-destruct, we must not store the reference to it inside the Component Holder. However, we want to get the reference to the component if it is already initialized. To do this, we will use WeakReference. The reference to the component is stored in the private property componentWeakRef.

Then, we need to provide a reference to the inner component, because the component can be used to provide inner module dependencies and we need to do dependency injection inside the module. Also, we need to provide FeatureApi externally. That is why we have two functions in the Component Holder: getComponent() which is not available outside of the module (internal) and get(), which is available externally. In the example, the function get() just calls getComponent(), because if you use Dagger, it is very convenient to force the component to implement FeatureApi; Dagger creates getters for us out-of-the-box. Here is a very important thing: references returned from get() and getComponent() must refer, directly or indirectly, to the inner component. Why is this important? That will be described later. Fortunately, it is easy to do, especially with Dagger.

Let us take a closer look at what happens when someone gets a reference to the component via the Component Holder (calls get() or getComponent()).

During the call, we check that dependencyProvider is set. We cannot init the component if dependencyProvider is not set, so we provide a reminder by throwing an exception.

Then, check the WeakReference to the component. If the reference is not initialized, we will create the component, save the reference in the Component Holder and return the reference.

A component will remain alive until other components hold a reference to it. A Component Holder as a global object does not affect the component’s lifetime.

If some other component needs to get the same component, it will get the existing reference to it. Or, the referred component will be created if it has been destroyed already.

What do we need to do next? Glue the modules. To do that, first of all, in Application.onCreate(), we need to set dependencyProvider in all Component Holders. The order does not matter, because Components will be created in a lazy manner.

You might think that initialization of many modules in Application.onCreate() is not a very good idea, because there will be a lot of code in one place. However, this code can be split into functions or files. And if you want to find the code for a particular module, just open the module and go to the FeatureDependencies implementation. Plus, as mentioned above, the order is not important. You will not need to work on this code too much.

One more important thing. Modules are not initialized here, i.e. inner components are not created immediately inside Application.onCreate(). Component Holders are initialized here, i.e. global tiny objects. Then a module (the inner component of the module) will be initialized lazily — when somebody gets it, and it will be destroyed automatically when it is not in use anymore.

Let us look at the code in Application.onCreate(). The code jumps ahead slightly: DependencyHolder is used, to be described in the next section. For now, it is important to understand that inside dependencyProvider, we call get() for each used component.

To understand this better, let us look at the following scheme.

Say, there is a module named Feature1, which uses some interfaces from the modules Feature2 and Feature3:

A call to Feature1ComponentHolder.get() will proceed as shown below.

When a component is used for the first time (get() or getComponent() is called), the Component Holder will initialize each component it requires one by one — unless the component has been initialized already — and then initialize the component.

This looks very interesting, but the approach has some issues. The issues follow from the nature of WeakReference: the component is only alive if someone holds a regular (hard) reference to it. So, the component could die unexpectedly and we would be in trouble.

Let us look at an example.

There are two modules: Foo and Bar. Let the module Foo have a State.

Inside module_bar, a FooWithState interface object is created from module_foo, and the object is subsequently used. However, the Foo component releases its state and dies immediately as no one holds a reference to it. Sad. This is no big deal if the state is basic, e.g. a text string. But the state can be mutable or observable, for example, a RxJava subject or Kotlin coroutine channel. In that case, the code might observe one state, but the inner component might send events to another.

Also, we may want to a component to remain alive while we use something in it. Let us imagine this situation: in the module API, we have two interfaces: interface1 and interface2. We get a reference from the component to an object of interface1, and the component dies immediately. Then we get a reference to an object of interface2 from the same component, but the object will be created from another instance of the component. If the implementations of interface1 and interface2 are connected in some way, then users of the component might get unexpected issues.

What can we do? Obviously, we need to save a reference to the whole Foo component inside the Bar component. We will formulate this as a rule: a component must store hard references to each used component. But how can you be sure that the rule will be followed? We would like to make it impossible to create a component if the references to used components are not saved inside the component. The simplest way to do this is by adding a new property to the BaseFeatureDependencies interface for the object that holds references to each used component. Meet the new entity: Dependency Holder.

Dependency Holder

So, we have agreed that BaseFeatureDependencies stores reference to the Dependency Holder object, which holds references to each used component.

We agreed that ComponentHolder.get() must return a reference to the inner component. This is important because this is the only reference available outside the module that can be used to save the component from dying. That is why the Dependency Holder below holds references to FeatureAPI, returned from ComponentHolder.get().

So, in BaseFeatureDependencies, we have the property dependencyHolder:

However, we also would like for FeatureDependencies to be created together with Dependency Holder, so that the creation of Dependency Holder entails the creation of FeatureDependencies automatically. Otherwise, we might forget to add a reference to a component into the Dependency Holder.

To do this, we can create the following abstract class for the Dependency Holder:

It will be used as follows:

Here we will have to write many abstract Dependency Holders with different numbers of used components. The example above is for two used components. However, in a real project, a module can use many more. And for each number, we need a separate abstract class. You can write a large number of Dependency Holders in advance that accept from 0 to 20 parameters and add more later if needed.

The requirement of having many abstract Dependency Holders is a drawback of the suggested Dependency Holder implementation. However, adding the abstract class is a trivial task: just copy the abstract class and create a new one with the required number of arguments. By the way, a situation where a module uses very many other modules is unlikely. If a module uses more than 20 other modules, something is wrong with the application architecture.

However, if you know a better Dependency Holder implementation, reach out to me or write an article about this.

Component of Activity and other entities with their own Context

It is important to mention components that contain entities with Android Context, e.g. Activity.

What is wrong with Activity?

Imagine we have an Activity, and it has a Presenter — if we use the MVP pattern — or another entity responsible for the business logic of the Activity.

Obviously, we want to create a Presenter via an Activity component. Let the Activity start from another Activity, then the parent Activity saves a reference to the component of the child Activity (see above: we agreed that a component must save a reference to each used component) and it looks good. It is good indeed.

However, there are more ways to start an Activity. It can be started from the launcher, a notification and even another application, i.e. an Activity may have no parent component. Where do we save a reference to the Activity component is that case? Answer: save the reference inside the Activity. In other words, the Activity must save a reference to its own component.

Android Activity objects can be re-created while the Activity content is still visible, for example on screen rotation. Therefore, a reference to the Activity component must be saved in a safe place, for example, where the reference will be preserved on screen rotation. In the case of MVP, if you use Moxy, the reference can be saved in Presenter. In the case of MVI, if you use MVIKotlin, the reference can be saved in InstanceKeeper.

Should we do the same for Android Fragments? No — because Fragments must be attached to the Activity, they are created from the Activity component or from components being used whose references are stored in the Activity component.

Besides the Activity, Android has other entities that can be created outside of the application, e.g. Services and Broadcast Receivers. These also must store references to their own components.

If a component must remain alive forever (while the application is running), the application component must depend on that component. A reference to the application component is stored in the Application object.

A general rule: if an entity can be started from the outside (with its own context), a reference to the component of the entity must be stored in the entity object.

What if the system kills the application and then restores the activities and fragments? In that case, all components will reinitialize themselves as when the Activity starts normally.

Conclusion

Component Holders with lazy initialization and automatic reset make it easier to glue modules. Modules are initialized automatically on demand and released when they are not in use.

The developer does not need to manage the component lifecycle manually, use DI framework between modules, etc. It is simple: a component is only alive as long as it remains in use. If a component is not in use, its instance does not exist either.

An example of using the above approach:

I extend my gratitude to Michael Emelyanov, Eugeny Matsyuk, Andrey Beryukhov, Timur Almametov, Mansur Biryukov, Stepan Goncharov, Alexander Blinov, Sergey Boishtyan for the article review, valuable comments and simple food for thought.

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 (2)

Write a response