banner
jzman

jzman

Coding、思考、自觉。
github

Application of Android Componentization

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

The previous article provided a general summary of some fundamental issues in 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 Applications

The Role of Application#

When an Android application starts, the first thing that is launched 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: Called when the application is created, this callback occurs before any Activity.
  • onTerminate: Called when the application is terminated; it cannot be guaranteed that this will 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: Called when a configuration change occurs, such as screen rotation.
  • onTrimMemory: Notifies the application of different memory situations. The memory level descriptions below come from

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

image

As a singleton object for the entire app, the Application serves the following purposes:

  1. As the entry point of the app, it can be used to initialize basic configurations, such as third-party SDK initialization.
  2. Variables for global use can be defined in the Application; however, when the application is force-closed, there may be a null pointer issue, leading to crashes when reopening the application. If this usage is necessary, be sure to 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 memory usage based on memory priority to decrease the likelihood of being force-closed by the system.

Merging Applications#

AndroidManifest is the declaration configuration file for each Module. When generating an app, there should also be a corresponding AndroidManifest file. In cases where multiple Modules depend on each other, it is necessary to merge the contents of the child Module's AndroidManifest file into the main Module's AndroidManifest file. The final merged 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 like Activity will be specified as package name + file name.

The rules for merging Applications in the AndroidManifest file are as follows:

  1. If the child Module has a custom Application and the main Module does not, 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, the final merged AndroidManifest file will use the Application from the main Module.
  3. If multiple child Modules have custom Applications, the final merged AndroidManifest file will use the Application from the last compiled Module after resolving conflicts.
  4. If both the child Module and the child Module have custom Applications, 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, with the following error message:

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 Applications#

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, specifically by 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 base initialization class, it should be placed in the base Module, as the base class is depended upon by all Modules. Each child Module can then inherit BaseAppInit to implement its own 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 instances of each child Module's initialization class 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 for 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 reflection decouples components, it also somewhat reduces application performance. Of course, the purpose of componentization is to decouple various components or Modules as much as possible. If sacrificing a bit of performance can achieve maximum decoupling, it is acceptable.

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