从零开始手写一个组件化demo
根据上篇Android组件化学习文章,编写一个组件化demo,github地址https://github.com/chaoyangsun/MyComponentDemo。项目结构如下:
- APP:主module
- Login:登录组件
- Pay:支付组件,支付前需要判断是否登录
- Common:基础组件,包含基础库、公共页面等
- CommonBase:包含各个组件对外提供访问自身数据的接口、抽象方法等
1、使用AS创建一个工程,同时创建Login、Pay、Common、CommonBase四个Libray
效果如下:
2、修改最外层的gradle.properties文件下,以便统一管理各个module的SDK等,同时为各个组件添加gradle.properties文件
project的gradle.properties文件
各组件的gradle.properties
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,只在打包的时候有效,编译不参与
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"
}
...
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')
}
...
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组件为例,其结构如下:
单独调试时没什么,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
: