banner
jzman

jzman

Coding、思考、自觉。
github

Application of Android Componentization

PS: If you don't want to look at professional knowledge, just check out some interesting knowledge. In short, don't be too lazy.

Previous article briefly summarized some basic issues of component-based development. This article continues the study of componentization, mainly introducing the Application in componentization from the following three aspects:

  1. The role of Application
  2. Merging Applications
  3. Dynamically configuring Application

The role of Application#

When an Android application starts, the first thing that starts is the Application. Each app creates a unique Application at runtime, and its lifecycle is the lifecycle of the app. The commonly used callback methods in Application are as follows:

  • onCreate: Callback when the application is created, triggered earlier than any Activity.
  • onTerminate: Called when the application is terminated, but it cannot be guaranteed to be called.
  • onLowMemory: Called when background applications are terminated, but the foreground application's memory is still insufficient. This method can be used to release some unnecessary resources to cope with this situation.
  • onConfigurationChanged: Callback when the configuration changes, such as when the phone screen rotates.
  • onTrimMemory: Notifies the application of different memory situations. The following memory level description comes from

Attached is a description of the memory levels related to onTrimMemory summarized by Carson_Ho:

image

As a singleton object of the entire app, the Application has the following roles:

  1. As the entry point of the app, it can be used to initialize basic configurations, such as the initialization of third-party SDKs.
  2. Variables for global use can be defined in the Application, but when the application is forcefully killed, there may be null pointer issues, leading to crashes when reopening the application. If you decide to use it this way, you must handle this situation properly.
  3. The Application can manage the lifecycle state of Activities and determine whether the application is in the foreground or background. It can reduce its own memory usage based on memory priority to decrease the likelihood of being forcefully killed by the system.

Merging Applications#

AndroidManifest is the declaration configuration file for each Module. Correspondingly, when generating an app, there should also be a corresponding AndroidManifest file. In the case of multiple Modules depending on each other, it is necessary to merge the contents of the child Module's AndroidManifest file into the main Module's AndroidManifest file. Ultimately, the final AndroidManifest file will be generated in the build directory, and the specific path of the compiled AndroidManifest file is as follows:

app\build\intermediates\manifests\full\debug\AndroidManifest.xml

When merging the child Module's AndroidManifest file, the compiler will complete the use-sdk information and some unset attributes. After merging, the name attributes in components such as Activity will be specified as package name + file name.

When merging AndroidManifest files, the Application needs to be merged, and the merging rules for Application are as follows:

  1. If the child Module has a custom Application and the main Module does not have a custom Application, the child Module's Application will be merged into the final AndroidManifest file.
  2. If the main Module has a custom Application and the child Module does not have a custom Application, the main Module's Application will be used in the final merged AndroidManifest file.
  3. If multiple child Modules have custom Applications, after resolving conflicts, the Application from the last compiled Module will be used in the final merged AndroidManifest file.
  4. If the child Module has a custom Application and the child Module also has a custom Application, it will prompt to add the tools attribute in the main Module's AndroidManifest file. After compilation, the merged AndroidManifest file will use the custom Application from the main Module.

If the tools attribute is not added during the merging process, it will prompt to add the tools attribute, and the error message is as follows:

Manifest merger failed : Attribute application@name value=(com.manu.module_one.OneApplication) from [:moduel_one] AndroidManifest.xml:13:9-58
	is also present at [:module_two] AndroidManifest.xml:13:9-58 value=(com.manu.module_two.TwoApplication).
	Suggestion: add 'tools:replace="android:name"' to <application> element at AndroidManifest.xml:6:5-21:19 to override.

For example, here you need to add the tools attribute under the application tag in the child Module's AndroidManifest file:

tools:replace="android:name"

Dynamically configuring Application#

In addition to merging Applications, the initialization of each Module during componentization is also very important. Reflection can be used to complete the initialization of each Module, which means reflecting to obtain the initialization objects of the child Modules in the main Module and then calling their initialization methods. To facilitate this, a class can be defined to manage the initialization of child Modules, as follows:

/**
 * Created by jzman
 * Powered by 2019/04/15 0022.
 */
public class ModuleConfig {
    private static final String moduleOneInit = "com.manu.module_one.ModuleOneAppInit";
    private static final String moduleTwoInit = "com.manu.module_two.ModuleTwoAppInit";
    public static String[] moduleInits = {
            moduleOneInit,
            moduleTwoInit
    };
}

Create a base interface for initialization as follows:

/**
 * Unified App initialization interface
 * Created by jzman
 * Powered by 2019/04/15 0022.
 */

public interface BaseAppInit {
    /**
     * High priority initialization
     * @param application
     * @return
     */
    boolean onInitHighPriority(Application application);

    /**
     * Low priority initialization
     * @param application
     * @return
     */
    boolean onInitLowPriority(Application application);
}

To make it easy for each child Module to use this initialization base class, it should be placed in the base Module, as the base class is depended upon by all Modules. Then, each child Module can inherit BaseAppInit to implement its own Module's initialization class, as follows:

/**
 * module_one initialization file
 * Created by jzman
 * Powered by 2019/04/15 0022.
 */

public class ModuleOneAppInit implements BaseAppInit {
    private static final String TAG = ModuleOneAppInit.class.getSimpleName();

    @Override
    public boolean onInitHighPriority(Application application) {
        Log.i(TAG, "ModuleOneAppInit---onInitHighPriority");
        return true;
    }

    @Override
    public boolean onInitLowPriority(Application application) {
        Log.i(TAG, "ModuleOneAppInit---onInitLowPriority");
        return true;
    }
}

Finally, in the custom Application of the main Module, create the initialization class objects of each child Module through reflection and call their initialization methods, as follows:

/**
 * High priority initialization
 */
private void initModuleHighPriority(){
    for (String init: ModuleConfig.moduleInits){
        try {
            Class<?> clazz = Class.forName(init);
            BaseAppInit appInit = (BaseAppInit) clazz.newInstance();
            appInit.onInitHighPriority(this);
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
    }
}

/**
 * Low priority initialization
 */
private void initModuleLowPriority(){
    for (String init: ModuleConfig.moduleInits){
        try {
            Class<?> clazz = Class.forName(init);
            BaseAppInit appInit = (BaseAppInit) clazz.newInstance();
            appInit.onInitLowPriority(this);
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
    }
}

The running log is as follows:

ModuleOneAppInit---onInitHighPriority 
ModuleTwoAppInit---onInitHighPriority
ModuleOneAppInit---onInitLowPriority
ModuleTwoAppInit---onInitLowPriority

Additionally, you can create a base initialization class and BaseApplication in the base Module, and then reflectively call specific initialization methods in BaseApplication. Ultimately, it still uses reflection, just another implementation method. First, create BaseAppInit in the base Module as follows:

/**
 * Created by jzman
 * Powered by 2019/04/15 0022.
 */
public abstract class BaseAppInit {

    private Application mApplication;

    public BaseAppInit() {
    }

    public void setApplication(@NonNull Application application) {
        this.mApplication = application;
    }

    public void onCreate(){}

    public void OnTerminate(){}

    public void onLowMemory(){}

    public void configurationChanged(Configuration configuration){}
}

Create BaseApplication in the base Module as follows:

/**
 * Created by jzman
 * Powered by 2019/04/15 0023.
 */

public abstract class BaseApplication extends Application {

    private List<Class<? extends BaseAppInit>> classInitList = new ArrayList<>();
    private List<BaseAppInit> appInitList = new ArrayList<>();

    @Override
    public void onCreate() {
        super.onCreate();
        appInit();
        initCreate();
    }

    protected abstract void appInit();

    protected void registerApplicationInit(Class<? extends BaseAppInit> classInit) {
        classInitList.add(classInit);
    }

    private void initCreate() {
        for (Class<? extends BaseAppInit> classInit : classInitList) {
            try {
                BaseAppInit appInit = classInit.newInstance();
                appInitList.add(appInit);
                appInit.onCreate();

            } catch (InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void onTerminate() {
        super.onTerminate();
        for (BaseAppInit appInit : appInitList) {
            appInit.OnTerminate();
        }
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
        for (BaseAppInit appInit : appInitList) {
            appInit.onLowMemory();
        }
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        for (BaseAppInit appInit : appInitList) {
            appInit.configurationChanged(newConfig);
        }
    }
}

Then, implement the specific initialization class in the child Module, as follows:

/**
 * Created by jzman
 * Powered by 2019/04/15 0023.
 */

public class ModuleThreeAppInit extends BaseAppInit {
    private static final String TAG = ModuleThreeAppInit.class.getSimpleName();

    @Override
    public void onCreate() {
        Log.i(TAG, "ModuleThreeAppInit---onCreate");
    }
}

Finally, in the main Module, inherit BaseApplication to implement a custom Application and register the initialization files of each child Module, as follows:

/**
 * Created by jzman
 * Powered by 2019/04/15 0023.
 */

public class MApplication extends BaseApplication{
    @Override
    protected void appInit() {
        registerApplicationInit(ModuleThreeAppInit.class);
        registerApplicationInit(ModuleForeAppInit.class);
    }
}

The running log is as follows:

ModuleThreeAppInit---onCreate
ModuleForeAppInit---onCreate

Both methods above use reflection. While decoupling, reflection also somewhat reduces the performance of the application. Of course, the purpose of componentization is to make each component or Module as decoupled as possible. If sacrificing a little performance can achieve maximum decoupling, it is also acceptable.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.