阅读 1478

协程 路由 组件化 1+1+1>3 | 掘金年度征文

废话

2020 对我来说还是一个比较重要的一年,我是一个90年的老安卓了,前几年一直有点中年危机的感觉。因为一直都在小公司,所以受限于开发眼界问题,只能自己钻研一些看到的技术,陷入了技术的瓶颈中,感觉到了所谓的天花板,要上手一些新的东西特别的难,特别是gradle插件相关的。

去年离职去了哈啰,然后也算了解了一些大公司的技术栈和开发模式等,同时也接触了一些基架相关的技术。抽取了一部分空余时间,仔细阅读了别人的实现的功能,补强了自己在Gradle和一部分简单的Apm相关的知识,同时也拓展了自己对于组件化的一些理解。

说点自己的看法把,现在能去大公司还是尽量去大公司吧,和一群优秀人的人在一起共事,进步的速度会更快一点。比如我现在同组的几个大佬,都尤其的强,在他们那边还是学到了很多之前完全没理解的技术。有个人能带你一把其实就可以少做很多弯路了。

正文

正文开始我要先把本文要介绍的两个项目地址先发出来给大家,其实配合着项目和Demo去阅读这篇文章,应该会让各位对于一些奇怪的姿势点有些不一样的理解把。

我自己写的玩具路由组件

更合理的多仓库编译插件

协程和响应式

我想先说下响应式编程,我个人理解就是一个输入值会有一个有效的输出。当我调用一个方法的时候这个方法最后能直接给我一个结果,而不是通过callback的形式告诉我这个结果。因为异步的情况下,简单的说就是不够直白。

而协程则是通过其中的挂起函数,即把回调函数通过挂起和恢复的机制,变成一个有返回值的方法,当我调用这个方法的时候,没有返回值的情况下,程序就是处于一个开发不需要关心的挂起状态,而有返回值之后,我们就可以继续向下变成了。

这么写可以让我们把原本复杂而繁琐的异步编程,还有其中最为可怕的回调地狱,变成线性类似同步的一句句的顺序执行的代码,还可以方便后续代码的迭代维护。

这也正是我比Rx更推崇协程的原因,凡事还是逃不开真香定律的。

从startActivityForResult说起

相信大家都用过startActivityForResult,其中有几个场景尤其恶心人,比如我如果在一个列表页内使用这个,我要先把Click事件抛给Activity,然后在onActivityResult方法中处理结果。而真实的使用方,那个adapter则就真的难受了。

抛开谷歌最近的那套框架,如果在既有代码上,或者路由上,有没有可能优化出一个比较细腻的写法呢?

在我的玩具路由框架中,我借鉴了Premission权限请求库的原理,通过一个代理的Fragment,将参数以及目标页等传递到这个Fragment上,将Fragment绑定到外层的Activity上。然后由这个Fragment发起页面跳转逻辑,同时接受页面跳转的返回值以及回调参数,然后通过Callback的方式来通知路由当前的转跳结果。

class FragmentForResult : Fragment() {

    var onSuccess: () -> Unit = {}
    var onFail: () -> Unit = {}
    var clazz: Class<out Any>? = null

    private var code by Delegates.notNull<Int>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        code = arguments?.getInt("requestCode", 0) ?: 0
        val intent = Intent()
        clazz?.let { intent.setClass(requireContext(), it) }
        //记住哦  一定是fragment自己的 而不是context的
        startActivityForResult(intent, code)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == code) {
            if (resultCode == Activity.RESULT_OK) {
                onSuccess.invoke()
            } else {
                onFail.invoke()
            }
        }
    }

}
// 
fun AppCompatActivity.startForResult(code: Int, clazz: Class<out Any>, bundle: Bundle? = null,
                                     onSuccess: () -> Unit = {}, onFail: () -> Unit = {}) {

    var fragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as FragmentForResult?
    if (fragment == null) {
        fragment = FragmentForResult()
    }
    fragment.onSuccess = onSuccess
    fragment.onFail = onFail
    fragment.clazz = clazz
    val mBundle = Bundle()
    bundle?.apply {
        mBundle.putAll(this)
    }
    mBundle.putInt("requestCode", code)
    fragment.arguments = bundle
    supportFragmentManager.beginTransaction().apply {
        if (fragment.isAdded) {
            remove(fragment)
        }
        add(fragment, FRAGMENT_TAG)
    }.commitNowAllowingStateLoss()
}

private const val FRAGMENT_TAG = "FragmentForResult"
复制代码

小贴士 这里一定要注意 千万使用Fragment内部的startActivityForResult,而不是context的。

虽然这个写法已经解决了我的一部分痛点,可以让我在非Activity内直接使用结果, 但是我个人觉得是不是还可以更流弊一点呢??

如果我能在调用方法的时候就获取到实际结果,那么多香呀!!如果在我的SDK内部,使用方直接可以用挂起恢复的机制获取到结果,岂不是卧槽牛逼。

suspend fun KRequest.dispatcher(context: Context): Boolean {
    return withContext(Dispatchers.Main) {
        val result = await(context)
        result
    }
}

suspend fun KRequest.await(context: Context): Boolean {
    return suspendCancellableCoroutine { continuation ->
        onSuccess = {
            continuation.resume(true)
        }
        onFail = {
            continuation.resume(false)
        }
        start(context)
    }
}
复制代码

首先我并没有直接在路由的module内直接引用协程,而是把协程的支持功能作为一个独立的仓库提供给使用方。这样如果当前项目没有使用到协程,也就可以不需要直接依赖到协程的仓库。

好吧,我们开始动手改一改好了。首先我们通过挂起函数,把一个异步封装suspend,最后把结果成抛给使用方。这里我只使用了协程内部提供的suspendCancellableCoroutine,把所有的callbak转化成了一个挂起函数,我之前的文章也介绍过,Rxjava中的Emitor就是类似的作用。而由于startActivity的操作必须操作在主线程中,所以在外层可以额外包裹一个线程调度。

最后我们看下使用方会变成什么样吧?

修改前

  try {
                val request = KRequest("https://www.baidu.com/test", onSuccess = {
                    Log.i("KRequest", "onSuccess")
                }, onFail = {
                    Log.i("KRequest", "onFail")
                }).apply {
                    activityResultCode = 12345
                }.start(this)
            } catch (e: Exception) {
                e.printStackTrace()
            }
复制代码

修改后

 GlobalScope.launch {
                val request = KRequest("https://www.baidu.com/test").apply {
                    activityResultCode = 12345
                    addValue("1234", "1234")
                }.dispatcher(this@MainActivity)
                Log.i("", "")
            }
复制代码

从我个人角度,起码这个更线性了,同时我如果后续有结果逻辑,也会编写起来更简单一点。

模块化优化技巧

如果文章就只有这么点内容,感觉还是有点水分的啊,没错我就是标题党,骗大家进来的核心目的还是让你们看看我最近在gradle插件上,对于模块化的装逼小技巧。

下面会从几个小问题分别来对其进行分析。

发布jcenter的aar怪怪的

不知道各位开发同学在实际项目编写中碰到一些烦恼。以我玩具路由组件项目为例。下图是我的Project的样子。

RouterLib依赖了RouterAnatation,我以前在推送aar到Jcenter的时候,就经常碰到RouterLib的pom中会找不到RouterAnatation的依赖的问题。

首先gradle通过group、name(id)、version唯一确定某jar包,这点和maven类似

我们的每个Modulebuild.gradle中其实也有group,version,而name(id)就是我们的ModuleName。我之前也困惑过,国外大佬们在推Aar的时候,难道也要这么麻烦,一个个修改之后推送吗,这也太过于不智能了吧。

jake大神的ButterKnife的 gradle upload

apply plugin: 'maven'
apply plugin: 'signing'

version = VERSION_NAME
group = GROUP
.....
复制代码

其实就是因为大神们在每个module都定义好了group+version,最后上传jcenter的时候他们根本就不需要关心这部分依赖的问题, 插件在生成Pom文件的时候就会把这些本地仓库都更换成group+id+version的方式。

合理的利用这个方式

这里告诉大家一个很简单的小技巧,你可以肆无忌惮的直接写group+id+version,通过简单的Gradle语法,把他们直接更换成我们本地的仓库。

先来看下这部分代码。

// 给所有的project增加configuration
allprojects {
 configurations.all { Configuration c ->
 		// 所有的依赖降级策略
       c.resolutionStrategy {
           dependencySubstitution {
               all { DependencySubstitution dependency ->
                   if (dependency.requested instanceof ModuleComponentSelector) {
                   	// 如果发现项目内存在 group +name 等于远端的group+name  那么直接采用本地工程进行编译
                       def p = rootProject.allprojects.find { p -> p.group == dependency.requested.group && p.name == dependency.requested.module }
                       if (p != null) {
                           dependency.useTarget(project(p.path), 'selected local project')
                       }
                   }
               }
           }
       }
   }
}
复制代码

在项目的根节点的build.gradle中声明上述代码,定义好configurations的依赖策略,将所有的远程依赖更换成本地依赖。。我简单的给上述代码增加了点注释,这样可以方便大家去理解这个代码到底是干了啥的。

通过这种方式,我们所有的模块内的implementation就可以都写成我们上传到jcenter上的远程依赖地址,即时他们并没有存在在远端也没有关系。在开发阶段,都会以本地的仓库的源代码来进行编译,而不会使用远端版本。

Gradle-Repo 升级版

之前给大家介绍了Gradle-Repo的一个插件,以前我们是用类似这个机制来做为分仓之后动态组合各个组件的核心,但是在使用的过程中又很多小毛病把,举个例子所以如果这个Module引用了当前Project全局的一些配置,就会导致编译失败的问题。

摘自 Gradle 文档:复合构建只是包含其他构建的构建. 在许多方面,复合构建类似于 Gradle 多项目构建,不同之处在于,它包括完整的 builds ,而不是包含单个 projects

组合通常独立开发的构建,例如,在应用程序使用的库中尝试错误修复时,将大型的多项目构建分解为更小,更孤立的块,可以根据需要独立或一起工作。

这个问题同样可以采用上述讲到ComposeBuilding的方式改进,因为每个Project都是独立编译的,所以也就不存在缺失项目所需的特定内容了。

我改进了下大佬之前的插件地址是GradleTask

路由的Plugin

路由组件中的Apt主要是帮助Module来生成路由表用的,其中这个apt插件在module上其实有很多小隐患和小毛病把,下面给大家展开稍微科普下如何优化。

小升级

路由中的含有一个Plugin,主要就是负责把收集每个Jar包或者源码中的类,然后通过asm插装的方式,动态的往一个注册类内去注册,我之前的文章也介绍过这方面的。我有个大胆的方案可以提高ARouter和WMRouter的编译速度

其实这里还有个小地方可以优化下,不知道各位有没有使用过ARouter, 每一个'com.android.library'都需要添加一个代码块。

kapt {
    arguments {
        arg("AROUTER_MODULE_NAME", project.getName())
    }
}
复制代码

这个原因吗是因为apt的执行顺序不固定,同时javapoet只能生成新的java类,而没有办法对一个原来的class进行修改。这就导致了每个模块生成的路由表的类都需要是一个独立不重复的名字。而开发如果万一漏了这句代码的话,那么就会因为路由表的类重复导致路由缺失了。

我感觉吧用文档束缚这个还是略微有点薄弱,所以我在我当前的Plugin插件内补充了下这部分功能。

class AutoRegisterPlugin implements Plugin<Project> {

    private static final String EXT_NAME = "AutoRegister";

    @Override
    void apply(Project project) {
        project.getExtensions().create(EXT_NAME, AutoRegisterConfig.class);
        if (project.plugins.hasPlugin('com.android.application')) {
            project.android.registerTransform(new NewAutoRegisterTransform())
            project.afterEvaluate(new Action<Project>() {
                @Override
                void execute(Project newProject) {
                    AutoRegisterConfig config = (AutoRegisterConfig) newProject.getExtensions().findByName(EXT_NAME);
                    if (config == null) {
                        config = new AutoRegisterConfig()
                    }
                    config.transform()
                }
            })
        }
        project.afterEvaluate {
            if (project.plugins.hasPlugin('com.android.library')) {
                def android = project.extensions.getByName('android')
                android.defaultConfig.javaCompileOptions.annotationProcessorOptions {
                    arguments = [ROUTER_MODULE_NAME: project.getName()]
                }
                if (project.plugins.hasPlugin('kotlin-kapt')) {
                    def kapt = project.extensions.getByName('kapt')
                    kapt.arguments {
                        arg("ROUTER_MODULE_NAME", project.getName())
                    }
                }
            }
        }
    }
}
复制代码

这样只要'com.android.library'也同样引入router-register插件就可以了,其实还可以给当前的module直接引用apt插件和路由的依赖,但是我只能暂时给自己加个TODO了。

No Gradle Plguin 不使用gradle插件的小tips

如果你当前本来就是基于ARouter或者类似的路由组件开发的时候,如果也需要你在Module内写arguments,而且你觉得写gradle plugin太麻烦的话,你也可以把代码拷贝到根目录的build.gradle下面,这样也可以做到同样的功能。

allprojects {
 project.afterEvaluate {
            if (project.plugins.hasPlugin('com.android.library')) {
                def android = project.extensions.getByName('android')
                android.defaultConfig.javaCompileOptions.annotationProcessorOptions {
                    arguments = [ROUTER_MODULE_NAME: project.getName()]
                }
                if (project.plugins.hasPlugin('kotlin-kapt')) {
                    def kapt = project.extensions.getByName('kapt')
                    kapt.arguments {
                        arg("ROUTER_MODULE_NAME", project.getName())
                    }
                }
            }
        }
}
复制代码

当场编译插件

gradle插件的开发以前我一直觉得有两个痛点。

1.插件没办法当场被项目所引用到,每次变更都需要搞好久,用了buildSrc之后也会要重新copy module 推送远端。

2.调试比较困难,插件对于新手来说,你没有人带的情况下,基本想学会插件的调试是地狱难度。

下面会给大家科普一个更有意思的玩法,还是基于ComposeBuilding的,但是需要依赖于另外一个gradle插件,让两个原本没关联的东西,强行关联到一起。

之前我安利过大家通过buildSrc+settings的方式去写一个gradle plugin。但是其实在年终的时候,已经有大佬翻译了gradle团队的文档,他们现在建议使用一种叫Composed Build的方式去编译插件。

再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度

其实我的路由项目中,也是有这个新版的plugin的方式的。有兴趣开发插件的同学后续开发可以考虑下这个方式的。

首先我们需要把在当前模块下新建一个Project,然后这个Project(后面就叫Plugin)就是我们的Plugin插件存放的位置了,然后在原始的Project的settings.gradle下,添加一句includeBuild('./Plugin')。这样在我们运行当前项目的时候就会把Plugin工程也进行编译。

之后我们只要在Plugin项目下新建我们的plugin module(后面都叫autoRegister)。

apply plugin: 'groovy'
apply plugin: 'java-gradle-plugin'

buildscript {
    repositories {
        jcenter()
        maven {
            url 'https://maven.aliyun.com/repository/central/'
        }
        maven {
            url 'https://dl.bintray.com/leifzhang/maven'
        }
        jcenter()
    }
    dependencies {
        // 因为使用的 Kotlin 需要需要添加 Kotlin 插件
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72"
    }
}
repositories {
    maven {
        url 'https://maven.aliyun.com/repository/central/'
    }
    maven {
        url 'https://dl.bintray.com/leifzhang/maven'
    }
    jcenter()
    google()
}

dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation 'com.kronos.plugin:BasePlugin:0.2.0'
    implementation 'com.android.tools.build:gradle:4.0.0'
    implementation 'commons-io:commons-io:2.6'
    implementation 'org.javassist:javassist:3.20.0-GA'
}

gradlePlugin {
    plugins {
        version {
            // 在 app 模块需要通过 id 引用这个插件
            id = 'router-register'
            // 实现这个插件的类的路径
            implementationClass = 'com.kronos.autoregister.AutoRegisterPlugin'
        }
    }
}
复制代码

这上面最重要的就是java-gradle-plugin这个插件,以及下面的Extension,这个是建立好两个Project的插件依赖的核心,Project通过这个插件就能直接找到我们的autoRegister插件了,而且后续的任意代码改动也就能当场生效了。

// 根目录下的 build.gradle
plugins {
    //  文件夹下 build.gradle 文件内定义的id apply false表示当前gradle 不引用
    id "router-register" apply false
}
复制代码

最后我们只需要在我们项目Project的build.gradle下,声明这个Plugin,这样我们的plugin就能被Project所引用到了。

总结

2020感恩的一年吧,其实蛮感谢身边的大佬的,因为跟着大佬其实可以更快的进步,而且可以少走很多弯路的。今年在掘金大概写了30篇文章了,要感谢各位读者,感谢各位能看我的文章,你们的阅读其实大大的满足了我的虚荣心。

其实写文章同时对自己也是一个提升的,不仅能提高自己的文档能力,而且当你写一篇文章的时候,更多的你会去把这方面相关的资料进行一次更系统的掌握,然后你才敢和各位大佬吹牛啊。

掘金年度征文 | 2020 与我的技术之路 征文活动正在进行中......