从零开始手写一个组件化demo

根据上篇Android组件化学习文章,编写一个组件化demo,github地址https://github.com/chaoyangsun/MyComponentDemo。项目结构如下:
从零开始手写一个组件化demo

  • APP:主module
  • Login:登录组件
  • Pay:支付组件,支付前需要判断是否登录
  • Common:基础组件,包含基础库、公共页面等
  • CommonBase:包含各个组件对外提供访问自身数据的接口、抽象方法等

1、使用AS创建一个工程,同时创建Login、Pay、Common、CommonBase四个Libray
效果如下:
从零开始手写一个组件化demo
2、修改最外层的gradle.properties文件下,以便统一管理各个module的SDK等,同时为各个组件添加gradle.properties文件
project的gradle.properties文件
从零开始手写一个组件化demo

各组件的gradle.properties
从零开始手写一个组件化demo
3、改写build.gradle文件

对build.gralde的改写包括根据isRunAlone 的值转换application和library类型、资源前缀resourcePrefix 、组件单独调试和集成调试时动态修改AndroidManifest文件、加入aRouter依赖等
app下build.gradle

apply plugin: 'com.android.application'
   ...
        //aRouter配置
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
   ...
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
    //common被所有组件和主module引用
    implementation project(':common')
    runtimeOnly project(':login')
    runtimeOnly project(':pay')
}

注:implementation和api取代了之前的compile,implementation依赖的库只能本module自己访问,api则是对所有可见。runtimeOnly取代了apk,只在打包的时候有效,编译不参与

Login的build.gradle

if (isRunAlone.toBoolean()) {//切换application和library
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
    ...
    resourcePrefix "login_"//资源前缀 如果资源名称不加该前缀 就会报红
    defaultConfig {
        //单独调试时设置的applicationId
        if (isRunAlone.toBoolean()) {
            applicationId "com.scy.component.login"
        }
       ...
    }
    sourceSets {
        //组件单独调试和集成调试时动态修改AndroidManifest文件
        main {
            if (isRunAlone.toBoolean()) {
                manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
  ...

Pay的build.gralde基本和Login的一样,只是资源前缀和applicationId不同

  ...
  resourcePrefix "pay_"//资源前缀 如果资源名称不加该前缀 就会报红
  defaultConfig {
      //单独调试时设置的applicationId
      if (isRunAlone.toBoolean()) {
          applicationId "com.scy.component.pay"
      }
  ...

Common下的build.gradle

apply plugin: 'com.android.library'
android {
    compileSdkVersion compile_sdk_version.toInteger()
    resourcePrefix "common_"//资源前缀 如果资源名称不加该前缀 就会报红
    ...
    
    //这里使用了api而非implementation  否则其他组件即便引入了Common库,
    //也无法引用constraint-layout和commonbase内的方法等
    api 'com.android.support.constraint:constraint-layout:1.1.3'
    api 'com.alibaba:arouter-api:1.4.1'
    api project(':commonbase')
}

CommonBase下的build.gradle

...
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    //注意这里是双引号 使用api是为了其他组件可以引用v7包
    api "com.android.support:appcompat-v7:$support_version"
}

4、填充代码,完成组件间的数据传递与页面跳转
添加业务代码布局等,这部分随意。下面重点编写组件间数据分享与页面跳转部分。
首先是在commonbase里创建Login组件对外暴露数据的接口方法及其空实现:

/**
 * 登录组件 对外暴露数据的接口方法
 */
public interface LoginDataInterface {
    /**
     * 获取登录状态
     * @return
     */
    boolean isLogin();

    /**
     * 获取登录人姓名
     * @return
     */
    String getUserName();
}

/**
 * LoginDataInterface接口的空实现 防止在其真正的实现类所在组件缺失时 出现崩溃
 */
public class EmptyImplementLogin implements LoginDataInterface {
    @Override
    public boolean isLogin() {
        return false;
    }

    @Override
    public String getUserName() {
        return null;
    }
}

然后创建工厂类,用来注册各个组件对外暴露数据的接口实现:

/**
 * 各个组件可以通过 InterfaceFactory 获取想要调用的其他组件的接口实现
 */
public class InterfaceFactory {

    private LoginDataInterface interfaceImplement;

    private InterfaceFactory() {
    }

    /**
     * 单例模式
     */
    public static InterfaceFactory getInstance() {
        return Inner.serviceFactory;
    }

    private static class Inner {
        private static InterfaceFactory serviceFactory = new InterfaceFactory();
    }

    /**
     * 设置Login组件的 LoginDataInterface 实现类
     */
    public void setLoginInterfaceImplement(LoginDataInterface interfaceImplement) {
        this.interfaceImplement = interfaceImplement;
    }

    /**
     * 返回 注册的接口实现类的实例
     */
    public LoginDataInterface getLoginInterfaceImplement() {
        //如果Login组件缺失或未注册实现类 返回一个空实现
        if (interfaceImplement == null) {
            interfaceImplement = new EmptyImplementLogin();
        }
        return interfaceImplement;
    }
}

接着去Login组件创建其对外暴露数据的接口的实现类:

/**
 * Login 组件对外暴露数据的接口实现类
 * 外界要通过 commonbase 里的 InterfaceFactory 工厂类获取该实现类的前提是
 * 该类需要在App初始化的时候就注册到 InterfaceFactory 里
 */
public class LoginDataShareImplement implements LoginDataInterface {
    @Override
    public boolean isLogin() {
        return !TextUtils.isEmpty(LoginUtil.getUserName());
    }

    @Override
    public String getUserName() {
        return LoginUtil.getUserName();
    }
}

最后就是去其他组件获取Login组件的登录信息,完成相关逻辑:

    /**
     * pay组件
     * 支付 前提是已登录
     * @param view
     */
    public void pay(View view) {
        //通过 InterfaceFactory 获取Login组件的登录信息
        if (InterfaceFactory.getInstance().getLoginInterfaceImplement().isLogin()) {
            ToastUtil.showToast(this, "支付成功");
        } else {
            ToastUtil.showToast(this, "还未登录");
        }
    }

页面跳转可以使用ARouter的 withXxx 方法传递数据,如果页面之间不存跳转,其数据传递就只能使用面向接口编程(即上述方法)、数据持久化、本地广播等方法。

5、组件 Application 的动态配置
前面的代码要想生效,还需要在App初始化的时候完成各个接口的注册工作。
第一,在common组件里定义抽象类 BaseApplication 继承 Application:

public abstract class BaseApplication extends Application {
    /**
     * Application 初始化
     */
    public abstract void init(Application application);

}

第二,在Login组件里创建 Application 并继承 BaseApplication:

//Login组件的Application
public class LoginApplication extends BaseApplication {

    @Override
    public void onCreate() {
        super.onCreate();
        init(this);
    }

    @Override
    public void init(Application application) {
        //将Login对外暴露接口的实现类注册到工厂类InterfaceFactory里
        InterfaceFactory.getInstance().setLoginInterfaceImplement(new LoginDataShareImplement());
    }
}

pay组件如果需要在App初始化的时候运行一些操作,和Login一样也需要创建一个Application

第三,在 CommonBase 中定义 AppConfig 类,将需要初始化的组件的 Application 的完整类名收集到这里,以方便在主module的Application里集中初始化:

public class AppConfig {
    private static final String LoginApp = "com.scy.component.login.LoginApplication";

    public static String[] moduleApps = {
            LoginApp
    };
}

然后在主module里通过反射的方式,初始化:

public class App extends BaseApplication {
    @Override
    public void onCreate() {
        super.onCreate();
        // ARouter配置
        if (isDebug()) {           // These two lines must be written before init, otherwise these configurations will be invalid in the init process
            ARouter.openLog();     // Print log
            ARouter.openDebug();   // Turn on debugging mode (If you are running in InstantRun mode, you must turn on debug mode! Online version needs to be closed, otherwise there is a security risk)
        }
        ARouter.init(this); // As early as possible, it is recommended to initialize in the Application

        init(this);
    }

    private boolean isDebug() {
        return BuildConfig.DEBUG;
    }

    @Override
    public void init(Application application) {
        //通过反射的方式 初始化各个组件的Application
        for (String moduleApp : AppConfig.moduleApps) {
            try {
                Class clazz = Class.forName(moduleApp);
                BaseApplication baseApp = (BaseApplication) clazz.newInstance();
                baseApp.init(this);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
    }
}

记得将App写到对应AndroidManifest里

ARouter相关https://github.com/alibaba/ARouter

5、有关组件AndroidManifest的合并问题
因为组件的集成调试和单独调试的功能需求,其AndroidManifest也需要对应的两份,以Login组件为例,其结构如下:
从零开始手写一个组件化demo
单独调试时没什么,AndroidManifest该有的属性都加里面就行:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.scy.component.login">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/login_app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/login_AppTheme"
        >
        <activity android:name=".LoginActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

集成调试时,业务组件是绝对不能拥有自己的 Application 和 launch 的 Activity的,也不能声明APP名称、图标等属性:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.scy.component.login">

    <application android:theme="@style/common_AppTheme">
        <activity android:name=".LoginActivity" />
    </application>

</manifest>

这里只声明了应用的主题,而且这个主题还是跟app壳工程中的主题是一致的,都引用了common组件中的资源文件。否则会报Manifest merger failed with multiple errors, see logs
从零开始手写一个组件化demo