Android 组件化项目实践

一、什么是组件化

1、概念

在项目迭代过程中,随着功能和开发人员增多,代码会逐渐臃肿起来,如何设计架构,保证代码质量和后续拓展变得尤为重要。一般的过程是从无架构到模块化,再到组件化或插件化,本质上是分而治之,降低耦合。

模块化:一般按照业务划分不同module,一个业务可能包含多个功能,偏向业务层。

组件化:将一个app按照功能划分不同module,更偏向底层,每个module可集成到app中,也可独立打包和调试。

插件化:是模块化的一种更高级的体现,将将一个apk根据业务功能拆分成不同的子apk (即插件)。每个子apk可以独立编译打包,而上线的一般为集成后的apk。在apk使用时,每个插件是动态加载的,插件也可以进行热修复和热更新。

三者有些区别,注意区分。

现在优秀的代码架构都会使用组件化或者插件化,本章节将对组件化做深入实践,插件化后续待实践。

二、组件化架构设计

一个科学的合理的的架构应该是有层次的。

Android 组件化项目实践

架构一般从上而下可分为应用层、组件层和基础层 。

  • 应用层:app主工程,可以按需要引用过不同的业务模块或组件功能,最后编译生成apk。
  • 组件层:将app功能划分为多个组件,例如登录,分享,支付等等。
  • 基础层:对一些基础库,无关业务的模块封装,例如网络请求,基础控件,数据存储等。同时为便于底层库的管理和引用,可以用一个总的base依赖多个基础库,基础层上层依赖时只需依赖base.

三、组件化需要解决的问题

按照组件化定义,首先要解决的问题就是

1、单独调试的问题

在项目开发中,以下场景也是需要满足的:

2、部分组件间会存在一些通信,包括数据的传递和方法的调用

例如分享或者支付甚至很多组件都需要判断用户是否登录,这就需要登录组件和其他组件间一些通信。

3、组件间的界面跳转。不同组件之间除了有数据的传递,也会有相互的页面跳转

例如使用当前组件时未登录需要先去登录。

4、组件开发中不仅要实现代码的隔离,还要实现资源文件的隔离

接下来,我们问题一个个来解决。

四、组件单独调试

在Android开发中,Android Gradle 中提供了三种插件:application,library,test来配置不同的工程。所以组件单独调试就变得简单,集成调试时,我们设为library,单独调试时,我们设为application。需要注意的是,组件在设为application单独调试时需要一个启动页,所以要有一份新的AndroidManifest文件。

因此为实现单独调试,我们需要:

1、设置一个变量isRunAlone,标记当前是否需要单独调试。

2、创建一份AndroidManifest文件,根据isRunAlone的取值取用不同的AndroidManifest文件。

问题1,在组件module 中添加一个 gradle.properties 配置文件 ,添加一个布尔类型的变量 isRunAlone 。

//组件module的gradle.properties文件.
isRunAlone=false

在 build.gradle 中通过 isRunAlone 的值来使用不同的插件从而配置不同的工程类型,在单独调试和集成调试时直接修改 isRunAlone 的值即可。

//组件module的build.gradle文件
if (isRunAlone.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
android{
    ......
}

问题2,在main下创建一个文件夹放置单独调试时的AndroidMainfest文件
Android 组件化项目实践
然后在组件的build.gradle文件的android中添加如下代码:

defaultConfig {
        if (isRunAlone.toBoolean()) {
            // 单独调试时添加 applicationId ,集成调试时移除
            applicationId "com.kwmax.login"
        }
	    ......
}
sourceSets {
        main {
            // 单独调试与集成调试时使用不同的 AndroidManifest.xml 文件
            if (isRunAlone.toBoolean()) {
                manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
}

五、组件间数据传递和方法调用

由于主项目与组件,组件与组件之间都是不可以直接使用类的相互引用来进行数据传递的,那如何解决组件间通信呢?这里我们可以采用 [接口 + 实现] 的方式来解决。

1、创建componentbase模块

如上面组件架构图所示,可以在基础层创建componentbase模块。在模块中,定义Service 和一个ServiceFactory。 Service 对外提供访问自身数据的接口方法,ServiceFactory接收组件中实现的接口对象的注册以及向外提供特定组件的接口实现 。

这样在不同组件交互时就可以通过 ServiceFactory 获取想要调用的组件的接口实现,然后调用其中的特定方法就可以实现组件间的数据传递与方法调用。

例如,分享组件调用登录组件中的方法来获取登录状态。我们可以在login组件中添加:

//IAccountService.java
public interface IAccountService {

    boolean isLogin();

    String getAccountId();
}

//ServiceFactory.java
public class ServiceFactory {

    private IAccountService accountService;

    private ServiceFactory(){
    }

    private static class Inner {
        private static ServiceFactory serviceFactory = new ServiceFactory();
    }
    
    public static ServiceFactory getInstance() {
        return Inner.serviceFactory;
    }

    
    public IAccountService getAccountService() {
        if (accountService == null){
            accountService = new emptyAccountService();
        }
        return accountService;
    }

    public void setAccountService(IAccountService accountService) {
        this.accountService = accountService;
    }
}

为避免调试时出现由于实现类对象为空引起的空指针异常,我们需要一个空实现类emptyService。

//emptyAccountService.java
public class emptyAccountService implements IAccountService {
    @Override
    public boolean isLogin() {
        return false;
    }

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

2、base层依赖

创建 base 模块,所有组件依赖base,而base依赖componentbase。

//base 的build.gradle
......
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
   
    api project(':componentbase')
}

3、组件 Application 的动态配置

一个项目时只能有一个 Application。当组件集成调试时只有主模块中的Application会初始化,而组件中的 Applicaiton 不会初始化 ,而组件的 Service 在 ServiceFactory 的注册又必须放到组件初始化的地方。

解决方法:

1)在 base 模块中定义抽象类 BasicApplication 继承 Application,定义两个方法:initModeApp 是初始化当前组件时需要调用的方法,initModuleData 是所有组件的都初始化后再调用的方法

// base 模块中定义
public abstract class BasicApplication extends Application {
    /**
     * Application 初始化
     */
    public abstract void initModuleApp(Application application);

    /**
     * 所有 Application 初始化后的自定义操作
     */
    public abstract void initModuleData(Application application);
}

2)所有的组件的 Application 都继承 BasicApplication,并在对应的方法中实现操作

// Login组件的 LoginApp
public class LoginApp extends BasicApplication {

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

    @Override
    public void initModuleApp(Application application) {
        ServiceFactory.getInstance().setAccountService(new AccountService());
    }

    @Override
    public void initModuleData(Application application) {

    }
}

3)在 base 模块中定义 AppConfig 类,其中的 moduleApps 是一个静态的 String 数组,我们将需要初始化的组件的 Application 的完整类名放入到这个数组中。

// base 模块的 AppConfig
public class AppConfig {
    private static final String LoginApp = "com.kwmax.login.LoginApp";

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

4)主 module 的 Application 也继承 BasicApplication ,并实现两个初始化方法,在这两个初始化方法中遍历 AppcConfig 类中定义的 moduleApps 数组中的类名,通过反射,初始化各个组件的 Application。

// 主 Module 的 Applicaiton
public class MainApplication extends BasicApplication  {
    @Override
    public void onCreate() {
        super.onCreate();
        
        // 初始化组件 Application
        initModuleApp(this);
        // 所有 Application 初始化后的操作
        initModuleData(this);
    }

    @Override
    public void initModuleApp(Application application) {
        for (String moduleApp : AppConfig.moduleApps) {
            try {
                Class clazz = Class.forName(moduleApp);
                BasicApplication basicApp = (BasicApplication) clazz.newInstance();
                basicApp.initModuleApp(this);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void initModuleData(Application application) {
        for (String moduleApp : AppConfig.moduleApps) {
            try {
                Class clazz = Class.forName(moduleApp);
                BasicApplication basicApp = (BasicApplication) clazz.newInstance();
                basicApp.initModuleData(this);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
    }
}

这样就通过反射,完成了组件 Application 的初始化操作,也实现了组件与化中的解耦需求。

4、组件在 ServiceFactory 中注册接口对象

组件依赖 componentbase 模块,创建类实现 Service 接口并实现其中的接口方法,并在组件初始化时将 Service 接口的实现类对象注册到 ServiceFactory 中。

还是以登录组件为例:

// login 组件的 build.gradle
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project (':base')
}

// login 组件中的 IAccountService 实现类
public class AccountService implements IAccountService {
    @Override
    public boolean isLogin() {
        return AccountUtils.userInfo != null;
    }

    @Override
    public String getAccountId() {
        return AccountUtils.userInfo == null ? null : AccountUtils.userInfo.getAccountId();
    }
}

// login 组件中的 Aplication 类
public class LoginApp extends BasicApplication {

    @Override
    public void onCreate() {
        super.onCreate();
        // 将 AccountService 类的实例注册到 ServiceFactory
        ServiceFactory.getInstance().setAccountService(new AccountService());
    }
}

5、组件间实现数据传递

在 调用组件 中直接通过 ServiceFactory 对象的 getService方法,即可获取到 被调用组件 提供的 Service接口的实现类对象,然后通过调用该对象的方法即可实现与 被调用组件 组件的数据传递。

例如,分享组件查询登录组件的登录状态:

// Share 组件的 ShareActivity
public class ShareActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_share);

        share();
    }

    private void share() {
        if(ServiceFactory.getInstance().getAccountService().isLogin()) {
            Toast.makeText(this, "分享成功", Toast.LENGTH_SHORT);
        } else {
            Toast.makeText(this, "分享失败:用户未登录", Toast.LENGTH_SHORT);
        }
    }
}

这样就实现了各个组件间的数据传递基于接口编程,接口和实现完全分离,从而组件间解耦。

六、组件间跳转

组件间的跳转我们需要借助一个组件间路由框架ARouter完成。路由是指从一个接口上收到数据包,根据数据路由包的目的地址进行定向并转发到另一个接口的过程。

1、添加ARouter依赖

// base 模块的 build.gradle
dependencies {
    api 'com.alibaba:arouter-api:1.3.1'
    // arouter-compiler 的注解依赖需要所有使用 ARouter 的 module 都添加依赖
    annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
}

需要注意的是,arouter-compiler 的依赖需要所有使用到 ARouter 的模块和组件中都单独添加,不然无法在 apt 中生成索引文件,也就无法跳转成功。并且在每一个使用到 ARouter 的模块和组件的 build.gradle 文件中,其 android{} 中的 javaCompileOptions 中也需要添加特定配置。

// 所有使用到 ARouter 的组件和模块的 build.gradle
android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [ moduleName : project.getName() ]
            }
        }
    }
}

dependencies {
    ...
    implementation project (':base')
    annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
}

2、ARouter初始化

项目的 Application 中将 ARouter 初始化 。

// 主项目的 Application
public class MainApplication extends BasicApplication {
    @Override
    public void onCreate() {
        super.onCreate();

        // 初始化 ARouter
        if (isDebug()) {           
            // 这两行必须写在init之前,否则这些配置在init过程中将无效
            
            // 打印日志
            ARouter.openLog();     
            // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
            ARouter.openDebug();   
        }
        
        // 初始化 ARouter
        ARouter.init(this);
        
        // 其他操作 ...
    }

    private boolean isDebug() {
        return BuildConfig.DEBUG;
    }
    
    // 其他代码 ...
}

3、添加路径注解 Route

注解:@Route(path = “/account/login”) ,其中path 是跳转的路径,至少需要有两级:/xx/xx

跳转方法:ARouter.getInstance().build(path).withString(key, value).navigation();

以主项目跳登录界面,登录成功后跳分享界面为例。

// 主项目的 MainActivity
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    /**
     * 跳登录界面
     * @param view
     */
    public void login(View view){
        ARouter.getInstance().build("/account/login").navigation();
    }

    /**
     * 跳分享界面
     * @param view
     */
    public void share(View view){
        ARouter.getInstance().build("/share/share").withString("share_content", "分享数据到微博").navigation();
    }
}

@Route(path = "/account/login")
public class LoginActivity extends AppCompatActivity {

    private TextView tvState;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        initView();
        updateLoginState();
    }

    private void initView() {
        tvState = (TextView) findViewById(R.id.tv_login_state);
    }

    public void login(View view) {
        AccountUtils.userInfo = new UserInfo("10086", "Admin");
        updateLoginState();
    }

    private void updateLoginState() {
        tvState.setText("这里是登录界面:" + (AccountUtils.userInfo == null ? "未登录" : AccountUtils.userInfo.getUserName()));
    }

    public void exit(View view) {
        AccountUtils.userInfo = null;
        updateLoginState();
    }

    public void loginShare(View view) {
        ARouter.getInstance().build("/share/share").withString("share_content", "分享数据到微博").navigation();
    }
}

@Route(path = "/share/share")
public class ShareActivity extends AppCompatActivity {
    private TextView tvState;
    private Button btnLogin, btnExit;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        initView();
        updateLoginState();
    }

    private void initView() {
        tvState = (TextView) findViewById(R.id.tv_login_state);
    }

    public void login(View view) {
        AccountUtils.userInfo = new UserInfo("10086", "Admin");
        updateLoginState();
    }

    public void exit(View view) {
        AccountUtils.userInfo = null;
        updateLoginState();
    }

    public void loginShare(View view) {
        ARouter.getInstance().build("/share/share").withString("share_content", "分享数据到微博").navigation();
    }
    
    private void updateLoginState() {
        tvState.setText("这里是登录界面:" + (AccountUtils.userInfo == null ? "未登录" : AccountUtils.userInfo.getUserName()));
    }
}

ARouter拥有自身的编译时注解框架,其跳转功能是通过编译时生成的辅助类完成的,最终的实现实际上还是调用了 startActivity。

路由的另外一个重要作用就是过滤拦截,以 ARouter 为例,如果我们定义了过滤器,在模块跳转前会遍历所有的过滤器,然后通过判断跳转路径来找到需要拦截的跳转,比如上面我们提到的分享功能一般都是需要用户登录的,如果我们不想在所有分享的地方都添加登录状态的判断,我们就可以使用路由的过滤功能,我们就以这个功能来演示,我们可以定义一个简单的过滤器:

// Login 模块中的登录状态过滤拦截器
@Interceptor(priority = 8, name = "登录状态拦截器")
public class LoginInterceptor implements IInterceptor {

    private Context context;

    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {

        // onContinue 和 onInterrupt 至少需要调用其中一种,否则不会继续路由
        
        if (postcard.getPath().equals("/share/share")) {
            if (ServiceFactory.getInstance().getAccountService().isLogin()) {
                callback.onContinue(postcard);  // 处理完成,交还控制权
            } else {
                callback.onInterrupt(new RuntimeException("请登录")); // 中断路由流程
            }
        } else {
            callback.onContinue(postcard);  // 处理完成,交还控制权
        }

    }

    @Override
    public void init(Context context) {
        // 拦截器的初始化,会在sdk初始化的时候调用该方法,仅会调用一次
        this.context = context;
    }
}

自定义的过滤器需要通过 @Tnterceptor 来注解,priority 是优先级,name 是对这个拦截器的描述。以上代码中通过 Postcard 获取跳转的 path,然后通过 path 以及特定的需求来判断是否拦截,在这里是通过对登录状态的判断进行拦截,如果已经登录就继续跳转,如果未登录就拦截跳转。

七、资源文件的隔离

在每个组件的 build.gradle 中添加 resourcePrefix 配置来固定这个组件中的资源前缀。不过 resourcePrefix 配置只能限定 res 中 xml 文件中定义的资源,并不能限定图片资源,所以我们在往组件中添加图片资源时要手动限制资源前缀。并将多个组件中都会用到的资源放入 Base 模块中。这样我们就可以在最大限度上实现组件间资源的隔离。

如果组件配置了 resourcePrefix ,其 xml 中定义的资源没有以 resourcePrefix 的值作为前缀,在对应的 xml 中定义的资源会报红。

// Login 组件的 build.gradle
android {
    resourcePrefix "login_"
    // 其他配置 ...
}

Android 组件化项目实践
只有修改前缀报红才消失,恢复正常:
Android 组件化项目实践
以上部分内容参考自:https://juejin.im/post/5b5f17976fb9a04fa775658d,特此感谢!