Android-组件化

组件化和模块化

区别

  • 两者本质思想是一样的,都是为了代码重用和业务解耦。
  • 在划分时,模块化是业务导向,组件化是功能导向。

组件化

指的是单一的功能组件,如支付组件等。每个组件都可以以一个单独的module开发,并可以单独抽出来作为SDK对外发布使用。

模块化

指的是独立的业务模块,如首页模块等。一个模块可能包含多个组件。

组件化架构

一个基础的组件化架构,并且组件之间是相互隔离的。从上向下,分别为应用层,组件层和基础层。

  • 应用层
    相当于具体的业务模块,可按需引用不同的组件,实现业务功能,最后将各个模块统筹输出APP。
  • 组件层
    包含一些简单的功能组件,如支付组件等。
  • 基础层
    包含一些基础库和基础库的封装,比如常用的图片加载,网络请求,数据库存储等。解耦了基础功能和业务功能的耦合。

组件化开发需要解决的问题

几个主要待解决的问题;

  1. 组件开发过程中,要满足单独运行及调式的要求,并且这样可以提高项目的编译速度。
  2. 数据传递,还有组件间方法的相互调用。
  3. 组件间在不相互依赖的情况下,如何实现界面跳转。
  4. 主项目在不直接访问组件中具体类的情况下,如何获取组件中的Fragment的实例,并将之添加到主项目的界面中。
  5. 组件间如何实现集成调试?调式阶段,当依赖多个组件开发时,如何实现只依赖部分组件时可以编译通过(这样也会降低编译时间,提升效率)?
  6. 如何实现组件解耦和代码隔离?也就是当模块依赖组件时,可以动态的增删组件,而模块不会对组件中特定的类进行操作,这样完全的隔离模块对组件中类的使用会使解耦更加彻底,程序也更加健壮。

组件单独调试

动态配置组件的工程类型

在AndroidStudio开发Android项目时,使用的是Android Gradle插件来构建,它提供了三种插件,开发中可通过配置不同的插件来配置不同的工程。(位于 build.gradle 文件中的最上面)

  • App 插件,id: com.android.application
    配置一个Android App工程,项目构建后输出一个APK安装包。
    1
    apply plugin: 'com.android.application'
  • Library 插件,id: com.android.libraay
    配置一个Android Library工程,项目构建后输出一个arr包。
    1
    apply plugin: 'com.android.library'
  • Test 插件,id: com.android.test
    配置一个Android Test工程。

而我们的组件既可以单独调试又可以被其他模块依赖,所以插件id不应该写死。
而是通过在 module 中添加一个 gradle.properties 配置文件,在配置文件中添加一个布尔类型的变量 isRunAlone。
在 build.gradle 中通过 isRunAlone 的值来使用不同的插件从而配置不同的工程类型。
在单独调试和集成调试时直接在gradle.properties中修改 isRunAlone 的值即可(更改完值后,在build.gradle中重新Sync Now下)。

gradle.properties
1
2
# 是否是组件开发
isRunAlone = flase
build.gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 注意这里
// 根据gradle.properties中的isRunAlon值来判断
if(isRunAlone.toBoolean()){
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}

android {
compileSdkVersion 26

defaultConfig {
// 注意这里
// 单独调试时添加applicationId,集成调试时移除。
if(isRunAlone.toBoolean()){
applicationId "com.example.login"
}
minSdkVersion 15
targetSdkVersion 26
versionCode 1
versionName "1.0"

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

}

dependencies {
// 防止编译报错
androidTestCompile('com.android.support:support-annotations:26.1.0') {
force = true
}

implementation fileTree(dir: 'libs', include: ['*.jar'])

implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

动态配置组件的ApplicationId和AndroidManifest文件

除了上述的配置工程类型外,还要根据 isRunAlone 的值来顺便修改其他配置。
一个App是只有一个 ApplicationId 的,也是只有一个启动页的。(当然,也可以配置不同Java源代码,不同的resource资源文件等)
而 ApplicationId 和启动页相关的AndroidManifest文件都是可以在build.gradle文件中进行配置。
可以先在main包下,新建manifest包,包下新建一个AndroidManifest.xml文件,用来做集成调试时的配置;

main/manifest/AndroidManifest.xml 集成调试
1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.login">

<!--main/manifest/AndroidManifest.xml 集成调试-->

<application android:theme="@style/AppTheme">
<!--也就是MainActivity-->
<activity android:name=".LoginActivity"/>
</application>

</manifest>

然后在 build.gradle 中通过判断 isRunAlone 的值,来配置不同的 ApplicationId 和 AndroidManifest.xml 文件的路径;

build.gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 注意这里
if(isRunAlone.toBoolean()){
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}
android {
...
defaultConfig {
// 注意这里
if(isRunAlone.toBoolean()){
// 单独调试时添加 applicationId ,集成调试时移除
applicationId "com.example.login"
}
...
}

// 注意这里
sourceSets {
main {
// 单独调试与集成调试时使用不同的 AndroidManifest.xml 文件
if (isRunAlone.toBoolean()) {
// 单独调试
manifest.srcFile 'src/main/AndroidManifest.xml'
} else {
// 集成调试
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
}
}
}
...
}
...

当 library 切换为 application 时,需要通过指定不同的 AndroidManifest.xml 文件来指定默认启动的 Activity 等配置。在模块的 src/main 目录下创建目录,用于存放单独配置的 AndroidManifest.xml 文件,然后使用 sourceSets 设置使用不同的配置文件:


组件间的数据传递和方法的相互调用

创建ComponentBase模块

由于主项目与组件,组件与组件之间都是不可以直接通过类的相互引用来进行数据传递的,所以我们可以采用‘接口+实现‘的方式。

  • 可以添加一个ComponentBase模块(注意,方便起见;新建Module时,创建组件用的是 Phone & Tablet Module(默认是App工程) ,而创建ComponentBase模块时用的是 Android Library(默认是Library工程))
  • 这个模块可以被所有的组件依赖
  • 其内部定义了组件可以供外部访问自身数据的抽象方法Service
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /**
    * 组件Service接口定义
    * 定义了,此组件向外提供的,数据传递的接口方法
    */
    public interface IAccountService {

    /**
    * 是否已经登陆
    * @return
    */
    boolean isLogin();

    /**
    * 获取登陆用户的AccountId
    * @return
    */
    String getAccountId();

    }
  • 还提供了一个ServiceFactory,每个组件中都要提供一个类实现自己对应的Serviece中的抽象方法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    /**
    * ComponentBase模块的ServiceFactory
    * 接收组件中实现的,接口对象的注册,以及向外提供特定组件的接口实现。
    */

    public class ServiceFactory {

    private IAccountService iAccountService;

    /**
    * private
    * 禁止外部创建ServicceFactory对象
    */
    private ServiceFactory(){

    }

    /**
    * 通过静态内部类的方式,实现SerrviceFactory的单例。
    */
    private static class Inner{
    private static ServiceFactory serviceFactory = new ServiceFactory();
    }

    public static ServiceFactory getInstance(){
    return Inner.serviceFactory;
    }

    /**
    * 接收Login组件实现的Service实例
    * @param iAccountService
    */
    public void setiAccountService(IAccountService iAccountService){
    this.iAccountService = iAccountService;
    }

    /**
    * 返回Login组件的Service实例
    */
    public IAccountService getiAccountService(){
    if (iAccountService == null){
    // 返回空实现
    iAccountService = new EmptyAccountService();
    }
    return iAccountService;
    }

    }


    /**
    * 登陆组件的 Service 类实现
    */
    public class AccountService implements IAccountService{

    @Override
    public boolean isLogin() {
    return AccountUtils.userBean != null;
    }

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

    }
  • 组件加载后,需要创建一个实现类的的对象,然后将实现了Service的类的对象添加到ServiceFactory中。
    1
    2
    // 将AccountService类的实例注册到ServiceFactory
    ServiceFactory.getInstance().setiAccountService(new AccountService());
  • 这样在不同组件交互时就可以通过ServiceFactory获取想要调用的组件的接口实现,然后调用其中的特定方法就可以实现组件间的数据传递和方法的调用。
    1
    2
    3
    4
    5
    if (ServiceFactory.getInstance().getiAccountService().isLogin()){
    Toast.makeText(this,"分享成功",Toast.LENGTH_SHORT).show();
    }else {
    Toast.makeText(this,"分享失败,用户未登录",Toast.LENGTH_SHORT).show();
    }
  • ServiceFactory中也会提供所有的Service的空实现,在组件单独调试或部分集成调试时,避免出现由于实现类对象为空引起的空指针问题。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /**
    * 组件Service空实现
    * service中,定义的,接口的空实现。
    */

    public class EmptyAccountService implements IAccountService {

    @Override
    public boolean isLogin() {
    return false;
    }

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

    }

Login组件在ServiceFactory中注册接口对象

  1. componentbase定义好Login组件需要提供的Service
  2. Login组件需要依赖componentbase模块
  3. Login组件中创建类实现IAccountService接口并实现其中的接口方法
  4. Login组件初始化(最好在Application中)时将IAccountService接口的实现类对象注册到ServiceFactory中。

组件与组件间实现数据传递

  1. 当Login组件将 IAccountService 实现类对象注册到ServiceFactory中后,其他模块就可以使用这个Service与Login组件进行数据传递。
  2. Share组件依赖base模块
  3. Share组件中直接通过ServiceFactory对象的getiAccountService()即可获取到Login组件提供的IAccountService接口的实现类对象
  4. 然后调用该对象的方法,即可实现与Login组件的数据传递。
  5. (除了Service这种交互方式,也可以选择 EventBus,广播,数据持久化等)

组件Application的动态配置

在主模块有Application的情况下,组件在集中调试时,其Application不会初始化。而组件的Service在ServiceFactory的注册,又需要在Application中进行初始化。
可以通过反射来实现组件的Application初始化

  1. 在base模块中定义抽象类BaseApp继承Application,内部定义了两个方法;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public abstract class BaseApp extends Application{

    /**
    * 当前组件初始化
    */
    public abstract void initModuleApp();

    /**
    * 所有组件都初始化完毕后,调用这个方法
    */
    public abstract void initModuleData();

    }
  2. 所有组件的Application都继承BaseApp,并在对应的方法中实现操作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class LoginApp extends BaseApp{

    /**
    * 当主模块有Application的情况下
    * 组件在集成调试时,Application不会初始化
    */
    @Override
    public void onCreate() {
    super.onCreate();
    // 在单独调试时,这样也会调用initModuleApp(),完成在ServiceFactory中的注册操作。
    initModuleApp(this);
    initModuleData(this);
    }

    @Override
    public void initModuleApp(Application application) {
    // 将AccountService类的实例注册到ServiceFactory
    ServiceFactory.getInstance().setiAccountService(new AccountService());
    }

    @Override
    public void initModuleData(Application application) {

    }
    }
  3. base模块中定义AppConfig类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class AppConfig {

    /**
    * 静态常量
    */
    private static final String LOGINAPP = "com.example.login";

    /**
    * 静态的String数组
    * 将需要初始化的,组件的Application,的完整类名放入其中。
    */
    public static String[] moduleApps={LOGINAPP};

    }
  4. 主module的Application也继承BaseApp,并实现两个初始化方法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    public class MyApp extends BaseApp{

    @Override
    public void onCreate() {
    super.onCreate();

    // 初始化组件 Application
    initModuleApp(this);

    // 其他操作

    // 所有 Application 初始化后的操作
    initModuleData(this);

    }

    /**
    * 遍历 AppcConfig 类中定义的 moduleApps 数组中的类名,
    * 通过反射,初始化各个组件的 Application。
    */
    @Override
    public void initModuleApp(Application application) {
    for (String moduleApp : AppConfig.moduleApps) {
    try {
    Class clazz = Class.forName(moduleApp);
    BaseApp baseApp = (BaseApp) clazz.newInstance();
    baseApp.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);
    BaseApp baseApp = (BaseApp) clazz.newInstance();
    baseApp.initModuleData(this);
    } catch (ClassNotFoundException e) {
    e.printStackTrace();
    } catch (IllegalAccessException e) {
    e.printStackTrace();
    } catch (InstantiationException e) {
    e.printStackTrace();
    }
    }
    }
    }

组件间的界面跳转

Android中的界面跳转,分显式和隐式两种。

  • 由于不同组件间没有相互依赖,所有不能直接访问彼此的类,这就没办法通过显式的方式来跳转。
  • 虽然隐式的可以,但需要通过AndroidMainfest集中管理,协作开发比较麻烦。
  • 可以使用Alibaba开源的ARouter

这里需要通过ARouter来实现组件间的路由功能(路由是指从一个接口上收到数据包,根据数据路由包的目的地址进行定向并转发到另一个接口的过程。非常适合组件化解耦。),做界面跳转。
首先,要添加对Arouter的依赖。

  1. base模块中,添加
    build.gradle
    1
    2
    3
    4
    // alibaba的Arouter库(https://github.com/alibaba/ARouter)
    compile 'com.alibaba:arouter-api:1.3.1'
    // arouter-compiler 的注解依赖需要所有使用 ARouter 的 module 都添加依赖
    annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
  2. 所有使用到Arouter的模块或组件都要单独添加
    build.gradle
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    android {
    defaultConfig {
    ...
    // alibaba Arouter
    javaCompileOptions {
    annotationProcessorOptions {
    arguments = [ moduleName : project.getName() ]
    }
    }
    }
    }

    dependencies {
    ...
    // 依赖base模块
    implementation project (':base')
    // alibaba arouter
    annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
    }
  3. 主项目记得要添加对各组件的依赖
    build.gradle
    1
    2
    3
    4
    // 依赖base
    implementation project(':base')
    implementation project(':login')
    implementation project(':share')
  4. 在主项目中对Arouter进行初始化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    /**
    * 主module的Application
    */
    public class MyApp extends BaseApp{

    /**
    * 双数为正式(true),单数为测试debug版本(false)
    */
    private boolean isReleaseVersion;

    @Override
    public void onCreate() {
    isReleaseVersion = (getVersionCode(this) % 2 == 0);
    super.onCreate();

    // 初始化Arouter
    if (!isReleaseVersion){
    // debug版
    // 这两行必须写在init之前,否则这些配置在init过程中将无效

    // 打印日志
    ARouter.openLog();
    // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
    ARouter.openDebug();

    }
    // 初始化 ARouter
    ARouter.init(this);

    // 初始化组件 Application
    initModuleApp(this);

    // 其他操作

    // 所有 Application 初始化后的操作
    initModuleData(this);

    }

    /**
    * 遍历 AppcConfig 类中定义的 moduleApps 数组中的类名,
    * 通过反射,初始化各个组件的 Application。
    */
    @Override
    public void initModuleApp(Application application) {
    for (String moduleApp : AppConfig.moduleApps) {
    try {
    Class clazz = Class.forName(moduleApp);
    BaseApp baseApp = (BaseApp) clazz.newInstance();
    baseApp.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);
    BaseApp baseApp = (BaseApp) clazz.newInstance();
    baseApp.initModuleData(this);
    } catch (ClassNotFoundException e) {
    e.printStackTrace();
    } catch (IllegalAccessException e) {
    e.printStackTrace();
    } catch (InstantiationException e) {
    e.printStackTrace();
    }
    }
    }

    public static int getVersionCode(Context context){
    int versionCode = 0 ;
    PackageManager pm = context.getPackageManager();
    PackageInfo pi;
    try {
    pi = pm.getPackageInfo(context.getPackageName(), 0);
    versionCode = pi.versionCode;
    } catch (PackageManager.NameNotFoundException e) {
    e.printStackTrace();
    }
    return versionCode;
    }
    }
  5. 为组件中的界面,添加注解Route。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    public class MainActivity extends AppCompatActivity {

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

    public void login(View view){
    ARouter.getInstance().build("/login/login").navigation();
    }

    public void share(View view){
    ARouter.getInstance().build("/share/share").withString("share_content","main_share_content").navigation(this, new NavCallback() {
    @Override
    public void onArrival(Postcard postcard) {
    // 路由到达了
    }

    @Override
    public void onInterrupt(Postcard postcard) {
    // 路由被拦截了
    runOnUiThread(new Runnable() {
    @Override
    public void run() {
    Toast.makeText(MainActivity.this,"请先登录",Toast.LENGTH_SHORT).show();
    }
    });
    super.onInterrupt(postcard);
    }
    });
    }
    }



    /**
    * 登陆页
    * 添加注解,注意path跳转路径至少有两级。
    * 并且,注意第一级的名称不要相同,否则会报错
    */
    @Route(path = "/login/login")
    public class LoginActivity extends AppCompatActivity {

    private TextView tvLoginState;

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

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

    /**
    * 登陆
    */
    public void login(View view){
    AccountUtils.userBean = new UserBean("123456","admin");
    updateLoginState();
    }

    private void updateLoginState() {
    tvLoginState.setText("登陆界面:"+(AccountUtils.userBean==null?"未登录":AccountUtils.userBean.getUserName()));
    }

    /**
    * 退出
    */
    public void exit(View view){
    AccountUtils.userBean = null;
    updateLoginState();
    }

    /**
    * 去分享页面
    */
    public void loginShare(View view){
    ARouter.getInstance().build("/share/share").withString("share_content","分享数据").navigation(this, new NavCallback() {
    @Override
    public void onArrival(Postcard postcard) {
    // 路由到达了
    }

    @Override
    public void onInterrupt(Postcard postcard) {
    // 路由被拦截了
    runOnUiThread(new Runnable() {
    @Override
    public void run() {
    Toast.makeText(LoginActivity.this,"请先登录",Toast.LENGTH_SHORT).show();
    }
    });
    super.onInterrupt(postcard);
    }
    });
    }

    }



    /**
    * 分享页
    * 添加注解,注意path跳转路径至少有两级。
    * 并且,注意第一级的名称不要相同,否则会报错
    */
    @Route(path = "/share/share")
    public class ShareActivity extends AppCompatActivity {

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

    if (getIntent()!=null){
    String content = getIntent().getStringExtra("share_content");
    if (!TextUtils.isEmpty(content)){
    ((TextView)findViewById(R.id.tv_share_content)).setText(content);
    }
    }

    share();
    }

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

    public void shareLogin(View view){
    ARouter.getInstance().build("/login/login").navigation();
    }
    }



    /**
    * Login模块中
    * 对登录状态的过滤拦截器
    * 自定义的过滤器需要通过 Interceptor 来注解
    * priority 是优先级
    * name 是对这个拦截器的描述
    * Created by xwxwaa on 2019/5/10.
    */
    @Interceptor(priority = 8,name = "登录状态拦截器")
    public class LoginInterceptor implements IInterceptor {

    private Context context;

    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {
    // onContinue 和 onInterrupt 至少要调用其中一种,否则不会继续路由。
    if ("/share/share".equals(postcard.getPath())){
    if (ServiceFactory.getInstance().getiAccountService().isLogin()){
    // 处理完成,交换控制权
    // 已经登陆,就继续跳转
    callback.onContinue(postcard);
    }else {
    // 中断路由流程
    callback.onInterrupt(null);
    }
    }else {
    // 处理完成,交换控制权
    callback.onContinue(postcard);
    }
    }

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

在不直接访问组件中具体类的情况下使用组件的Fragment

为了实现模块与组件间的解耦,并且在移除组件时不会由于引用的 Fragment 不存在而编译失败,就不能模块中直接访问组件的 Fragment 类。

  1. 通过反射来解决;初始化 Fragment 对象并返回给 Activity,在 Actiivty 中将 Fragment 添加到特定位置即可。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    public class FragmentActivity extends AppCompatActivity{
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_fragment);
    // 1.通过反射来解决
    showFragment(0);
    }

    /**
    * 通过反射来加载Fragment
    * currIndex 上一个fragment的序列号
    * showFragment() 展示Fragment
    */
    int currIndex = -1;
    Fragment[] fragmentList = new Fragment[1];

    public void showFragment(int index) {
    if (currIndex == index)
    return;
    FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
    if (currIndex != -1) {
    ft.detach(fragmentList[currIndex]);
    }
    if (fragmentList[index] != null) {
    ft.attach(fragmentList[index]);
    } else {
    creatFragment(index);
    ft.replace(R.id.layout_fragment, fragmentList[index]);
    }
    // if (currIndex != -1) {
    // tab.getChildAt(currIndex).setSelected(false);
    // }
    // tab.getChildAt(index).setSelected(true);
    currIndex = index;
    ft.commit();
    }
    public String[] str = {"ShareFragment"};

    public void creatFragment(int index) {
    try {
    Fragment fragment = (Fragment) Class.forName("com.example.share." + str[index]).newInstance();
    fragmentList[index] = fragment;
    } catch (InstantiationException e) {
    e.printStackTrace();
    } catch (IllegalAccessException e) {
    e.printStackTrace();
    } catch (ClassNotFoundException e) {
    e.printStackTrace();
    }
    }

    /**
    * 界面跳转
    * @param mContext
    * @param bundle
    */
    public static void startAction(Context mContext,Bundle bundle){
    Intent intent = new Intent(mContext,FragmentActivity.class);
    mContext.startActivity(intent);
    }
    }
  2. 通过 componentbase 模块实现;把 Fragment 的初始化工作放到每一个组件中,模块需要使用组件的 Fragment 时,通过 componentbase 提供的 Service 中的方法来实现 Fragment 的初始化。
  3. 在componentbase中的IAccountService接口中添加方法并返回Fragment
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
     /**
    * 组件Service接口定义
    * 定义了,此组件向外提供的,数据传递的接口方法
    * Created by xwxwaa on 2019/5/9.
    */
    public interface IAccountService {

    /**
    * 添加 newUserFragment() 方法
    * 用来获取 Fragment 实例
    * @param activity 上下文
    * @param containerId FrameLayout 的布局 id
    * @param manager
    * @param bundle
    * @param tag
    * @return
    */
    Fragment newUserFragment(Activity activity, int containerId, FragmentManager manager, Bundle bundle, String tag);
    }
  4. 在组件中的AccountService实现类中和componentbase中IAccountService的空实现类中实现这个方法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
     /**
    * 登陆组件的 Service 类实现
    */
    public class AccountService implements IAccountService{

    @Override
    public Fragment newUserFragment(Activity activity, int containerId, FragmentManager manager, Bundle bundle, String tag) {
    FragmentTransaction transaction = manager.beginTransaction();
    Fragment userFragment = new UserFragment();
    transaction.add(containerId,userFragment,tag);
    transaction.commit();
    return userFragment;
    }
    }


    /**
    * 组件Service空实现
    * service中,定义的,接口的空实现。
    */
    public class EmptyAccountService implements IAccountService {

    @Override
    public Fragment newUserFragment(Activity activity, int containerId, FragmentManager manager, Bundle bundle, String tag) {
    return null;
    }
    }
  5. 在主模块中通过ServiceFactory获取IAccountService的实现类对象,调用其相关方法即可获取到Fragment的实例。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class FragmentActivity extends AppCompatActivity{
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_fragment);

    ServiceFactory.getInstance().getiAccountService().newUserFragment(this,R .id.layout_fragment,getSupportFragmentManager(),null,"");
    }
    }

组件解耦的目标及代码隔离

解耦目的

代码解耦的首要目标就是组件之间的完全隔离,在开发过程中我们要时刻牢记,我们不仅不能直接使用其他组件中的类,最好能根本不了解其中的实现细节。

代码隔离

1
2
3
4
5
6
7
dependencies {

// runtimeOnly,只有在打包过程中才能直接引用组件中的类,在开发阶段,所有组件中的类都是不可以访问的。
runtimeOnly project(':login')
runtimeOnly project(':share')

}

资源隔离

在每个组件的build.gradle中添加 resourcePrefix 配置,以值作为资源前缀。
但 resourcePrefix 只能限定 res 中 xml 文件中定义的资源,并不能限定图片资源,所以在往组件中添加图片资源时要手动限制资源前缀。
在添加配置之后,相应的xml中定义的资源会报红,要更改其资源名称前缀或文件名称前缀。

1
2
3
4
5
// share 组件的 build.gradle
android {
resourcePrefix "share_"
// 其他配置 ...
}
colors.xml
1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="share_colorPrimary">#3F51B5</color>
<color name="share_colorPrimaryDark">#303F9F</color>
<color name="share_colorAccent">#FF4081</color>
</resources>

备注

参考资料;
组件化

源码地址:GitHub

欢迎关注微信公众号:非也缘也