此外的问题,模块边界破坏、基础工程中心化,都是代码持续劣化的帮凶…
看完之后就陷入了沉思,这个问题不就是我们面临的问题吗?不仅是在组件化中,在很多形成依赖关系的场景中都有此类问题。
假设有user组件和分享组件,分享组件需要user组件提供数据。
具体是怎么体现的呢,我们来看一组图:
解决方式为分享组件依赖user组件,能解决问题,假设,有一个组件A,需要引用分享组件,就必须依赖分享组件和user组件,这就一举打破了组件编译隔离的远景,组件化将失去香味儿。
将user组件中的公共数据部分下沉到base组件,分享组件依赖base组件即可实现数据提供,然而当非常多的组件需要互相提供数据时,将出现中心化问题,只需要分享组件的B组件不得不依赖base组件,引入其他数据。也就造成了代码中心化下沉失去组件化的意义。
=======================================================================
微信面对这个痛心疾首的问题时发出了“君有疾在腠理,不治将恐深” 的感慨,但也出具了非常厉害的操作-.api化。
这个操作非常高级,做法非常腾讯,但是此文档中只提到了精髓,没有具体的操作步骤,对我们来讲依然存在挑战。
什么是代码中心化问题的.api方案
先看一下具体的操作过程是什么样的。上图3中,我们使用某种技术将user组件中需要共享数据的部分抽象成接口,利用AS对文件类型的配置将(kotlin)后拽修改为.api ,然后再创建一个同包名的module-api 组件用来让其他组件依赖,分享组件和其他组件以及自身组件在module模式下均依赖该组件,这样就能完美的将需要共享的数据单独出去使用了。
SPI 方式实现
这个有点类似SPI(Service Provider Interface)机制:
大概就是说我们可以将要共享的数据先抽象到接口中形成标准服务接口,然后在具体的实现中,然后在对应某块中实现该接口,当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;然后利用 ServiceLoader 来加载配置文件中指定的实现,此时我们在不同组件之间通过ServiceLoader加载需要的文件了。
利用ARouter
利用ARouter在组件间传递数据的方式+gralde自动生成module-api组件,形成中心化问题的.api化。假设我们满足上述的所有关系,并且构建正确,那我们怎么处理组件间的通信?
Arouter 阿里通信路由
@Route(path = "/test/activity")
public class YourActivity extend Activity {
...
}
跳转:
ARouter.getInstance().build("/test/activity").withLong("key1", 666L).navigation()
// 声明接口,其他组件通过接口来调用服务
public interface HelloService extends IProvider {
String sayHello(String name);
}
// 实现接口
@Route(path = "/yourservicegroupname/hello", name = "测试服务")
public class HelloServiceImpl implements HelloService {
@Override
public String sayHello(String name) {
return "hello, " + name;
}
@Override
public void init(Context context) {
}
}
//测试
public class Test {
@Autowired
HelloService helloService;
@Autowired(name = "/yourservicegroupname/hello")
HelloService helloService2;
HelloService helloService3;
HelloService helloService4;
public Test() {
ARouter.getInstance().inject(this);
}
public void testService() {
// 1. (推荐)使用依赖注入的方式发现服务,通过注解标注字段,即可使用,无需主动获取
// Autowired注解中标注name之后,将会使用byName的方式注入对应的字段,不设置name属性,会默认使用byType的方式发现服务(当同一接口有多个实现的时候,必须使用byName的方式发现服务)
helloService.sayHello("Vergil");
helloService2.sayHello("Vergil");
// 2. 使用依赖查找的方式发现服务,主动去发现服务并使用,下面两种方式分别是byName和byType
helloService3 = ARouter.getInstance().navigation(HelloService.class);
helloService4 = (HelloService)ARouter.getInstance().build("/yourservicegroupname/hello").navigation();
helloService3.sayHello("Vergil");
helloService4.sayHello("Vergil");
}
}
假如user组件的用户信息需要给支付组件使用,那我们怎么处理?
ARouter可以通过上面的IProvider注入服务的方式通信,或者使用EventBus这种方式。
data class UserInfo(val uid: Int, val name: String)
/**
*@author kpa
*@date 2021/7/21 2:15 下午
*@email billkp@yeah.net
*@description 用户登录、获取信息等
*/
interface IAccountService : IProvider {
//获取账号信息 提供信息*
fun getUserEntity(): UserInfo?
}
//注入服务
@Route(path = "/user/user-service")
class UserServiceImpl : IAccountService {
//...
}
在支付组件中
问题就暴露在了我们眼前,支付组件中的IAccountService和UserInfo从哪里来?
这也就是module-api 需要解决的问题,在原理方面:
将需要共享的数据和初始化数据的类文件设计为.api文件
打开AS-> Prefernces -> File Types找到kotlin(Java)选中在File name patterns 里面添加".api"(注意这个后缀随意开心的话都可以设置成.kpa)
举例:
data class UserInfo(val userName: String, val uid: Int)
interface UserService {
fun getUserInfo(): UserInfo
}
生成包含共享的数据和初始化数据的类文件的module-api组件
这步操作有以下实现方式。
-
自己手动创建一个module-api 组件 显然这是不可取但是可行的
-
使用脚本语言shell 、python 等扫描指定路径生成对应module-api
-
利用Android 编译环境及语言groovy,编写gradle脚本,优势在于不用考虑何时编译,不打破编译环境,书写也简单
=========================================================================
找到这些问题出现的原理及怎么去实现之后,从github上找到了优秀的人提供的脚本,完全符合我们的使用预期。
def includeWithApi(String moduleName) {
def packageName = "com/xxx/xxx"
//先正常加载这个模块
include(moduleName)
//找到这个模块的路径
String originDir = project(moduleName).projectDir
//这个是新的路径
String targetDir = "${originDir}-api"
//原模块的名字
String originName = project(moduleName).name
//新模块的名字
def sdkName = "${originName}-api"
//这个是公共模块的位置,我预先放了一个 新建的api.gradle 文件进去
String apiGradle = project(":apilibrary").projectDir
// 每次编译删除之前的文件
deleteDir(targetDir)
//复制.api文件到新的路径
copy() {
from originDir
into targetDir
exclude '**/build/'
exclude '**/res/'
include '**/*.api'
}
//直接复制公共模块的AndroidManifest文件到新的路径,作为该模块的文件
copy() {
from "${apiGradle}/src/main/AndroidManifest.xml"
into "${targetDir}/src/main/"
}
//复制 gradle文件到新的路径,作为该模块的gradle
copy() {
from "${apiGradle}/api.gradle"
into "${targetDir}/"
}
//删除空文件夹
deleteEmptyDir(new File(targetDir))
//todo 替换成自己的包名
//为AndroidManifest新建路径,路径就是在原来的包下面新建一个api包,作为AndroidManifest里面的包名
String packagePath = "{originName}/api"
//todo 替换成自己的包名,这里是apilibrary模块拷贝的AndroidManifest,替换里面的包名
//修改AndroidManifest文件包路径
fileReader("{originName}.api")
new File(packagePath).mkdirs()
//重命名一下gradle
def build = new* File(targetDir + "/api.gradle")
if(build.exists()) {
build.renameTo(new File(targetDir + "/build.gradle"))
}
// 重命名.api文件,生成正常的.java文件
renameApiFiles(targetDir, '.api', '.java')
//正常加载新的模块
include ":$sdkName"
}
private void deleteEmptyDir(File dir) {
if(dir.isDirectory()) {
File[] fs = dir.listFiles()
if(fs != null && fs.length > 0) {
for (int i = 0; i < fs.length; i++) {
File tmpFile = fs[i]
if (tmpFile.isDirectory() {
deleteEmptyDir(tmpFile)
}
if (tmpFile.isDirectory() && tmpFile.listFiles().length <= 0){
tmpFile.delete()
}
}
}
if (dir.isDirectory() && dir.listFiles().length == 0) {
dir.delete()
}
}
总结
现在新技术层出不穷,如果每次出新的技术,我们都深入的研究的话,很容易分散精力。新的技术可能很久之后我们才会在工作中用得上,当学的新技术无法学以致用,很容易被我们遗忘,到最后真的需要使用的时候,又要从头来过(虽然上手会更快)。
我觉得身为技术人,针对新技术应该是持拥抱态度的,入了这一行你就应该知道这是一个活到老学到老的行业,所以面对新技术,不要抵触,拥抱变化就好了。
Flutter 明显是一种全新的技术,而对于这个新技术在发布之初,花一个月的时间学习它,成本确实过高。但是周末花一天时间体验一下它的开发流程,了解一下它的优缺点、能干什么或者不能干什么。这个时间,并不是我们不能接受的。
如果有时间,其实通读一遍 Flutter 的文档,是最全面的一次对 Flutter 的了解过程。但是如果我们只有 8 小时的时间,我希望能关注一些最值得关注的点。
附
(跨平台开发(Flutter)、java基础与原理,自定义view、NDK、架构设计、性能优化、完整商业项目开发等)