Android 组件化的概念大概从两年前开始有人讨论,到目前为止,技术已经慢慢沉淀下来,愈来愈多团队开源了本身组件化框架。本人所在团队从去年开始调研组件化框架,在了解社区众多组件化方案以后,决定自研组件化方案。为何明明已经有不少轮子能够用了,却仍是决定要本身造个新轮子呢?java
主要的缘由是在调研了诸多组件化方案以后,发现尽管它们都有各自的优势,可是依然有一些地方不是使人十分满意。而其中最重要的一个因素就是引入组件化方案成本较高,对已有项目改造过大。我想这一点应该不少人都有相同的体会,不少时候 咱们对于项目的重构是须要与新需求的迭代同步进行的 ,几乎很难停下来只作项目的组件化。android
另一点,我不太但愿本身的项目和某一款组件化框架 强耦合。 Activity 的路由方案也好,跨模块的同步或异步方法调用也好,我但愿可以沿用项目已有的调用方式,而不是使用某款组件化框架本身特定的调用方式。例如某个接口已经基于 RxJava 封装为了 Observable
的接口,我就不太但愿由于组件化的关系,这个接口位于另外一个模块以后,我就不得不用这个组件化框架定义的方式去调用,我仍是但愿以 RxJava 的方式去调用。git
我认为目前想要进行组件化的项目应该能够分为两类:github
不管是哪一种类型的项目,面临的问题应该都是相似的,那就是项目大起来之后,编译实在是太慢了。数据库
除此之外,就是 跨模块的功能调用很是不便 ,这个问题主要体如今上面列举的第二种类型的项目。本人所在的项目在组件化以前就是上面列举的第二种类型的项目,application 模块最先用来承载业务逻辑代码,随着业务发展,大概是某位开发人员以为, “不行,这样下去 application 模块代码数量会失控的”,因而后续新的业务模块都会新开一个 library 模块进行开发,就这样断断续续目前有了大概 20+ 个 library 模块(业务相关模块,技术模块不包含在内)。api
这种作法是符合软件工程思想的,可是也带来了一些棘手的问题,因为 application 模块里的业务功能和 library 模块里的业务功能在逻辑地位上是平等的,因此不免会有互相调用的状况,可是它们在项目依赖层次上却不是处于相等的地位,application 调用 library 倒没事,可是反过来调用就成了问题。另外,剩下这 20 + 个 library 模块在依赖层次中也不全是属于同一层次的,library 模块之间互相依赖也很复杂。数组
因此我指望的组件化方案要求解决的问题很简单:浏览器
基于上述的思想,咱们开发了 AppJoint 这个框架用来帮助咱们实现组件化。安全
AppJoint 是一个很是简单有效的方案,引入 AppJoint 进行组件化全部的 API 只包含 3 个注解,加 1 个方法,这多是目前最简单的组件化方案了,咱们的框架不追求功能要多么复杂强大,只专一于框架自己实用、简单与高效。并且总体实现也很是简单,核心源码 不到500行。bash
本人接触最先的组件化方案是 DDComponentForAndroid,学习这个方案给了我不少启发,在这个方案中,做者提出,能够在 gradle.properties
中新增一个变量 isRunAlone=true
,用来控制某个业务模块是 以 library 模块集成到 App 的全量编译中 仍是 以 application 模块独立编译启动 。不知道是否是不少人也受了相同的启发,后面不少的组件化框架都是使用相似的方案:
if (isRunAlone.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
复制代码
根据我本人的实践,这种方式有一些缺点。首先有一些开源框架在 library 模块中和在 application 模块中使用方法是不同的,例如 ButterKinfe , 在 application 中使用 R.id.xxx
,在 library 模块中使用 R2.id.xxx
,若是想组件化,代码必须保证在两种状况下均可用,因此基本只能抛弃 ButterKnife 了,这会给项目带来巨大的改形成本。
除此之外,还有一些开源框架是只能在 application 模块中配置的,配置完之后对整个项目的全部 library 模块都生效的,例如一些字节码修改的框架(好比 AOP 一类的),这是一种状况。还有一种状况,若是原先项目已是多模块的状况下,可能多个模块的初始化都是放在 application 模块里,由于 application 模块是 上帝模块,他能够访问到项目中任意一块代码,因此在这里作初始化是最省事的。可是如今拆分为模块以后,由于每一个模块须要独立运行,因此模块须要负责自身的初始化,但是有时候这个模块的初始化是只能在 application 模块里才能够作的,咱们把这段逻辑下放到 library 以后,如何初始化就成了问题。
这两种状况,若是咱们使用 gradle.properties
中的变量来切换 application 和 library 的话,咱们势必须要在这个模块中维护两套逻辑,一套是在 application 模式下的启动逻辑,一套是在 library 模式下的启动逻辑。原先这个模块是专一本身自己的业务逻辑的,如今不得不为了可以独立做为 application 启动,而加入许多其余代码。一方面 build.gradle
文件中会充满不少 if - else
,另外一方面 Java 源码中也会加入许多判断是否独立运行的逻辑。
最终 Release App 打包时,这些模块是做为 library 存在的,可是咱们为了组件化已经在这个模块中加入了不少帮助该模块独立运行(以 application 模式)的代码(例如模块须要单独运行,须要一个属于这个模块的 Laucher Activity),虽然这些代码在线上不会生效,但是从洁癖的角度来说,这些代码其实不该该被打包进去。其实说了这么多无非就是想说明,若是咱们但愿经过某个变量来控制模块以 application 形式仍是以 library 形式存在,那么咱们确定要在这个模块中加入维护二者的差别的代码,并且可能代码量还很多,最后代码呈现的状态多是不太优雅的。
此外模块中的 AndroidManifest.xml
也须要维护两份:
if (isRunAlone.toBoolean()) {
manifest.srcFile 'src/main/runalone/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
复制代码
可是 xml 毕竟不是代码,没有封装继承这些面向对象的特性,因此每当咱们增长、修改、删除四大组件的时候,都须要记得要在两个 AndroidManifest.xml
都作对应的修改。除了 AndroidManifest.xml
之外,资源文件也存在这个问题,虽然工做量不至于特别巨大,但这样的作法其实已经违背了面向对象的设计原则。
最后还有一个问题,每当模块在 application 模式和 library 模式之间进行切换的时候,都须要从新 Gradle Sync 一次,我想既然是须要组件化的项目那确定已是那种编译速度极慢的项目了,即便是 Gradle Sync 也须要等待很多时间,这点也是咱们不太能接收的。
咱们最后是如何解决模块的单独编译运行这个问题的呢?答案是 为每一个模块新建一个对应的 application 模块 。也许你会对此表示怀疑:若是为每一个业务模块配一个用于独立启动的 application 模块,那模块会显得特别多,项目看起来会很是的乱的。可是其实咱们能够把全部用于独立启动业务模块的 application 模块收录到一个目录中:
projectRoot
+--app
+--module1
+--module2
+--standalone
| +--module1Standalone
| +--module2Standalone
复制代码
在上面这个项目结构图中,app
模块是全量编译的 application 模块入口,module1
和 module2
是两个业务 library 模块, module1Standalone
和 module2Standalone
是分别使用来独立启动 module1
和 module2
的 2 个 application 模块,这两个模块都被收录在 standalone
文件夹下面。事实上,standalone
目录下的模块不多须要修改,因此这个目录大多数状况下是属于折叠状态,不会影响整个项目结构的美观。
这样一来,在项目根目录下的 settings.gradle
里的代码是这样的:
// main app
include ':app'
// library modules
include ':module1'
include ':module2'
// for standalone modules
include ':standalone:module1Standalone'
include ':standalone:module2Standalone'
复制代码
在主 App 模块(app
模块)的 build.gradle
文件里,咱们只须要依赖 module1
和 module2
,两个 standalone 模块只和各自对应的业务模块的独立启动有关,它们不须要被 app
模块依赖,因此 app
模块的 build.gradle
中的依赖部分代码以下:
dependencies {
implementation project(':module1')
implementation project(':module1')
}
复制代码
那些用于独立运行的 application 模块里的 build.gradle
文件中,就只有一个依赖,那就是须要被独立运行的 library 模块。以 standalone/module1Standalone
为例,它对应的 build.gradle
中的依赖为:
dependencies {
implementation project(':module1')
}
复制代码
在 Android Studio 中建立模块,默认模块是位于项目根目录之下的,若是但愿把模块移动到某个文件夹下面,须要对模块右键,选择 "Refactor -- Move" 移动到指定目录之下。
当咱们建立好这些 application 模块以后,在 Android Studio 的运行小三角按钮旁边,就能够选择咱们须要运行哪一个模块了:
这样一来,咱们首先能够感觉到的一点就是模块再也不须要改 gradle.properties 文件切换 library 和 application 状态了,也再也不须要忍受 Gradle Sync 浪费宝贵的开发时间,想全量编译就全量编译,想单独启动就单独启动。
因为专门用于单独启动的 standalone 模块 的存在,业务的 library 模块只须要按本身是 library 模块这一种状况开发便可,不须要考虑本身会变成 application 模块,因此不管是新开发一个业务模块仍是从一个老的业务模块改形成组件化形式的模块,所要作的工做都会比以前更轻松。而以前提到的,为了让业务模块单独启动所须要的配置、初始化工做均可以放到 standalone 模块 里,而且不用担忧这些代码被打包到最终 Release 的 App 中,前面例子中提到的用来使模块单独启动的 Launcher Activity,只要把它放到 standalone 模块 模块便可。
AndroidManifest.xml
和资源文件的维护也变轻松了。四大组件的增删改只须要在业务的 library 模块修改便可,不须要维护两份 AndroidManifest.xml
了,standalone 模块 里的 AndroidManifest.xml
只须要包含模块独立启动时和 library 模块中的 AndroidManifest.xml
不一样的地方便可(例如 Launcher Activity 、图标等),编译工具会自动完成两个文件的 merge。
推荐在 standalone 模块 内指定一个不一样于主 App 的 applicationId,即模块单独启动的 App 与主 App 能够在手机内共存。
咱们分析一下这个方案,和原先的比,首先缺点是,引入了不少新的 standalone 模块,项目彷佛变复杂了。可是优势也是明显的,组件化的逻辑更加清晰,尤为是在老项目改造状况下,所须要付出的工做量更少,并且不须要在开发期间频繁 Gradle Sync。 总的来讲,改造后的组件化项目更符合软件工程的设计原则,尤为是开闭原则(open for extension, but closed for modification)。
介绍到这里为止,咱们尚未使用任何 AppJoint 的 API,咱们之因此没有借助任何组件化框架的 API 来实现模块的独立启动,是由于本文一开始提出的,咱们不但愿项目和任何组件化框架强绑定, 包括 AppJoint 框架自己,AppJoint 框架自己的设计是与项目松耦合的,因此使用了 AppJoint 框架进行组件化的项目,若是从此但愿能够切换到其它更优秀的组件化方案,理论上是很轻松的。
在组件化以前,咱们经常把项目中须要在启动时完成的初始化行为,放在自定义的 Application
中,根据本人的项目经验,初始化行为能够分为如下两类:
咱们在上一步中,为每一个业务模块创建了独立运行的 standalone 模块 ,可是此时还并不能把业务模块独立启动起来,由于模块的初始化工做并无完成。咱们在前面介绍 AppJoint 的设计思想的时候,曾经说过咱们但愿组件化方案最好 『不要有太多的学习成本,沿用目前已有的开发方式』,因此这里咱们的解决方案是,在每一个业务模块里新建一个自定义的 Application
类,用来实现该业务模块的初始化逻辑,这里以在 module1
中新建自定义 Application
为例:
@ModuleSpec
public class Module1Application extends Application {
@Override
public void onCreate() {
super.onCreate();
// do module1 initialization
Log.i("module1", "module1 init is called");
}
}
复制代码
如上面的代码所示,咱们在 module1
中新建一个自定义的 Application
类,名为 Module1Application
。那咱们是否是应该把与这个模块有关的全部初始化逻辑都放在这个类里面呢?并不彻底是这样。
首先,对于前面提到的当前模块的 业务相关的初始化 ,毫无疑问应该放在这个 Module1Application
类中,可是针对前面提到的该模块的 与业务无关的技术组件的初始化 放在这里就不是很合适了。
首先,从逻辑上考虑,业务无关的技术组件的初始化应该放在一个统一的地方,把它们放在主 App 的自定义 Application
类中比较合适,若是每一个模块为了本身能够独立编译运行,都要本身初始化一遍,那么全部代码最后一块儿全量编译的时候,这些初始化行为就会在代码中出现好几回,这样既不合理,也可能会形成潜在问题。
那么,若是咱们在 Module1Application
中作判断,若是它自身处于独立编译运行状态,就执行技术组件的初始化,反之,若它处于全量编译运行状态中,就不执行技术组件的初始化,由主 App 的 Application
来实现这些逻辑,这样是否能够呢?理论上这种方案可行,可是这么作就会遇到和前面提到的 『在 gradle.properties
中维护一个变量来控制模块是否独立编译』一样的问题,咱们不但愿把和业务无关的逻辑(用于业务模块独立启动的逻辑)打包进最终 Release 的 App。
那应该如何解决这个问题呢?解决方案和前面一小节相似,咱们不是为 module1
模块准备了一个 module1Standalone
模块吗?既然技术相关的组件的初始化并非 module1
模块的核心,只和 module1
模块的独立启动有关,那么放在 module1Standalone
模块里是最合适的,由于这个模块只会在 module1
的独立编译运行中使用到,它的任何代码都不会被打包到最终 Release 的 App 中。咱们能够在 module1Standalone
中定义一个 Module1StandaloneApplication
类,它从 Module1Application
继承下来:
public class Module1StandaloneApplication extends Module1Application {
@Override
public void onCreate() {
// module1 init inside super.onCreate()
super.onCreate();
// initialization only used for running module1 standalone
Log.i("module1Standalone", "module1Standalone init is called");
}
}
复制代码
而且咱们在 module1Standalone
模块的 AndroidManifest.xml
中把 Module1StandaloneApplication
设置为 Standalone App 使用的自定义 Application
类:
<application android:icon="@mipmap/module1_launcher" android:label="@string/module1_app_name" android:theme="@style/AppTheme" android:name=".Module1StandaloneApplication">
<activity android:name=".Module1MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
复制代码
在上面的代码中,咱们除了设置了自定义的
Application
之外,还设置了一个 Launcher Activity (Module1MainActivity
),这个Activity
即为模块的启动Activity
,因为它只存在于模块的独立编译运行期间,App 全量打包时是不包含这个Module1MainActivity
的,因此咱们能够在里面定义一些方便模块独立调试的功能,例如快速前往某个页面以及建立 Mock 数据。
这样,只要咱们单独运行 module1Standalone
这个模块的时候,使用的 Application
类就是 Module1StandaloneApplication
。在开发时,咱们须要单独调试 module1
时,咱们只须要启动 module1Standalone
这个模块进行调试便可;而在 App 须要全量编译时,咱们则正常启动原来的主 App 。不管是哪一种状况, module1
这个模块始终是以 library
形式存在的,这意味着,若是咱们但愿把原先的业务模块改形成组件化模块,须要的改造量缩小不少,咱们改造的过程主要是在 增长代码,而不是 修改代码,这点符合软件工程中的『开闭原则』。
写到这里,咱们其实还有一个问题没有解决。Module1Application
目前除了被 Module1StandaloneApplication
继承之外,没有被任何其它地方引用到。您可能会有疑问:那咱们如何保证 App 全量编译运行时,Module1Application
里的初始化逻辑会被调用到呢?细心的您可能早就已经发现了:咱们在上面定义 Module1Application
时,同时标记了一个注解 @ModuleSpec
:
@ModuleSpec
public class Module1Application extends Application {
...
}
复制代码
这个注解的做用是告知 AppJoint 框架,咱们须要确保当前模块该 Application
中的初始化行为,可以在最终全量编译时,被主 App 的 Application
类调用到。因此对应的,咱们的主 App 模块(app
模块)的自定义 Application
类也须要被一个注解 -- AppSpec
标记,代码以下所示:
@AppSpec
public class App extends Application {
...
}
复制代码
上面代码中的 App
为主 App 对应的自定义 Application
类,咱们给这个类上方标记了 @AppSpec
注解,这样系统在执行 App
自身初始化的同时会一并执行这些子模块的 Application
里对应声明周期的初始化。即:
App
执行 onCreate
方法时,保证也同时执行 Module1Application
和 Module2Application
的 onCreate
方法 。App
执行 attachBaseContext
方法时,保证也同时执行 Module1Application
和 Module2Application
的 attachBaseContext
方法。App
执行某个生命周期方法时,保证子模块的 Application
的对应的生命周期方法也会被执行。这样,咱们经过 AppJoint 的 @ModuleSpec
和 @AppSpec
两个注解,在主 App 的 Application
和子模块的 Application
之间创建了联系,保证了在全量编译运行时,全部业务模块的初始化行为都能被保证执行。
到这里为止,咱们已经处理好了业务模块在 独立编译运行模式 和 全量编译运行模式 这两种状况下的初始化问题,目前关于 Application
还有一个潜在问题,咱们的项目在组件化以前,咱们常常会在 Applictaion
类的 onCreate
周期保存当前 Appliction
的引用,而后在应用的任何地方均可以使用这个 Application
对象,例以下面这样:
public class App extends Application {
public static App INSTANCE;
@Override
public void onCreate() {
super.onCreate();
INSTANCE = this;
}
}
复制代码
这么处理以后,咱们能够在项目任意位置经过 App.INSTANCE
使用 Application Context 对象。可是,如今组件化改造之后,以 module1
为例,在独立运行模式时,应用的 Application
对象是 Module1StandaloneApplication
的实例,而在全量编译运行模式时,应用的 Application
对象是主 App 模块的 App
的实例,咱们如何能像以前同样,作到在项目中任何一个地方都能获取到当前使用的 Application
实例呢?
咱们能够把项目中全部自定义 Application
内部保存的自身的 Application
实例的类型,从具体的自定义类,改成标准的 Application
类型,以 Module1Application
为例:
@ModuleSpec
public class Module1Application extends Application {
public static Application INSTANCE;
@Override
public void onCreate() {
super.onCreate();
INSTANCE = (Application)getApplicationContext()
// do module1 initialization
Log.i("module1", "module1 init is called");
}
}
复制代码
咱们能够看到,若是按原来的写法, INSTANCE
的类型通常是具体的自定义类型 Module1Application
,如今咱们改为了 Application
。同时 onCreate
方法里为 INSTANCE
赋值的语句再也不是 INSTANCE = this
,而是 INSTANCE = (Application)getApplicationContext()
。这样处理之后,就能够保证 module1
里面的代码,不管是在 App 全量编译模式下,仍是独立编译调试模式下,均可以经过 Module1Application.INSTANCE
访问当前的 Application
实例。这是因为 AppJoint 框架 保证了当主 App 的 App
对象被调用 attachBaseContext
回调时,全部组件化业务模块的 Application
也会被调用 attachBaseContext
这个回调。
这样,咱们在 module1
这个模块里的任何位置使用 Module1Application.INSTANCE
总能正确地得到 Application
的实例。对应的,咱们使用相同的方法在 module2
这个模块里,也能够在任何位置使用 Module2Application.INSTANCE
正确地得到 Application
的实例,而不须要知道当前处于独立编译运行状态仍是全量编译运行状态。
必定不要 依赖某个业务模块自身定义的
Application
类的实例(例如Module1Application
的实例),由于在运行时真正使用的Application
实例可能不是它。
咱们已经解决业务模块在 单独编译运行模式 下和在 App 全量编译模式 下,初始化逻辑应该如何组织的问题。咱们沿用了咱们熟悉的自定义 Application
方案,来承载各个模块的初始化行为,同时利用 AppJoint 这个胶水,把每一个模块的初始化逻辑集成到最终全量编译的 App 中。而这一切和 AppJoint 有关的 API 仅仅是两个注解,这里很好的说明了 AppJoint 是个学习成本低的工具,咱们能够沿用咱们已有的开发方式而不是改造咱们原有的代码逻辑致使项目和组件化框架形成过分耦合。
虽然目前每一个模块已经有独立编译运行的可能了,可是开发一个成熟的 App 咱们还有一个重要的问题没有解决,那就是跨模块的方法调用。由于咱们的业务模块不管是从业务逻辑上考虑仍是从在依赖树上的位置考虑,都应该是具备同等的地位的,体如今依赖层次上,这些业务模块应该是平级的,且互相之间没有依赖:
上图是咱们比较理想状况下的组件化的最终状态,App
模块不承载任何业务逻辑,它的做用仅仅是做为一个 application
壳把 Module1
~ Module(n)
这个 n 个模块的功能都集成在一块儿成为一个完整的 App。Module1
~ Module(n)
这 n 个模块互相之间不存在任何交叉依赖,它们各自仅包含各自的业务逻辑。这种方式虽然完成了业务模块之间的解耦,可是给咱们带来的新的挑战:业务模块之间互相调用彼此的功能是很是常见且合理的需求,可是因为这些模块在依赖层次上位于同一层次,因此显然是没法直接调用的。
此外,上图的这种形态是组件化的最终的理想状态,若是咱们要将项目改造以达到这种状态,毫无疑问须要付出巨大的时间成本。在业务快速迭代期间,这是咱们没法承担的成本,咱们只能逐渐地改造项目,也就是说,App
模块内的业务代码是被逐渐拆解出来造成新的独立模块的,这意味着在组件化过程的至关长一段时间内,App
内仍是存在业务代码的,而被拆解出来的模块内的业务逻辑代码,是有可能调用到 App
模块内的代码的。这是一种很尴尬的状态,在依赖层次中,位于依赖层次较低位置的代码反而要去调用依赖层次较高位置的代码。
针对这种状况,咱们比较容易想到,咱们再新建一个模块,例如 router
模块,咱们在这个模块内定义 全部业务模块但愿暴露给其它模块调用的方法,以下图:
projectRoot
+--app
+--module1
+--module2
+--standalone
+--router
| +--main
| | +--java
| | | +--com.yourPackage
| | | | +--AppRouter.java
| | | | +--Module1Router.java
| | | | +--Module2Router.java
复制代码
在上面的项目结构层次中,咱们在新建的 router
模块下定义了 3 个 接口:
AppRouter
接口声明了 app
模块暴露给 module1
、module2
的方法的定义。Module1Router
接口声明了 module1
模块暴露给 app
、module2
的方法的定义。Module2Router
接口声明了 module2
模块暴露给 module1
、app
的方法的定义。以 AppRouter
接口文件为例,这个接口的定义以下:
public interface AppRouter {
/** * 普通的同步方法调用 */
String syncMethodOfApp();
/** * 以 RxJava 形式封装的异步方法 */
Observable<String> asyncMethod1OfApp();
/** * 以 Callback 形式封装的异步方法 */
void asyncMethod2OfApp(Callback<String> callback);
}
复制代码
咱们在 AppRouter
这个接口内定义了 1 个同步方法,2 个异步方法,这些方法是 app
模块须要暴露给 module1
、 module2
的方法,同时 app
模块自身也须要提供这个接口的实现,因此首先咱们须要在 app
、module1
、module2
这三个模块的 build.gradle
文件中依赖 router
这个模块:
dependencies {
// Other dependencies
...
api project(":router")
}
复制代码
这里依赖
router
模块的方式是使用api
而不是implementation
是为了把router
模块的信息暴露给依赖了这些业务模块的 standalone 模块,app
模块因为没有别的模块依赖它,不受上面所说的限制,能够写成implementation
依赖。
而后咱们回到 app
模块,为刚刚在 router
定义的 AppRouter
接口提供一个实现:
@ServiceProvider
public class AppRouterImpl implements AppRouter {
@Override
public String syncMethodOfApp() {
return "syncMethodResult";
}
@Override
public Observable<String> asyncMethod1OfApp() {
return Observable.just("asyncMethod1Result");
}
@Override
public void asyncMethod2OfApp(final Callback<String> callback) {
new Thread(new Runnable() {
@Override
public void run() {
callback.onResult("asyncMethod2Result");
}
}).start();
}
}
复制代码
咱们能够发现,咱们把 app
模块内的方法暴露给其它模块的方式和咱们平时写代码并无什么不一样,就是声明一个接口提供给其它模块,同时在本身内部编写一个这个接口的实现类。不管是同步仍是异步,不管是 Callback 的方式,仍是 RxJava 的方式,均可以使用咱们原有的开发方式。惟一的区别就是,咱们在 AppRouterImpl
实现类上方标记了一个 @ServiceProvider
注解,这个注解的做用是用来通知 AppJoint
框架在 AppRouter
和 AppRouterImpl
之间创建联系,这样其它模块就能够经过 AppJoint
找到一个 AppRouter
的实例并调用里面的方法了。
假设如今 module1
中须要调用 app
模块中的 asyncMethod1OfApp
方法,因为 app
模块已经把这个方法声明在了 router
模块的 AppRouter
接口中了,module1
因为也依赖了 router
模块,因此 module1
内能够访问到 AppRouter
这个接口,可是却访问不到 AppRouterImpl
这个实现类,由于这个类定义在 app
模块内,这时候咱们可使用 AppJoint 来帮助 module1
获取 AppRouter
的实例:
AppRouter appRouter = AppJoint.service(AppRouter.class);
// 得到同步调用的结果
String syncResult = appRouter.syncMethodOfApp();
// 发起异步调用
appRouter.asyncMethod1OfApp()
.subscribe((result) -> {
// handle asyncResult
});
// 发起异步调用
appRouter.asyncMethod2OfApp(new Callback<String>() {
@Override
public void onResult(String data) {
// handle asyncResult
}
});
复制代码
在上面的代码中,咱们能够看到,除了第一步获取 AppRouter
接口的实例咱们用到了 AppJoint 的 API AppJoint.service
之外,剩下的代码,module1
调用 app
模块内的方法的方式,和咱们原来的开发方式没有任何区别。AppJoint.service
就是 AppJoint 全部 API 里惟一的那个方法。
也就是说,若是一个模块须要提供方法供其余模块调用,须要作如下步骤:
router
模块中@ServiceProvider
注解完成这两步之后就能够在其它模块中使用如下方式获取该模块声明的接口的实例,并调用里面的方法:
AppRouter appRouter = AppJoint.service(AppRouter.class);
Module1Router module1Router = AppJoint.service(Module1Router.class);
Module2Router module2Router = AppJoint.service(Module2Router.class);
复制代码
这种方法不只仅能够保证处于相同依赖层次的业务模块能够互相调用彼此的方法,还能够支持从业务模块中调用 app
模块内的方法。这样就能够 保证咱们组件化的过程能够是渐进的 ,咱们不须要一口气把 app
模块中的全部功能所有拆分到各个业务模块中,咱们能够逐渐地把功能拆分出来,以保证咱们的业务迭代和组件化改造同时进行。当咱们的 AppRouter
里面的方法愈来愈少直到最后能够把这个类从项目中安全删除的时候,咱们的组件化改造就完成了。
上面一个小结中咱们已经介绍了使用 AppJoint 在 App 全量编译运行期间,业务模块之间跨模块方法调用的解决方案。在全量编译期间,咱们能够经过 AppJoint.service
这个方法找到指定模块提供的接口的实例,可是在模块单独编译运行期间,其它的模块是不参与编译的,它们的代码也不会打包进用于模块独立运行的 standalaone 模块,咱们如何解决在模块单独编译运行模式下,跨模块调用的代码依然有效呢?
以 module1
为例,首先为了便于在 module1
内部任何地方均可以调用其它模块的方法,咱们建立一个 RouterServices
类用于存放其它模块的接口的实例:
public class RouterServices {
// app 模块对外暴露的接口
public static AppRouter sAppRouter = AppJoint.service(AppRouter.class);
// module2 模块对外暴露的接口
public static Module2Router sModule2Router = AppJoint.service(Module2Router.class);
}
复制代码
有了这个类之后,咱们在 module1
内部若是须要调用其它模块的功能,咱们只须要使用 RouterServices.sAppRouter
和 RouterServices.sModule2Router
这两个对象就能够了。可是若是是刚刚提到的 module1
独立编译运行的状况,即启动的 application
模块是 module1Standalone
, 那么 RouterServices.sAppRouter
和 RouterServices.sModule2Router
这两个对象的值均为 null
,这是由于 app
和 module2
这两个模块此时是没有被编译进来的。
若是咱们须要在这种状况下保证已有的 module1
内部的经过 RouterServices.sAppRouter
和 RouterServices.sModule2Router
进行跨模块方法调用的代码依然能工做,咱们就须要对这两个引用手动赋值,即咱们须要建立 Mock 了 AppRouter
和 Module2Router
功能的类。这些类因为只对 module1
的独立编译运行有意义,因此这些类最合适的位置是放在 module1Standalone
这个模块内,以 AppRouter
的 Mock 类 AppRouterMock
为例:
public class AppRouterMock implements AppRouter {
@Override
public String syncMethodOfApp() {
return "mockSyncMethodOfApp";
}
@Override
public Observable<String> asyncMethod1OfApp() {
return Observable.just("mockAsyncMethod1OfApp");
}
@Override
public void asyncMethod2OfApp(final Callback<String> callback) {
new Thread(new Runnable() {
@Override
public void run() {
callback.onResult("mockAsyncMethod2Result");
}
}).start();
}
}
复制代码
已经建立好了 Mock 类,接下来咱们要作的是,在 module1
独立编译运行的模式下,用 Mock 类的对象,去替换 RouterServices
里面的对应的引用,因为这些逻辑只和 module1
的独立编译运行有关,咱们不但愿这些逻辑被打包进真正 Release 的 App 中,那么最合适的地方就是 Module1StandaloneApplication
里了:
public class Module1StandaloneApplication extends Module1Application {
@Override
public void onCreate() {
// module1 init inside super.onCreate()
super.onCreate();
// initialization only used for running module1 standalone
Log.i("module1Standalone", "module1Standalone init is called");
// Replace instances inside RouterServices
RouterServices.sAppRouter = new AppRouterMock();
RouterServices.sModule2Router = new Module2RouterMock();
}
}
复制代码
有了上面的初始化动做之后,咱们就能够在 module1
内部安全地使用 RouterServices.sAppRouter
和 RouterServices.sModule2Router
这两个对象进行跨模块的方法调用了,不管当前是处于 App 全量编译模式仍是 modul1Standalone
独立编译运行模式。
在组件化改造过程当中,除了跨模块的方法调用以外,跨模块启动 Activity 和跨模块引用 Fragment 也是咱们常常遇到的需求。目前社区中大多数组件化方案都是使用自定义私有协议,使用 URL-Scheme 的方式来实现跨模块 Activity 的启动,这一块已经有不少成熟的方案了,有的组件化方案直接推荐使用 ARouter 来实现这块功能。可是 AppJoint 没有使用这类方案。
本文开头曾经介绍过,AppJoint 全部的 API 只包含 3 个注解加 1 个方法,而这些 API 咱们在前文中已经都介绍完了,也就是说,咱们没有提供专门的 API 来实现跨模块的 Activity / Fragment 调用。
咱们回想一下,在没有实现组件化时,咱们启动 Activity 的推荐写法以下,首先在被启动的 Activity 内实现一个静态 start
方法:
public class MyActivity extends AppCompatActivity {
public static void start(Context context, String param1, Integer param2) {
Intent intent = new Intent(context, MyActivity.class);
intent.putExtra("param1", param1);
intent.putExtra("param2", param2);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
}
}
复制代码
而后咱们若是在其它 Activity 中启动这个 MyActivity
的话,写法以下:
MyActivity.start(param1, param2);
复制代码
这里的思想是,服务的提供者应该把复杂的逻辑放在本身这里,而只提供给调用者一个简单的接口,用这个简单的接口隔离具体实现的复杂性,这是符合软件工程思想的。
那么若是目前 module1
模块中有一个 Module1Activity
,如今这个 Activity 但愿可以从 module2
启动,应该如何写呢?首先,在 router
模块的 Module1Router
内声明启动 Module1Activity
的方法:
public interface Module1Router {
...
// 启动 Module1Activity
void startModule1Activity(Context context);
}
复制代码
而后在 module1
模块里 Module1Router
对应的实现类 Module1RouterImpl
中实现刚刚定义的方法:
@ServiceProvider
public class Module1RouterImpl implements Module1Router {
...
@Override
public void startModule1Activity(Context context) {
Intent intent = new Intent(context, Module1Activity.class);
context.startActivity(intent);
}
}
复制代码
这样, module2
中就能够经过下面的方式启动 module1
中的 Module1Activity
了。
RouterServices.sModule1Router.startModule1Activity(context);
复制代码
跨模块获取 Fragment
实例也是相似的方法,咱们在 Module1Router
里继续声明方法:
public interface Module1Router {
...
// 启动 Module1Activity
void startModule1Activity(Context context);
// 获取 Module1Fragment
Fragment obtainModule1Fragment();
}
复制代码
差很少的写法,咱们只要在 Module1RouterImpl
里接着实现方法便可:
@ServiceProvider
public class Module1RouterImpl implements Module1Router {
@Override
public void startModule1Activity(Context context) {
Intent intent = new Intent(context, Module1Activity.class);
context.startActivity(intent);
}
@Override
public Fragment obtainModule1Fragment() {
Fragment fragment = new Module1Fragment();
Bundle bundle = new Bundle();
bundle.putString("param1", "value1");
bundle.putString("param2", "value2");
fragment.setArguments(bundle);
return fragment;
}
}
复制代码
前面提到过,目前社区大多数组件化方案都是使用 自定义私有协议,利用 URL-Scheme 的方式来实现跨模块页面跳转 的,即相似 ARouter 的那种方案,为何 AppJoint 不采用这种方案呢?
缘由其实很简单,假设项目中没有组件化的需求,咱们在同一个模块内进行 Activity 的跳转,确定不会采用 URL-Scheme 方式进行跳转,咱们确定是本身建立 Intent
进行跳转的。其实说到底,使用 URL-Scheme 进行跳转是 不得已而为之,它只是手段,不是目的,由于在组件化以后,模块之间彼此的 Activity 变得不可见了,因此咱们转而使用 URL-Scheme 的方式进行跳转。
如今 AppJoint 从新支持了使用代码进行跳转,只须要把跳转的逻辑抽象为接口中的方法暴露给其它模块,其它模块就能够调用这个方法实现跳转逻辑。除此之外,使用接口提供跳转逻辑相比 URL-Scheme 方式还有什么优点呢?
类型安全。充分利用 Java 这种静态类型语言的编译器检查功能,经过接口暴露的跳转方法,不管是传参仍是返回值,若是类型错误,在编译期间就能发现错误,而使用 URL-Scheme 进行跳转,若是发生类型上的错误,只能在运行期间才能发现错误。
效率高。即便是使用 URL-Scheme 进行跳转,底层仍然是构造 Intent
进行跳转,可是却额外引入了对跳转 URL 进行构造和解析的过程,涉及到额外的序列化和反序列化逻辑,下降了代码的执行效率。而使用接口提供的跳转逻辑,咱们直接构造 Intent
进行跳转,不涉及到任何额外的序列化和反序列化操做,和咱们平常的 Activity 跳转逻辑执行效率相同。
IDE 友好。使用 URL-Scheme 进行跳转,IDE 没法提供任何智能提示,只能依靠完善的文档或者开发者自身检查来确保跳转逻辑的正确性,而经过接口提供跳转逻辑能够最大限度发挥 IDE 的智能提示功能,确保咱们的跳转逻辑是正确的。
易于重构。使用 URL-Scheme 进行跳转,若是遇到跳转逻辑须要重构的状况,例如 Activity 名字的修改,参数名称的修改,参数数量的增删,只能依靠开发者对使用到跳转逻辑的地方一个一个修改,并且没法确保所有都修改正确了,由于编译器没法帮咱们检查。而经过接口提供的跳转逻辑代码须要重构时,编译器能够自动帮助咱们检查,一旦有地方没有改对,直接在编译期报错,并且 IDE 都提供了智能重构的功能,咱们能够方便地对接口中定义的方法进行重构。
学习成本低。咱们能够沿用咱们熟悉的开发方式,不须要去学习 URL-Scheme 跳转框架的 API。这样还能够保证咱们的跳转逻辑不与具体的框架强绑定,咱们经过接口隔离了跳转逻辑的真正实现,即便使用 AppJoint 进行跳转,咱们也能够在随时把跳转逻辑切换到其余方案,包括 URL-Scheme 方式。
我我的的实践,目前项目中同一进程内的页面跳转已经所有由 AppJoint 的方式实现,目前只有跨进程的页面启动交给了 URL-Scheme 这种方式(例如从浏览器唤醒 App 某个页面)。
最后再提一点,因为跨模块启动 Activity 沿用了跨模块方法调用的开发方式,在业务模块单独编译运行模式下,咱们也须要 Mock 这些启动方法。既然咱们是在独立调试某个业务模块,咱们确定不是真的但愿跳转到那些页面,咱们在 Mock 方法里直接输出 Log 或者 Toast 便可。
到这里为止,使用 AppJoint 进行组件化的介绍就已经结束了。AppJoint 的 Github 地址为:github.com/PrototypeZ/… 。核心代码不超过 500 行,您彻底能够快速掌握这个工具加速您的组件化开发,只要 Fork 一份代码便可。若是您不想本身引入工程,咱们也提供了一个开箱即用的版本,您能够直接经过 Gradle 引入。
build.gradle
文件中添加 AppJoint插件 依赖:buildscript {
...
dependencies {
...
classpath 'io.github.prototypez:app-joint:{latest_version}'
}
}
复制代码
dependencies {
...
implementation "io.github.prototypez:app-joint-core:{latest_version}"
}
复制代码
apply plugin: 'com.android.application'
apply plugin: 'app-joint'
复制代码
经过本文的介绍,咱们其实能够发现 AppJoint 是个思想很简单的组件化方案。虽然简单,可是却直接并且够用,尽管没有像其它的组件化方案那样提供了各类各样强大的 API,可是却足以胜任大多数中小型项目,这是咱们一以贯之的设计理念。
若是您感受这个项目对您有帮助,但愿能够点一个 Star ,谢谢 : ) 。文章很长,感谢您耐心读完。因为本人能力有限,文章可能存在纰漏的地方,欢迎各位指正,再次谢谢你们!
若是您对个人技术分享感兴趣,欢迎关注个人我的公众号:麻瓜日记,不按期更新原创技术分享,谢谢!:)