├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── README2.md ├── build.gradle ├── build.versions.toml ├── buildDebug.sh ├── buildHub.sh ├── buildHub.yaml ├── buildModule.sh ├── common ├── implements-process │ ├── .gitignore │ ├── build.gradle │ └── src │ │ └── main │ │ └── java │ │ └── com │ │ └── example │ │ └── RouterProcess.java └── java-base │ ├── .gitignore │ ├── build.gradle │ ├── gradle │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle │ └── src │ └── main │ └── java │ └── cn │ └── mycommons │ └── modulebase │ └── annotations │ ├── Implements.kt │ ├── ImplementsManager.java │ ├── Router.java │ └── RouterParam.java ├── doc ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png └── 未命名.key ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── maven.gradle ├── module.gradle ├── plugin ├── module-plugin-ksp │ ├── .gitignore │ ├── build.gradle │ ├── gradle │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle │ └── src │ │ └── main │ │ ├── kotlin │ │ └── cn │ │ │ └── mycommons │ │ │ └── module_plugin │ │ │ └── ksp │ │ │ ├── KspConsts.kt │ │ │ ├── ModuleProcessorProvider.kt │ │ │ ├── ModuleSymbolProcessor.kt │ │ │ ├── PluginContextKit.kt │ │ │ ├── model │ │ │ └── model.kt │ │ │ ├── process │ │ │ ├── ModuleConfigProcess.kt │ │ │ ├── RouterParamKSVisitorVoid.kt │ │ │ ├── RouterParamProcess.kt │ │ │ └── ksp.kt │ │ │ └── util │ │ │ └── logs.kt │ │ └── resources │ │ └── META-INF │ │ └── services │ │ └── com.google.devtools.ksp.processing.SymbolProcessorProvider └── module-plugin │ ├── .gitignore │ ├── build.gradle │ ├── gradle │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle │ └── src │ └── main │ ├── kotlin │ └── cn │ │ └── mycommons │ │ └── module_plugin │ │ ├── ModulePlugin.kt │ │ ├── core │ │ ├── Consts.kt │ │ ├── ImplementRecord.kt │ │ ├── PluginKit.kt │ │ └── v2 │ │ │ └── ModuleProcessTask.kt │ │ └── util │ │ └── file.kt │ └── resources │ └── META-INF │ └── gradle-plugins │ └── cn.mycommons.module_plugin.properties ├── settings.gradle ├── test-module ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ ├── src │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── cn │ │ │ │ └── mycommons │ │ │ │ └── androidmodular │ │ │ │ ├── InjectHelper.kt │ │ │ │ ├── RouterHelper.kt │ │ │ │ ├── app │ │ │ │ ├── AppContext.kt │ │ │ │ ├── ModuleConfig.kt │ │ │ │ └── ModuleLifeCycleManager.kt │ │ │ │ └── ui │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ ├── layout │ │ │ └── activity_main.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ ├── test1.ks │ └── test1.properties ├── module-base │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── cn │ │ │ └── mycommons │ │ │ └── modulebase │ │ │ ├── IModuleConfig.kt │ │ │ ├── IModuleLifeCycle.kt │ │ │ ├── IRouterProcess.kt │ │ │ ├── annotations │ │ │ └── LogTrace.kt │ │ │ ├── base │ │ │ └── BaseActivity.kt │ │ │ ├── simple │ │ │ └── SimpleModuleLifeCycle.kt │ │ │ └── util │ │ │ └── AppLog.kt │ │ └── res │ │ └── values │ │ └── strings.xml ├── module-component │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── cn │ │ │ └── mycommons │ │ │ └── modulecomponent │ │ │ └── UIButton.kt │ │ └── res │ │ └── values │ │ └── strings.xml ├── module-order │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── cn │ │ │ └── mycommons │ │ │ └── moduleorder │ │ │ ├── OrderActivity.kt │ │ │ ├── OrderModuleLifeCycle.kt │ │ │ └── OrderServiceImpl.kt │ │ └── res │ │ ├── layout │ │ └── activity_order.xml │ │ └── values │ │ └── strings.xml ├── module-service │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── cn │ │ │ └── mycommons │ │ │ └── moduleservice │ │ │ ├── IOrderService.kt │ │ │ ├── IShoppingService.kt │ │ │ ├── IUserService.kt │ │ │ └── bean │ │ │ └── OrderInfo.kt │ │ └── res │ │ └── values │ │ └── strings.xml ├── module-shopping │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── cn │ │ │ └── mycommons │ │ │ └── moduleshopping │ │ │ ├── ShoppingActivity.kt │ │ │ ├── ShoppingModuleLifeCycle.kt │ │ │ └── ShoppingServiceImpl.kt │ │ └── res │ │ ├── layout │ │ └── activity_shopping.xml │ │ └── values │ │ └── strings.xml └── module-user │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── cn │ │ └── mycommons │ │ └── moduleuser │ │ ├── JavaUserActivity.java │ │ ├── UserActivity.kt │ │ ├── UserModuleLifeCycle.kt │ │ └── UserServiceImpl.kt │ └── res │ ├── layout │ └── activity_user.xml │ └── values │ └── strings.xml └── test-plugin ├── test-plugin-app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── cn │ │ │ └── mycommons │ │ │ └── testplugin │ │ │ ├── MainActivity.kt │ │ │ └── api │ │ │ ├── IApi.kt │ │ │ └── impl │ │ │ └── ApiImpl.kt │ │ └── res │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml ├── test1.ks └── test1.properties └── test-plugin-lib ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml ├── java └── cn │ └── mycommons │ └── testpluginlib │ ├── ITest.kt │ └── impl │ └── TestImpl.kt └── res └── values └── strings.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .DS_Store 5 | /build 6 | /captures 7 | .externalNativeBuild 8 | .idea 9 | 10 | module 11 | module.gradle -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | debug: 3 | sh buildDebug.sh 4 | 5 | module: 6 | sh buildModule.sh 7 | 8 | 9 | debugPlugin: 10 | ./gradlew :test-module:module-user:assembleDebug --no-daemon -Dorg.gradle.debug=true -Pkotlin.compiler.execution.strategy=in-process 11 | 12 | hub: 13 | sh buildHub.sh -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 关于Android模块化我有一些话不知当讲不当讲 2 | 3 | --- 4 | 5 | 可直接运行与开发 6 | 7 | ```shell 8 | 9 | # 执行 Makefile debug 任务 10 | make debug 11 | 12 | # 执行 Makefile module 任务 13 | make module 14 | ``` 15 | 16 | ## 修改记录 17 | 18 | ### 2024-06-17 19 | 20 | * 升级 kotlin 2.0.0 21 | 22 | ### 2024-01-23 23 | 24 | * 升级到agp 8.2.0 25 | 26 | [关于Android模块化我有一些话不知当讲不当讲](README2.md) 27 | 28 | ## Star History 29 | 30 | [![Star History Chart](https://api.star-history.com/svg?repos=LiushuiXiaoxia/AndroidModular&type=Date)](https://star-history.com/#LiushuiXiaoxia/AndroidModular&Date) 31 | -------------------------------------------------------------------------------- /README2.md: -------------------------------------------------------------------------------- 1 | # 关于Android模块化我有一些话不知当讲不当讲 2 | 3 | --- 4 | 5 | 6 | - [关于Android模块化我有一些话不知当讲不当讲](#关于android模块化我有一些话不知当讲不当讲) 7 | - [模块化场景](#模块化场景) 8 | - [架构演变](#架构演变) 9 | - [技术要点](#技术要点) 10 | - [Library module](#library-module) 11 | - [Library module开发问题](#library-module开发问题) 12 | - [依赖管理](#依赖管理) 13 | - [数据通信](#数据通信) 14 | - [页面路由跳转](#页面路由跳转) 15 | - [Interface和Implement](#interface和implement) 16 | - [EventBus](#eventbus) 17 | - [映射匹配](#映射匹配) 18 | - [Map register](#map-register) 19 | - [APT](#apt) 20 | - [Gradle Transform](#gradle-transform) 21 | - [映射匹配总结](#映射匹配总结) 22 | - [开发调试技巧](#开发调试技巧) 23 | - [Debug](#debug) 24 | - [容器设计](#容器设计) 25 | - [CI](#ci) 26 | - [总结](#总结) 27 | - [相关资料](#相关资料) 28 | 29 | 30 | 31 | 最近公司一个项目使用了模块化设计,本人参与其中的一个小模块开发,但是整体的设计并不是我架构设计的,开发半年有余,在此记录下来我的想法。 32 | 33 | [TOC] 34 | 35 | ## 模块化场景 36 | 37 | 为什么需要模块化? 38 | 39 | 当一个App用户量增多,业务量增长以后,就会有很多开发工程师参与同一个项目,人员增加了,原先小团队的开发方式已经不合适了。 40 | 41 | 原先的一份代码,现在需要多个人来维护,每个人的代码质量也不相同,在进行代码Review的时候,也是比较困难的,同时也容易会产生代码冲突的问题。 42 | 43 | 同时随着业务的增多,代码变的越来越复杂,每个模块之间的代码耦合变得越来越严重,解耦问题急需解决,同时编译时间也会越来越长。 44 | 45 | 人员增多,每个业务的组件各自实现一套,导致同一个App的UI风格不一样,技术实现也不一样,团队技术无法得到沉淀。 46 | 47 | ## 架构演变 48 | 49 | 在刚刚开始的时候,项目架构使用的是MVP模式,这也是最近几年很流行的一个架构方式,下面是项目的原始设计。 50 | 51 | ![MVP](https://raw.githubusercontent.com/LiushuiXiaoxia/AndroidModular/master/doc/1.png) 52 | 53 | 随着业务的增多,我们添加了Domain的概念,Domain从Data中获取数据,Data可能会是Net,File,Cache各种IO等,然后项目架构变成了这样。 54 | 55 | ![MVP2](https://raw.githubusercontent.com/LiushuiXiaoxia/AndroidModular/master/doc/2.png) 56 | 57 | 再然后随着人员增多,各种基础组件也变的越来越多,业务也很复杂,业务与业务之间还有很强的耦合,就变成了这样的。 58 | 59 | ![](https://raw.githubusercontent.com/LiushuiXiaoxia/AndroidModular/master/doc/3.png) 60 | 61 | 使用模块化技术以后,架构变成了这样。 62 | 63 | ![组件化架构](https://raw.githubusercontent.com/LiushuiXiaoxia/AndroidModular/master/doc/4.png) 64 | 65 | ## 技术要点 66 | 67 | 这里简单介绍下Android项目实现模块化需要使用的技术以及技术难点。 68 | 69 | ### Library module 70 | 71 | 在开始开始进行模块化之前,需要把各个业务单独抽取成Android Library Module,这个是Android Studio自带一个功能,可以把依赖较少的,作为基本组件的抽取成一个单独模块。 72 | 73 | 如图所示,我把各个模块单独分为一个独立的项目。 74 | 75 | ![组件化架构](https://raw.githubusercontent.com/LiushuiXiaoxia/AndroidModular/master/doc/5.png) 76 | 77 | 在主项目中使用gradle添加代码依赖。 78 | 79 | ```gradle 80 | // common 81 | compile project(':ModuleBase') 82 | compile project(':ModuleComponent') 83 | compile project(':ModuleService') 84 | 85 | // biz 86 | compile project(':ModuleUser') 87 | compile project(':ModuleOrder') 88 | compile project(':ModuleShopping') 89 | ``` 90 | 91 | ### Library module开发问题 92 | 93 | 在把代码抽取到各个单独的Library Module中,会遇到各种问题。最常见的就是R文件问题,Android开发中,各个资源文件都是放在res目录中,在编译过程中,会生成R.java文件。R文件中包含有各个资源文件对应的id,这个id是静态常量,但是在Library Module中,这个id不是静态常量,那么在开发时候就要避开这样的问题。 94 | 95 | 举个常见的例子,同一个方法处理多个view的点击事件,有时候会使用`switch(view.getId())`这样的方式,然后用`case R.id.btnLogin`这样进行判断,这时候就会出现问题,因为id不是经常常量,那么这种方式就用不了。 96 | 97 | 同样开发时候,用的最多的一个第三方库就是ButterKnife,ButterKnife也是不可以用的,在使用ButterKnife的时候,需要用到注解配置一个id来找到对应view,或者绑定对应的各种事件处理,但是注解中的各个字段的赋值也是需要静态常量,那么就不能够使用ButterKnife了。 98 | 99 | 解决方案有下面几种: 100 | 101 | 1.重新一个Gradle插件,生成一个R2.java文件,这个文件中各个id都是静态常量,这样就可以正常使用了。 102 | 103 | 2.使用Android系统提供的最原始的方式,直接用`findViewById`以及`setOnClickListener`方式。 104 | 105 | 3.设置项目支持Databinding,然后使用Binding中的对象,但是会增加不少方法数,同时Databinding也会有编译问题和学习成本,但是这些也是小问题,个人觉的问题不大。 106 | 107 | 上面是主流的解决方法,个人推荐的使用优先级为 3 > 2 > 1。 108 | 109 | 当把个模块分开以后,每个人就可以单独分组对应的模块就行了,不过会有资源冲突问题,个人建议是对各个模块的资源名字添加前缀,比如user模块中的登录界面布局为`activity_login.xml`,那么可以写成这样`us_activity_login.xml`。这样就可以避免资源冲突问题。同时Gradle也提供的一个字段`resourcePrefix`,确保各个资源名字正确,具体用法可以参考官方文档。 110 | 111 | ### 依赖管理 112 | 113 | 当完成了Library module后,代码基本上已经很清晰了,跟我们上面的最终架构已经很相似了,有了最基本的骨架,但是还是没有完成,因为还是多个人操作同一个git仓库,各个开发小伙伴还是需要对同一个仓库进行各种fork和pr。 114 | 115 | 随着对代码的分割,但是主项目app的依赖变多了,如果修改了lib中的代码,那么编译时间是很恐怖的,大概统计了一下,原先在同一个模块的时候,编译时间大概需要2-3min,但是分开以后大概需要5-6min,这个是绝对无法忍受的。 116 | 117 | 上面的第一问题,可以这样解决,把各个子module分别使用单独的一个git仓库,这样每个人也只需要关注自己需要的git仓库即可,主仓库使用git submodule的方式,分别依赖各个子模块。 118 | 119 | 但是这样还是无法解决编译时间过长的问题,我们把各个模块也单独打包,每次子模块开发完成以后,发布到maven仓库中,然后在主项目中使用版本进行依赖。 120 | 121 | 举个例子,比如进行某一版本迭代,这个版本叫1.0.0,那么各个模块的版本也叫同样的版本,当版本完成测试发布后,对各个模块打对应版本的tag,然后就很清楚的了解各模块的代码分布。 122 | 123 | gradle依赖如下。 124 | 125 | ```gradle 126 | // common 127 | compile 'cn.mycommons:base:1.0.0' 128 | compile 'cn.mycommons:component:1.0.0' 129 | compile 'cn.mycommons:service:1.0.0' 130 | 131 | // biz 132 | compile 'cn.mycommons:user:1.0.0' 133 | compile 'cn.mycommons:order:1.0.0' 134 | compile 'cn.mycommons:shopping:1.0.0' 135 | ``` 136 | 137 | 可能有人会问,既然各个模块已经分开开发,那么如果进行开发联调,别急,这个问题暂时保留,后面会对这个问题后面再表。 138 | 139 | ### 数据通信 140 | 141 | 当一个大项目拆成若干小项目时候,调用的姿势发生了少许改变。我这边总结了App各个模块之间的数据通信几种方式。 142 | 143 | * 页面跳转,比如在订单页面下单时候,需要判断用户是否登录,如果没有则需要跳到登录界面。 144 | * 主动获取数据,比如在下单时候,用户已经登录,下单需要传递用户的基本信息。 145 | * 被动获得数据,比如在切换用户的时候,有时候需要更新数据,如订单页面,需要把原先用户的购物车数据给清空。 146 | 147 | 再来看下App的架构。 148 | 149 | ![App架构](https://raw.githubusercontent.com/LiushuiXiaoxia/AndroidModular/master/doc/3.png) 150 | 151 | 第一个问题,原先的方式,直接指定某个页面的ActivityClass,然后通过intent跳转即可,但是在新的架构中,由于shopping模块不直接依赖user,那么则不能使用原始的进行跳转,我们解决方式使用Router路由跳转。 152 | 153 | 第二个问题,原先的方式有个专门的业务单利,比如UserManager,直接可以调用即可,同样由于依赖发生了改变,不能够进行调用。解决方案是所有的需要的操作,定义成接口放在Service中。 154 | 155 | 第三个问题,原先的方式,可以针对事件变化提供回调接口,当我需要监听某个事件时候,设置回调即可。 156 | 157 | #### 页面路由跳转 158 | 159 | 如上分析,原先方式代码如下。 160 | 161 | ```java 162 | Intent intent = new Intent(this, UserActivity.class); 163 | startActivity(intent); 164 | ``` 165 | 166 | 但是使用Router后,调用方式改变了。 167 | 168 | ```java 169 | RouterHelper.dispatch(getContext(), "app://user"); 170 | ``` 171 | 172 | 具体的原理是什么,很简单的,做一个简单的映射匹配即可,把`"app://user"`与`UserActivity.class`配对,具体的就是定义一个Map,key是对应的Router字符,value是Activity的class。在跳转时候从map中获取对应的ActivityClass,然后在使用原始的方式。 173 | 174 | 可能有人的会问,要向另外一个页面传递参数怎么办,没事我们可以在router后面直接添加参数,如果是一个复杂的对象那么可以把对象序列化成json字符串,然后再从对应的页面通过反序列化的方式,得到对应的对象。 175 | 176 | 例如: 177 | 178 | ```java 179 | RouterHelper.dispatch(getContext(), "app://user?id=123&obj={"name":"admin"}"); 180 | ``` 181 | 182 | **注:** 上面的router中json字符串是需要url编码的,不然会有问题的,这里只是做个示例。 183 | 184 | 185 | 除了使用Router进行跳转外,我想了一下,可以参考Retrofit方式,直接定义跳转Java接口,如果需要传递额外参数,则以函数参数的方式定义。 186 | 187 | 这个Java接口是没有实现类的,可以使用动态代理方式,然后接下来的方式,和使用Router的方式一样。 188 | 189 | 那么这总两种方式有什么优缺点呢。 190 | 191 | **Router方式:** 192 | 193 | * 有点:不需要高难度的技术点,使用方便,直接使用字符串定义跳转,可以好的往后兼容 194 | * 缺点:因为使用的是字符串配置,如果字符输入字符,则很难发现bug,同时也很难知道某个参数对应的含义 195 | 196 | **仿Retrofit方式:** 197 | 198 | * 因为是Java接口定义,所以可以很简单找到对应的跳转方法,参数定义也很明确,可以直接写在接口定义处,方便查阅。 199 | * 同样因为是Java接口定义,那么如果需要扩展参数,只能重新定义新方法,这样会出现多个方法重载,如果在原先接口上修改,对应的原先调用方也要做响应的修改,比较麻烦。 200 | 201 | 上面是两种实现方式,如果有相应同学要实现模块化,可以根据实际情况做出选择。 202 | 203 | #### Interface和Implement 204 | 205 | 如上分析,如果需要从某个业务中获取数据,我们分别需要定义接口以及实现类,然在获取的时候在通过反射来实例化对象。 206 | 207 | 下面是简单的代码示例 208 | 209 | 210 | 接口定义 211 | 212 | ```java 213 | public interface IUserService { 214 | 215 | String getUserName(); 216 | } 217 | ``` 218 | 219 | 实现类 220 | 221 | ```java 222 | class UserServiceImpl implements IUserService { 223 | 224 | @Override 225 | public String getUserName() { 226 | return "UserServiceImpl.getUserName"; 227 | } 228 | } 229 | ``` 230 | 231 | 反射生成对象 232 | 233 | ``` 234 | public class InjectHelper { 235 | 236 | @NonNull 237 | public static AppContext getAppContext() { 238 | return AppContext.getAppContext(); 239 | } 240 | 241 | @NonNull 242 | public static IModuleConfig getIModuleConfig() { 243 | return getAppContext().getModuleConfig(); 244 | } 245 | 246 | @Nullable 247 | public static T getInstance(Class tClass) { 248 | IModuleConfig config = getIModuleConfig(); 249 | Class implementClass = config.getServiceImplementClass(tClass); 250 | if (implementClass != null) { 251 | try { 252 | return implementClass.newInstance(); 253 | } catch (Exception e) { 254 | e.printStackTrace(); 255 | } 256 | } 257 | return null; 258 | } 259 | } 260 | ``` 261 | 262 | 实际调用 263 | 264 | ``` 265 | IUserService userService = InjectHelper.getInstance(IUserService.class); 266 | if (userService != null) { 267 | Toast.makeText(getContext(), userService.getUserName(), Toast.LENGTH_SHORT).show(); 268 | } 269 | ``` 270 | 271 | 本示例中每次调用都是用反射生成新的对象,实际应用中可能与IoC工具结合使用,比如Dagger2. 272 | 273 | #### EventBus 274 | 275 | 针对上面的第三个问题,原先设计的使用方式也是可以的,只需要把回调接口定义到对应的service接口中,然后调用方就可以使用。 276 | 277 | 但是我建议可以使用另外一个方式——EventBus,EventBus也是利用观察者模式,对事件进行监听,是设置回调更优雅方式的实现。 278 | 279 | 优点:不需要定义很多个回调接口,只需要定义事件Class,然后通过Claas的唯一性来进行事件匹配。 280 | 281 | 缺点:需要定义很多额外的类来表示事件,同时也需要关注EventBus的生命周期,在不需要使用事件时候,需要注销事件绑定,不然容易发生内存泄漏。 282 | 283 | 284 | ## 映射匹配 285 | 286 | 上面的介绍的各个模块之间通信,都运涉及到映射匹配问题,在此我总结了一下,主要涉及到一下三种方式。 287 | 288 | ### Map register 289 | 290 | Map register是这样的,全局定义一个Map,各个模块在初始化的时候,分别在初始化的时候注册映射关系。 291 | 292 | 下面是简单的代码示例,比如我们定义一个模块生命周期,用于初始化各个模块。 293 | 294 | ```java 295 | public interface IModuleLifeCycle { 296 | 297 | void onCreate(IModuleConfig config); 298 | 299 | void onTerminate(); 300 | } 301 | ``` 302 | 303 | User模块初始化 304 | 305 | ```java 306 | public class UserModuleLifeCycle extends SimpleModuleLifeCycle { 307 | 308 | public UserModuleLifeCycle(@NonNull Application application) { 309 | super(application); 310 | } 311 | 312 | @Override 313 | public void onCreate(@NonNull IModuleConfig config) { 314 | config.registerService(IUserService.class, UserServiceImpl.class); 315 | config.registerRouter("app://user", UserActivity.class); 316 | } 317 | } 318 | ``` 319 | 320 | 在Application中完成初始化 321 | 322 | ```java 323 | public class AppContext extends Application { 324 | 325 | private ModuleLifeCycleManager lifeCycleManager; 326 | 327 | @Override 328 | public void onCreate() { 329 | super.onCreate(); 330 | 331 | lifeCycleManager = new ModuleLifeCycleManager(this); 332 | lifeCycleManager.onCreate(); 333 | } 334 | 335 | @Override 336 | public void onTerminate() { 337 | super.onTerminate(); 338 | 339 | lifeCycleManager.onTerminate(); 340 | } 341 | 342 | @NonNull 343 | public IModuleConfig getModuleConfig() { 344 | return lifeCycleManager.getModuleConfig(); 345 | } 346 | } 347 | 348 | public class ModuleLifeCycleManager { 349 | 350 | @NonNull 351 | private ModuleConfig moduleConfig; 352 | @NonNull 353 | private final List moduleLifeCycleList; 354 | 355 | ModuleLifeCycleManager(@NonNull Application application) { 356 | moduleConfig = new ModuleConfig(); 357 | moduleLifeCycleList = new ArrayList<>(); 358 | moduleLifeCycleList.add(new UserModuleLifeCycle(application)); 359 | moduleLifeCycleList.add(new OrderModuleLifeCycle(application)); 360 | moduleLifeCycleList.add(new ShoppingModuleLifeCycle(application)); 361 | } 362 | 363 | void onCreate() { 364 | for (IModuleLifeCycle lifeCycle : moduleLifeCycleList) { 365 | lifeCycle.onCreate(moduleConfig); 366 | } 367 | } 368 | 369 | void onTerminate() { 370 | for (IModuleLifeCycle lifeCycle : moduleLifeCycleList) { 371 | lifeCycle.onTerminate(); 372 | } 373 | } 374 | 375 | @NonNull 376 | IModuleConfig getModuleConfig() { 377 | return moduleConfig; 378 | } 379 | } 380 | ``` 381 | 382 | ### APT 383 | 384 | 使用注解的方式配置映射信息,然后生成一个类似Database一样的文件,然后Database文件中包含一个Map字段,Map中记录各个映射信息。 385 | 386 | 首先需要定义个Annotation。 387 | 388 | 如: 389 | 390 | ```java 391 | @Target(ElementType.TYPE) 392 | @Retention(RetentionPolicy.CLASS) 393 | public @interface Implements { 394 | 395 | Class parent(); 396 | } 397 | ``` 398 | 399 | 需要实现一个 Annotation Process Tool,来解析自己定义的Annotation。 400 | 401 | 代码略,此代码有点复杂,暂时不贴了。 402 | 403 | 编译产生的文件,大概如下所示。 404 | 405 | ```java 406 | public class Implement_$$_Database { 407 | 408 | @NonNull 409 | private final Map, Class> serviceConfig; 410 | 411 | public Implement_$$_Database() { 412 | 413 | serviceConfig = new HashMap<>(); 414 | serviceConfig.put(IUserService.class, UserServiceImpl.class); 415 | } 416 | 417 | public Class getServiceImplementClass(Class serviceClass) { 418 | return (Class) serviceConfig.get(serviceClass); 419 | } 420 | } 421 | ``` 422 | 423 | 然后利用反射找到`Implement_$$_Database`这个类,然后从方法中找到配对。 424 | 425 | ``` 426 | public class InjectHelper { 427 | 428 | @Nullable 429 | public static T getInstanceByDatabase(Class tClass) { 430 | Implement_$$_Database database = new Implement_$$_Database(); 431 | Class implementClass = database.getServiceImplementClass(tClass); 432 | if (implementClass != null) { 433 | try { 434 | return implementClass.newInstance(); 435 | } catch (Exception e) { 436 | e.printStackTrace(); 437 | } 438 | } 439 | return null; 440 | } 441 | } 442 | ``` 443 | 444 | 然后在需要配置的地方添加注解即可。 445 | 446 | ```java 447 | @Implements(parent = IUserService.class) 448 | class UserServiceImpl implements IUserService { 449 | 450 | @Override 451 | public String getUserName() { 452 | return "UserServiceImpl.getUserName"; 453 | } 454 | } 455 | ``` 456 | 457 | 调用姿势。 458 | 459 | ```java 460 | binding.button.setOnClickListener(new View.OnClickListener() { 461 | @Override 462 | public void onClick(View v) { 463 | IUserService userService = InjectHelper.getInstanceByDatabase(IUserService.class); 464 | if (userService != null) { 465 | Toast.makeText(getContext(), userService.getUserName(), Toast.LENGTH_SHORT).show(); 466 | } 467 | } 468 | }); 469 | ``` 470 | 471 | 472 | **注意点:** 473 | 474 | 有时候,在生成最终的配置文件的时候,文件的名字是固定的,比如上面的`Implement_$$_Database`,最终的路径是这样的`cn.mycommons.implements.database.Implement_$$_Database.java`,然后通过编译到apk中或则是aar中。 475 | 476 | 但是有个问题,如果各个子模块都使用了这样的插件,那么每个子模块的就会有这个Implement_$$_Database.class,那么就会编译出错。 477 | 478 | 因为aar中包含的时候class文件,不是java文件,不能在使用APT做处理了。下面有2中解决方案。 479 | 480 | 1. 子工程的插件生成的文件包含一定的规则,比如包含模块名字,如`User_Implement_$$_Database.java`,同时修改编译过程,把java文件也打包到aar中,主工程的插件在编译时候,提取aar中的文件,然后合并子工程的所有的代码,这个思路是可行的,不过技术实现起来比较麻烦。 481 | 482 | 2. 同一的方式类似,也是生成有一定规则的的文件,或者在特地package下生成class,这些class再通过接下来的所讲的Gradle Transform方式,生成一个新的Database.class文件。 483 | 484 | ### Gradle Transform 485 | 486 | 这是Android Gradle编译提供的一个接口,可以供开发自定义一些功能,而我们就可以根据这个功能生成映射匹配,这种方式和APT类似,APT是运行在代码编译时期,而且Transform是直接扫描class,然后再生成新的class,class中包含Map映射信息。修改class文件,使用的是[javassist](http://jboss-javassist.github.io/javassist/)一个第三方库。 487 | 488 | 下面简单讲述代码实现,后面有机会单独写一篇文章讲解。 489 | 490 | 首先定义一个注解,这个注解用于标注一个实现类的接口。 491 | 492 | ```java 493 | package cn.mycommons.modulebase.annotations; 494 | 495 | @Target(ElementType.TYPE) 496 | @Retention(RetentionPolicy.CLASS) 497 | public @interface Implements { 498 | Class parent(); 499 | } 500 | ``` 501 | 502 | 一个测试用的接口以及实现类。 503 | 504 | ```java 505 | public interface ITest { 506 | } 507 | 508 | 509 | @Implements(parent = ITest.class) 510 | public class TestImpl implements ITest { 511 | 512 | } 513 | ``` 514 | 515 | 516 | 定义一个静态方法,用于获取某个接口的实现类。 517 | 518 | ```java 519 | package cn.mycommons.modulebase.annotations; 520 | 521 | public class ImplementsManager { 522 | 523 | private static final Map CONFIG = new HashMap<>(); 524 | 525 | 526 | public static Class getImplementsClass(Class parent) { 527 | return CONFIG.get(parent); 528 | } 529 | } 530 | ``` 531 | 532 | 如果不使用任何黑科技,直接使用Java技术,那么在定义时候需要主动的往CONFIG这个map中添加配置,但是这里我们利用transform,直接动态的添加。 533 | 534 | 定义一个`ImplementsPlugin` gradle插件。 535 | 536 | ```java 537 | public class ImplementsPlugin implements Plugin { 538 | 539 | @Override 540 | public void apply(Project project) { 541 | AppExtension app = project.getExtensions().getByType(AppExtension.class); 542 | app.registerTransform(new ImplementsTransform(project)); 543 | } 544 | } 545 | ``` 546 | 547 | 自定义的Transform实现。 548 | 549 | ```groovy 550 | public class ImplementsTransform extends Transform { 551 | 552 | static final String IMPLEMENTS_MANAGER = "cn/mycommons/modulebase/annotations/ImplementsManager.class" 553 | static final String IMPLEMENTS_MANAGER_NAME = "cn.mycommons.modulebase.annotations.ImplementsManager" 554 | Project project 555 | 556 | ImplementsTransform(Project project) { 557 | this.project = project 558 | } 559 | 560 | void log(String msg, Object... args) { 561 | String text = String.format(msg, args) 562 | 563 | project.getLogger().error("[ImplementsPlugin]:${text}") 564 | } 565 | 566 | @Override 567 | public String getName() { 568 | return "ImplementsTransform" 569 | } 570 | 571 | @Override 572 | public Set getInputTypes() { 573 | return ImmutableSet.of(QualifiedContent.DefaultContentType.CLASSES) 574 | } 575 | 576 | @Override 577 | public Set getScopes() { 578 | return ImmutableSet.of( 579 | QualifiedContent.Scope.PROJECT, 580 | QualifiedContent.Scope.PROJECT_LOCAL_DEPS, 581 | QualifiedContent.Scope.SUB_PROJECTS, 582 | QualifiedContent.Scope.SUB_PROJECTS_LOCAL_DEPS, 583 | QualifiedContent.Scope.EXTERNAL_LIBRARIES 584 | ) 585 | } 586 | 587 | @Override 588 | boolean isIncremental() { 589 | return false 590 | } 591 | 592 | @Override 593 | void transform(TransformInvocation transformInvocation) 594 | throws TransformException, InterruptedException, IOException { 595 | super.transform(transformInvocation) 596 | long time1 = System.currentTimeMillis(); 597 | log(this.toString() + ".....transform") 598 | 599 | TransformOutputProvider outputProvider = transformInvocation.outputProvider 600 | outputProvider.deleteAll() 601 | 602 | def classPool = new ClassPool() 603 | classPool.appendSystemPath() 604 | 605 | // 记录所有的符合扫描条件的记录 606 | List implementsList = [] 607 | // ImplementsManager 注解所在的jar文件 608 | JarInput implementsManagerJar = null 609 | 610 | // 扫描所有的文件 611 | transformInvocation.inputs.each { 612 | it.directoryInputs.each { 613 | classPool.appendClassPath(it.file.absolutePath) 614 | def dst = outputProvider.getContentLocation(it.name, it.contentTypes, it.scopes, Format.DIRECTORY) 615 | FileUtils.copyDirectory(it.file, dst) 616 | 617 | project.fileTree(dst).each { 618 | String clazzPath = it.absolutePath.replace(dst.absolutePath, "") 619 | clazzPath = clazzPath.replace("/", ".").substring(1) 620 | if (clazzPath.endsWith(".class")) { 621 | clazzPath = clazzPath.substring(0, clazzPath.size() - 6) 622 | CtClass clazz = classPool.get(clazzPath) 623 | // 如果class中的类包含注解则先收集起来 624 | Implements annotation = clazz.getAnnotation(Implements.class) 625 | if (annotation != null) { 626 | implementsList.add(new Entry(annotation, clazz)) 627 | } 628 | } 629 | } 630 | } 631 | it.jarInputs.each { 632 | classPool.appendClassPath(it.file.absolutePath) 633 | 634 | if (implementsManagerJar == null && isImplementsManager(it.file)) { 635 | implementsManagerJar = it 636 | } else { 637 | def dst = outputProvider.getContentLocation(it.name, it.contentTypes, it.scopes, Format.JAR) 638 | FileUtils.copyFile(it.file, dst) 639 | 640 | def jarFile = new JarFile(it.file) 641 | def entries = jarFile.entries() 642 | 643 | // 如果jar中的class中的类包含注解则先收集起来 644 | while (entries.hasMoreElements()) { 645 | def jarEntry = entries.nextElement() 646 | String clazzPath = jarEntry.getName() 647 | clazzPath = clazzPath.replace("/", ".") 648 | if (clazzPath.endsWith(".class")) { 649 | clazzPath = clazzPath.substring(0, clazzPath.size() - 6) 650 | def clazz = classPool.get(clazzPath) 651 | Implements annotation = clazz.getAnnotation(Implements.class) 652 | if (annotation != null) { 653 | implementsList.add(new Entry(annotation, clazz)) 654 | } 655 | } 656 | } 657 | } 658 | } 659 | } 660 | 661 | log("implementsManagerJar = " + implementsManagerJar) 662 | 663 | Map config = new LinkedHashMap<>() 664 | 665 | implementsList.each { 666 | def str = it.anImplements.toString(); 667 | log("anImplements =" + it.anImplements) 668 | def parent = str.substring(str.indexOf("(") + 1, str.indexOf(")")).replace("parent=", "").replace(".class", "") 669 | log("parent =" + parent) 670 | log("sub =" + it.ctClass.name) 671 | 672 | // 收集所有的接口以及实现类的路径 673 | config.put(parent, it.ctClass.name) 674 | } 675 | 676 | log("config = " + config) 677 | 678 | long time2 = System.currentTimeMillis(); 679 | 680 | if (implementsManagerJar != null) { 681 | def implementsManagerCtClass = classPool.get(IMPLEMENTS_MANAGER_NAME) 682 | log("implementsManagerCtClass = " + implementsManagerCtClass) 683 | 684 | // 修改class,在class中插入静态代码块,做初始化 685 | def body = "{\n" 686 | body += "CONFIG = new java.util.HashMap();\n" 687 | 688 | for (Map.Entry entry : config.entrySet()) { 689 | body += "CONFIG.put(${entry.key}.class, ${entry.value}.class);\n" 690 | } 691 | 692 | body += "}\n" 693 | log("body = " + body) 694 | 695 | implementsManagerCtClass.makeClassInitializer().body = body 696 | 697 | def jar = implementsManagerJar 698 | def dst = outputProvider.getContentLocation(jar.name, jar.contentTypes, jar.scopes, Format.JAR) 699 | println dst.absolutePath 700 | 701 | // 修改完成后,完成后再写入到jar文件中 702 | rewriteJar(implementsManagerJar.file, dst, IMPLEMENTS_MANAGER, implementsManagerCtClass.toBytecode()) 703 | } 704 | 705 | log("time = " + (time2 - time1) / 1000) 706 | } 707 | 708 | static boolean isImplementsManager(File file) { 709 | return new JarFile(file).getEntry(IMPLEMENTS_MANAGER) != null 710 | } 711 | 712 | static void rewriteJar(File src, File dst, String name, byte[] bytes) { 713 | dst.getParentFile().mkdirs() 714 | 715 | def jarOutput = new JarOutputStream(new FileOutputStream(dst)) 716 | def rcJarFile = new JarFile(src) 717 | 718 | jarOutput.putNextEntry(new JarEntry(name)) 719 | jarOutput.write(bytes) 720 | 721 | def buffer = new byte[1024] 722 | int bytesRead 723 | def entries = rcJarFile.entries() 724 | 725 | while (entries.hasMoreElements()) { 726 | def entry = entries.nextElement() 727 | if (entry.name == name) continue 728 | jarOutput.putNextEntry(entry) 729 | 730 | def jarInput = rcJarFile.getInputStream(entry) 731 | while ((bytesRead = jarInput.read(buffer)) != -1) { 732 | jarOutput.write(buffer, 0, bytesRead) 733 | } 734 | jarInput.close() 735 | } 736 | 737 | jarOutput.close() 738 | } 739 | } 740 | ``` 741 | 742 | 具体代码可以参考[这里](https://github.com/LiushuiXiaoxia/AndroidModular/tree/master/ImplementsTransformPlugin) 743 | 744 | 745 | ### 映射匹配总结 746 | 747 | **优点:** 748 | 749 | * Map:简单明了,很容易入手,不会对编译时间产生任何影响,不会随着Gradle版本的升级而受影响,代码混淆时候不会有影响,无需配置混淆文件。 750 | * APT:使用简单,使用注解配置,代码优雅,原理是用代码生成的方式生成新的文件。 751 | * Transform:使用简单,使用注解配置,代码优雅,原理是用代码生成的方式生成新的文件,不过生成的文件的时期和APT不同,会编译时间产生少许影响。 752 | 753 | **缺点:** 754 | 755 | * Map:在需要新添加映射的时候,需要手动添加,不然不会生效,代码不优雅。 756 | * APT:在编译时期生成文件,会编译时间产生少许影响,同时在不同的Gradle的版本中可能会产生错误或者兼容问题。需要配置混淆设置,不然会丢失文件。技术实现复杂,较难维护。 757 | * Transform:在编译时期生成文件,会编译时间产生少许影响,同时在不同的Gradle的版本中可能会产生错误或者兼容问题。需要配置混淆设置,不然会丢失文件。技术实现复杂,较难维护。 758 | 759 | 从技术复杂性以及维护性来看,Map > APT = Transform 760 | 761 | 从使用复杂性以及代码优雅性来看,Transform > APT > Map 762 | 763 | ## 开发调试技巧 764 | 765 | ### Debug 766 | 767 | 上面介绍了很多关于模块化的概念以及技术难题,当模块化完成以后,再进行完成开发时候还是会遇到不少问题。不如原先代码在一起的时候很方便的进行代码调试。但是进行模块化以后,直接使用的是aar依赖,不能直接修改代码,可以使用下面技巧,可以直接进行代码调试。 768 | 769 | 770 | 在根目录下面创建一个module目录以及module.gradle文件,这个目录和文件是git ignore的,然后把对应的模块代码clone到里面,根目录的setting.gradlew apply module.gradle文件,如下所示,如果需要源码调试,则在module中添加对应的模块。然后在app的依赖中去掉aar依赖,同时添加项目依赖即可。当不需要源码调试好,再修改为到原先代码即可。 771 | 772 | ```gradle 773 | try { 774 | apply from: "./module.gradle" 775 | } catch (e) { 776 | } 777 | ``` 778 | 779 | module.gradle 780 | 781 | ```gradle 782 | include ':ModuleShopping' 783 | ``` 784 | 785 | 比如调试shopping模块 786 | 787 | ```gradle 788 | // common 789 | compile 'cn.mycommons:base:1.0.0' 790 | compile 'cn.mycommons:component:1.0.0' 791 | compile 'cn.mycommons:service:1.0.0' 792 | 793 | // biz 794 | compile 'cn.mycommons:user:1.0.0' 795 | compile 'cn.mycommons:order:1.0.0' 796 | // compile 'cn.mycommons:shopping:1.0.0' 797 | compile project(':ModuleShopping') 798 | 799 | ``` 800 | 801 | 当然还有个更具技术挑战性方案,使用gradle插件的形式,如果发现root项目中包含的模块化的源码,则不适用aar依赖,直接使用源码依赖,当然这个想法是不错的,不过具有技术挑战性,同时有可能随着Gradle版本的升级,编写的gradle插件也要做相对于的兼容风险,这是只是简单提示一下。 802 | 803 | ### 容器设计 804 | 805 | 上面讲到的如果要调试代码时候,需要完整的运行的整个项目,随着项目的增大,编译时间可能变得很长。 806 | 807 | 我们可以做一个简单的,类似与主app模块一样,比如我是负责user模块的开发者,那么我只要调试我这个模块就行了,如果需要其他的模块,我可以简单的做一个mock,不是把其他的模块直接依赖过来,这样可以做到调试作用。等到再需要完整项目调试时候,我们在使用上面介绍的方式,这样可以节省不少开发时间。 808 | 809 | 还有一种实现调试的方式,比如上面的user模块,目录下面的build.gradle文件是这样的 810 | 811 | ```gradle 812 | apply plugin: 'com.android.library' 813 | 814 | xxx 815 | xxx 816 | ``` 817 | 818 | 我们可以在gradle.properties中设置编译变参数isLibModule,当需要完整调试好,设置为`isLibModule=false`,这样我这个子模块就是一个`apply plugin: 'com.android.application'`这样的模块,是可以单独运行的一个项目 819 | 820 | ``` 821 | try { 822 | if (isLibModule) { 823 | apply from: "./build_lib.gradle" 824 | }else{ 825 | apply from: "./build_app.gradle" 826 | } 827 | } catch (e) { 828 | } 829 | ``` 830 | 831 | 可能有时候还是需要单独的运行环境,android编译方式有2中,一种是debug,一种是release。当打包成aar的时候,使用的是release方式,我们可以把需要调试的代码全部放到debug中,这样打包的时候就不会把调试的文件发布到aar中。不过这种实现方式,需要对Android项目的目录有较高的认识,才可以熟练使用。 832 | 833 | ### CI 834 | 835 | 上面介绍的各个模块需要单独到独立的git仓库,同时打包到单独的maven仓库,当开发完成后,这时候就需要进行打包,但这个是一个简单和重复的事情,所以我们需要一个工具来完成这些事情,我们可以利用CI系统来搞定这件事情,这里我推荐Jenkins,主流厂商使用jenkins作为CI服务器这个方案。 836 | 837 | 具体的步骤就是,需要对每个模块的git仓库做web hook,我们公司使用的是git lab,可以对git的各种操作做hook,比如push,merge,tag等。 838 | 839 | 当代码发送了变化了,我们可以发送事件到CI服务器,CI服务器再对各个事件做处理,比如user模块develop分支有代码变化,这个变化可能是merge,也有可能是push。我们可以把主项目代码和user项目的代码单独clone下拉,然后编译一下,确认是否有编译问题,如果有编译通过,那么在使用相关gradle命令发布到maven仓库中。 840 | 841 | 不管每次编译结果怎样,是成功还是失败,我们都应该把结果回馈给开发者,常见的方式是邮件,不过这个信息邮件方式可能很频繁,我们建议使用slack。 842 | 843 | ## 总结 844 | 845 | 模块化架构主要思路就是分而治之,把依赖整理清楚,减少代码冗余和耦合,在把代码抽取到各自的模块后,了解各个模块的通信方式,以及可能发生的问题,规避问题或者解决问题。最后为了开发和调试方便,开发一些周边工具,帮助开发更好的完成任务。 846 | 847 | ## 相关资料 848 | 849 | [天猫手机解耦之路](http://mobile.51cto.com/app-show-523490.htm) 850 | 851 | [iOS 模块化BeeHive](https://github.com/alibaba/BeeHive) 852 | 853 | [蘑菇街 App 的组件化之路](http://limboy.me/tech/2016/03/10/mgj-components.html) 854 | 855 | [蘑菇街 App 的组件化之路·续](http://limboy.me/tech/2016/03/14/mgj-components-continued.html) 856 | 857 | [iOS应用架构谈 组件化方案](https://casatwy.com/iOS-Modulization.html) 858 | 859 | [javassist资料](http://jboss-javassist.github.io/javassist/) 860 | 861 | [iOS组件化方案调研](http://www.jianshu.com/p/34f23b694412) 862 | 863 | [安居客Android项目架构演进](http://www.cnblogs.com/baronzhang/p/6442047.html) 864 | 865 | [关于Android模块化我有一些话不知当讲不当讲](https://github.com/LiushuiXiaoxia/AndroidModular/) -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | maven { url 'https://maven.aliyun.com/repository/public/' } 7 | google() 8 | mavenCentral() 9 | mavenLocal() 10 | maven { url "https://jitpack.io" } 11 | // maven { url uri("${project.rootDir.absolutePath}/repo") } 12 | } 13 | 14 | dependencies { 15 | classpath libs.plugins.androidBuild.get().toString() 16 | classpath libs.plugins.kotlinBuild.get().toString() 17 | // classpath "com.android.tools.build:gradle:$gradle_version" 18 | // classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 19 | // classpath "cn.mycommons:ImplementsTransformPlugin:$LIB_VERSION" 20 | 21 | // classpath 'com.android.tools:r8:4.0.48' 22 | // classpath 'com.android.tools:r8:8.0.40' 23 | 24 | if (Boolean.parseBoolean(PLUGIN_DEV)) { 25 | classpath "cn.mycommons:module-plugin" 26 | } 27 | } 28 | } 29 | 30 | allprojects { 31 | repositories { 32 | maven { url 'https://maven.aliyun.com/repository/public/' } 33 | google() 34 | mavenLocal() 35 | mavenCentral() 36 | maven { url "https://jitpack.io" } 37 | // maven { url uri("${project.rootDir.absolutePath}/repo") } 38 | } 39 | } 40 | 41 | gradle.addListener(new TaskActionListener() { 42 | 43 | @Override 44 | void beforeActions(Task task) { 45 | def name = task.name 46 | if (name.startsWith("dexBuilder")) { 47 | for (def f in task.inputs.files.files) { 48 | logger.quiet("${name}.inputs: $f") 49 | } 50 | } 51 | } 52 | 53 | @Override 54 | void afterActions(Task task) { 55 | def name = task.name 56 | if (name.startsWith("dexBuilder")) { 57 | for (def f in task.outputs.files.files) { 58 | logger.quiet("${name}.outputs: $f") 59 | } 60 | } 61 | } 62 | }) 63 | 64 | task clean(type: Delete) { 65 | delete rootProject.buildDir 66 | } 67 | 68 | if (hasProperty('buildScan')) { 69 | buildScan { 70 | termsOfServiceUrl = 'https://gradle.com/terms-of-service' 71 | termsOfServiceAgree = 'yes' 72 | } 73 | } -------------------------------------------------------------------------------- /build.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.0.0" 3 | androidAgp = "8.2.0" 4 | minSdkVersion = "21" 5 | compileSdkVersion = "34" 6 | targetSdkVersion = "33" 7 | 8 | [libraries] 9 | kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } 10 | gson = "com.google.code.gson:gson:2.10.1" 11 | 12 | androidx-coreKtx = 'androidx.core:core-ktx:1.12.0' 13 | androidx-appcompat = 'androidx.appcompat:appcompat:1.6.1' 14 | androidx-material = 'com.google.android.material:material:1.10.0' 15 | androidx-constraintlayout = 'androidx.constraintlayout:constraintlayout:2.1.4' 16 | 17 | okhttp = "com.squareup.okhttp3:okhttp:4.11.0" 18 | okhttp-logging = "com.squareup.okhttp3:logging-interceptor:4.9.3" 19 | leakcanary = 'com.squareup.leakcanary:leakcanary-android:2.9.1' 20 | 21 | # for test 22 | junit = 'junit:junit:4.13.2' 23 | androidx-junit = 'androidx.test.ext:junit:1.1.5' 24 | androidx-espresso = 'androidx.test.espresso:espresso-core:3.5.1' 25 | 26 | [bundles] 27 | androidx = ['androidx-coreKtx', 'androidx-appcompat', 'androidx-material', 'androidx-constraintlayout'] 28 | okhttp = ['okhttp', 'okhttp-logging'] 29 | 30 | # for test 31 | test = ['androidx-junit', 'androidx-espresso'] 32 | 33 | [plugins] 34 | kotlinBuild = { id = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 35 | androidBuild = { id = "com.android.tools.build:gradle", version.ref = "androidAgp" } -------------------------------------------------------------------------------- /buildDebug.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | set -v on 4 | 5 | ./gradlew clean 6 | #git clean -xdf 7 | ./gradlew --stop 8 | 9 | set -e 10 | 11 | ./gradlew :test-plugin:testpluginapp:assembledebug --profile --no-daemon --no-build-cache -s 12 | #./gradlew :TestPluginApp:assembleDebug --profile --no-daemon --no-build-cache -s -w -Dorg.gradle.debug=true 13 | 14 | #exit 15 | adb uninstall cn.mycommons.testplugin || echo "" 16 | adb install -r test-plugin/testpluginapp/build/outputs/apk/debug/testpluginapp-debug.apk 17 | sleep 1 18 | adb shell am start -n cn.mycommons.testplugin/.MainActivity 19 | -------------------------------------------------------------------------------- /buildHub.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | adb uninstall 'cn.mycommons.testplugin' 4 | adb uninstall 'cn.mycommons.androidmodular' 5 | 6 | hub -f buildHub.yaml --rebuild --log normal --save-in-project --jdk 17 -------------------------------------------------------------------------------- /buildHub.yaml: -------------------------------------------------------------------------------- 1 | hub: 2 | env: 3 | hello: world 4 | before: 5 | - ./gradlew --stop 6 | - git clean -xdf 7 | # build: assembleDebug --no-daemon --no-build-cache --scan -s -w 8 | # build: assembleRelease --no-daemon --no-build-cache --scan -s -w 9 | build: assemble --no-daemon --no-build-cache --scan -s -w 10 | artifacts: 11 | xbuild: test-plugin/test-plugin-app/build/outputs 12 | # magicMirror: true -------------------------------------------------------------------------------- /buildModule.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | set -v on 4 | 5 | ./gradlew clean 6 | #git clean -xdf 7 | ./gradlew --stop 8 | 9 | set -e 10 | 11 | ./gradlew :test-module:app:assembleDebug --profile --no-daemon --no-build-cache 12 | #./gradlew :TestPluginApp:assembleDebug --profile --no-daemon --no-build-cache -s -w -Dorg.gradle.debug=true 13 | 14 | #exit 15 | adb uninstall cn.mycommons.androidmodular || echo "" 16 | adb install -r test-module/app/build/outputs/apk/debug/app-debug.apk 17 | sleep 1 18 | adb shell am start -n cn.mycommons.androidmodular/.ui.MainActivity -------------------------------------------------------------------------------- /common/implements-process/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /common/implements-process/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | 3 | sourceCompatibility = "11" 4 | targetCompatibility = "11" 5 | 6 | dependencies { 7 | implementation fileTree(dir: 'libs', include: ['*.jar']) 8 | 9 | // compile project(':java-base') 10 | implementation "cn.mycommons:java-base" 11 | } -------------------------------------------------------------------------------- /common/implements-process/src/main/java/com/example/RouterProcess.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | // 占坑用的 4 | public class RouterProcess { 5 | } -------------------------------------------------------------------------------- /common/java-base/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /common/java-base/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'java-library' 3 | apply plugin: 'kotlin' 4 | 5 | buildscript { 6 | ext.kotlin_version = '2.0.0' 7 | repositories { 8 | maven { url 'https://maven.aliyun.com/repository/public/' } 9 | mavenLocal() 10 | google() 11 | mavenCentral() 12 | } 13 | 14 | dependencies { 15 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 16 | } 17 | } 18 | 19 | repositories { 20 | maven { url 'https://maven.aliyun.com/repository/public/' } 21 | mavenLocal() 22 | google() 23 | mavenCentral() 24 | } 25 | 26 | sourceCompatibility = "11" 27 | targetCompatibility = "11" 28 | 29 | compileKotlin { 30 | kotlinOptions { 31 | jvmTarget = "11" 32 | } 33 | } 34 | 35 | //compileTestKotlin { 36 | // kotlinOptions { 37 | // jvmTarget = "11" 38 | // } 39 | //} 40 | 41 | group "cn.mycommons" 42 | 43 | dependencies { 44 | implementation fileTree(dir: 'libs', include: ['*.jar']) 45 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 46 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 47 | } -------------------------------------------------------------------------------- /common/java-base/gradle: -------------------------------------------------------------------------------- 1 | ../../gradle -------------------------------------------------------------------------------- /common/java-base/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /common/java-base/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /common/java-base/settings.gradle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiushuiXiaoxia/AndroidModular/db93875b0657bac65d9733531b6bad3cd6d49fbe/common/java-base/settings.gradle -------------------------------------------------------------------------------- /common/java-base/src/main/java/cn/mycommons/modulebase/annotations/Implements.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.modulebase.annotations 2 | 3 | import kotlin.reflect.KClass 4 | 5 | /** 6 | * Router

7 | * Created by xiaqiulei on 2017-05-14. 8 | */ 9 | @Retention(AnnotationRetention.RUNTIME) 10 | @Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) 11 | annotation class Implements( 12 | val parent: KClass<*>, 13 | val single: Boolean = true, 14 | ) -------------------------------------------------------------------------------- /common/java-base/src/main/java/cn/mycommons/modulebase/annotations/ImplementsManager.java: -------------------------------------------------------------------------------- 1 | package cn.mycommons.modulebase.annotations; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * ImplementsManager
8 | * Created by xiaqiulei on 2017-05-17. 9 | */ 10 | public class ImplementsManager { 11 | 12 | private static final Map CONFIG = new HashMap<>(); 13 | 14 | public static Class getImplementsClass(Class parent) { 15 | return CONFIG.get(parent); 16 | } 17 | } -------------------------------------------------------------------------------- /common/java-base/src/main/java/cn/mycommons/modulebase/annotations/Router.java: -------------------------------------------------------------------------------- 1 | package cn.mycommons.modulebase.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Router
10 | * Created by xiaqiulei on 2017-05-14. 11 | */ 12 | @Target(ElementType.TYPE) 13 | @Retention(RetentionPolicy.CLASS) 14 | public @interface Router { 15 | 16 | String uri(); 17 | } -------------------------------------------------------------------------------- /common/java-base/src/main/java/cn/mycommons/modulebase/annotations/RouterParam.java: -------------------------------------------------------------------------------- 1 | package cn.mycommons.modulebase.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Router
10 | * Created by xiaqiulei on 2017-05-14. 11 | */ 12 | @Target(ElementType.FIELD) 13 | @Retention(RetentionPolicy.CLASS) 14 | public @interface RouterParam { 15 | 16 | String name(); 17 | } -------------------------------------------------------------------------------- /doc/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiushuiXiaoxia/AndroidModular/db93875b0657bac65d9733531b6bad3cd6d49fbe/doc/1.png -------------------------------------------------------------------------------- /doc/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiushuiXiaoxia/AndroidModular/db93875b0657bac65d9733531b6bad3cd6d49fbe/doc/2.png -------------------------------------------------------------------------------- /doc/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiushuiXiaoxia/AndroidModular/db93875b0657bac65d9733531b6bad3cd6d49fbe/doc/3.png -------------------------------------------------------------------------------- /doc/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiushuiXiaoxia/AndroidModular/db93875b0657bac65d9733531b6bad3cd6d49fbe/doc/4.png -------------------------------------------------------------------------------- /doc/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiushuiXiaoxia/AndroidModular/db93875b0657bac65d9733531b6bad3cd6d49fbe/doc/5.png -------------------------------------------------------------------------------- /doc/未命名.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiushuiXiaoxia/AndroidModular/db93875b0657bac65d9733531b6bad3cd6d49fbe/doc/未命名.key -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | # When configured, Gradle will run in incubating parallel mode. 15 | # This option should only be used with decoupled projects. More details, visit 16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 17 | # org.gradle.parallel=true 18 | 19 | # android.enableBuildCache=false 20 | android.useAndroidX=true 21 | 22 | LIB_VERSION=0.4 23 | GRADLE_VERSION=3.1.4 24 | 25 | PLUGIN_DEV=true 26 | #PLUGIN_DEV=false 27 | 28 | # gen buildconfig 29 | android.defaults.buildfeatures.buildconfig=true 30 | 31 | # k2 32 | kotlin.experimental.tryK2=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiushuiXiaoxia/AndroidModular/db93875b0657bac65d9733531b6bad3cd6d49fbe/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip 4 | #distributionUrl=http\://gradle.bilibili.co/gradle/gradle-8.4-all.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /maven.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven-publish' 2 | 3 | group = 'cn.mycommons' 4 | version = "$LIB_VERSION" 5 | 6 | //uploadArchives { 7 | // repositories { 8 | // mavenDeployer { 9 | // repository(url: uri("${project.rootDir.absolutePath}/repo")) 10 | // } 11 | // } 12 | //} 13 | 14 | publishing { 15 | repositories { 16 | mavenLocal { 17 | // url = "${project.rootDir.absolutePath}/repo" 18 | } 19 | } 20 | publications { 21 | maven(MavenPublication) { 22 | from components.java 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /module.gradle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiushuiXiaoxia/AndroidModular/db93875b0657bac65d9733531b6bad3cd6d49fbe/module.gradle -------------------------------------------------------------------------------- /plugin/module-plugin-ksp/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /plugin/module-plugin-ksp/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | apply plugin: 'kotlin' 3 | 4 | buildscript { 5 | ext.kotlin_version = '2.0.0' 6 | repositories { 7 | maven { url 'https://maven.aliyun.com/repository/public/' } 8 | mavenLocal() 9 | google() 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 15 | } 16 | } 17 | 18 | repositories { 19 | maven { url 'https://maven.aliyun.com/repository/public/' } 20 | mavenLocal() 21 | google() 22 | mavenCentral() 23 | } 24 | 25 | sourceCompatibility = "11" 26 | targetCompatibility = "11" 27 | 28 | compileKotlin { 29 | kotlinOptions { 30 | jvmTarget = "11" 31 | } 32 | } 33 | 34 | compileTestKotlin { 35 | kotlinOptions { 36 | jvmTarget = "11" 37 | } 38 | } 39 | 40 | group "cn.mycommons" 41 | 42 | dependencies { 43 | implementation fileTree(dir: 'libs', include: ['*.jar']) 44 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 45 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 46 | 47 | implementation "cn.mycommons:java-base" 48 | 49 | implementation "com.google.code.gson:gson:2.10.1" 50 | implementation "com.google.devtools.ksp:symbol-processing-api:2.0.0-1.0.22" 51 | implementation 'com.squareup:kotlinpoet:1.14.2' 52 | } -------------------------------------------------------------------------------- /plugin/module-plugin-ksp/gradle: -------------------------------------------------------------------------------- 1 | ../../gradle -------------------------------------------------------------------------------- /plugin/module-plugin-ksp/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /plugin/module-plugin-ksp/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /plugin/module-plugin-ksp/settings.gradle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiushuiXiaoxia/AndroidModular/db93875b0657bac65d9733531b6bad3cd6d49fbe/plugin/module-plugin-ksp/settings.gradle -------------------------------------------------------------------------------- /plugin/module-plugin-ksp/src/main/kotlin/cn/mycommons/module_plugin/ksp/KspConsts.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.module_plugin.ksp 2 | 3 | object KspConsts { 4 | 5 | const val PLUGIN_NAME = "ModuleKspPlugin" 6 | const val INTERNAL_PKG = "cn.mycommons.module_plugin" 7 | 8 | const val MODULE_PACKAGE = "modulePackage" 9 | const val MODULE_NAME = "moduleName" 10 | } -------------------------------------------------------------------------------- /plugin/module-plugin-ksp/src/main/kotlin/cn/mycommons/module_plugin/ksp/ModuleProcessorProvider.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.module_plugin.ksp 2 | 3 | import cn.mycommons.module_plugin.ksp.util.LogKit 4 | import com.google.devtools.ksp.processing.SymbolProcessor 5 | import com.google.devtools.ksp.processing.SymbolProcessorEnvironment 6 | import com.google.devtools.ksp.processing.SymbolProcessorProvider 7 | 8 | class ModuleProcessorProvider : SymbolProcessorProvider { 9 | 10 | override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { 11 | LogKit.setup(environment.logger) 12 | LogKit.warn("options = ${environment.options}") 13 | 14 | val pkg = environment.options[KspConsts.MODULE_PACKAGE] 15 | val name = environment.options[KspConsts.MODULE_NAME] 16 | 17 | if (pkg.isNullOrBlank() || name.isNullOrBlank()) { 18 | throw RuntimeException("project must be assign moduleName and modulePackage") 19 | } 20 | 21 | PluginContextKit.setup(pkg, name) 22 | 23 | return ModuleSymbolProcessor( 24 | environment.codeGenerator 25 | ) 26 | } 27 | } -------------------------------------------------------------------------------- /plugin/module-plugin-ksp/src/main/kotlin/cn/mycommons/module_plugin/ksp/ModuleSymbolProcessor.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.module_plugin.ksp 2 | 3 | import cn.mycommons.module_plugin.ksp.process.ModuleConfigProcess 4 | import cn.mycommons.module_plugin.ksp.process.RouterParamProcess 5 | import cn.mycommons.module_plugin.ksp.util.LogKit 6 | import cn.mycommons.modulebase.annotations.Implements 7 | import cn.mycommons.modulebase.annotations.Router 8 | import cn.mycommons.modulebase.annotations.RouterParam 9 | import com.google.devtools.ksp.processing.CodeGenerator 10 | import com.google.devtools.ksp.processing.Dependencies 11 | import com.google.devtools.ksp.processing.Resolver 12 | import com.google.devtools.ksp.processing.SymbolProcessor 13 | import com.google.devtools.ksp.symbol.KSAnnotated 14 | import com.google.devtools.ksp.symbol.KSClassDeclaration 15 | import com.google.devtools.ksp.validate 16 | import com.google.gson.GsonBuilder 17 | import java.util.concurrent.atomic.AtomicInteger 18 | 19 | class ModuleSymbolProcessor(private val codeGenerator: CodeGenerator) : SymbolProcessor { 20 | 21 | private val idx = AtomicInteger(0) 22 | 23 | override fun process(resolver: Resolver): List { 24 | LogKit.warn("~~~ ${KspConsts.PLUGIN_NAME} process(${idx.getAndIncrement()}) ~~~") 25 | 26 | resolver.getAllFiles().forEach { 27 | LogKit.warn("allFiles: ${it.fileName}") 28 | } 29 | resolver.getNewFiles().forEach { 30 | LogKit.warn("newFiles: ${it.fileName}") 31 | } 32 | 33 | val routerList = resolver.getSymbolsWithAnnotation(Router::class.java.name) 34 | .filterIsInstance() 35 | .toList() 36 | 37 | val serviceList = resolver.getSymbolsWithAnnotation(Implements::class.java.name) 38 | .filterIsInstance() 39 | .toList() 40 | PluginContextKit.saveModuleConfig(routerList, serviceList) 41 | 42 | val routerParams = resolver.getSymbolsWithAnnotation(RouterParam::class.java.name) 43 | .map { it.parent } 44 | .filter { it != null && it is KSClassDeclaration } 45 | .map { it as KSClassDeclaration } 46 | .distinct() 47 | .toList() 48 | PluginContextKit.saveRouterParam(routerParams) 49 | 50 | return listOf() 51 | } 52 | 53 | override fun finish() { 54 | genCode() 55 | 56 | LogKit.warn("~~~ ${KspConsts.PLUGIN_NAME} finish ~~~") 57 | } 58 | 59 | private fun genCode() { 60 | genRes() 61 | PluginContextKit.apply { 62 | RouterParamProcess(codeGenerator).process(routerParams) 63 | ModuleConfigProcess(codeGenerator).process(routerList, serviceList) 64 | } 65 | 66 | codeGenerator.generatedFile.forEach { 67 | LogKit.warn("generatedFile = $it") 68 | } 69 | } 70 | 71 | private fun genRes() { 72 | val fs = codeGenerator.createNewFileByPath( 73 | Dependencies(false), 74 | "META-INF/module/${PluginContextKit.moduleName}.meta", 75 | "json" 76 | ) 77 | val map = mutableMapOf("hello" to "world") 78 | fs.bufferedWriter().use { 79 | it.write(GsonBuilder().setPrettyPrinting().create().toJson(map)) 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /plugin/module-plugin-ksp/src/main/kotlin/cn/mycommons/module_plugin/ksp/PluginContextKit.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.module_plugin.ksp 2 | 3 | import com.google.devtools.ksp.symbol.KSClassDeclaration 4 | 5 | object PluginContextKit { 6 | 7 | lateinit var modulePackage: String 8 | lateinit var moduleName: String 9 | 10 | val routerList: MutableList = mutableListOf() 11 | val serviceList: MutableList = mutableListOf() 12 | 13 | val routerParams: MutableList = mutableListOf() 14 | 15 | fun setup(pkg: String, name: String) { 16 | this.modulePackage = pkg 17 | this.moduleName = name 18 | } 19 | 20 | fun saveModuleConfig(routers: List, services: List) { 21 | routerList.addAll(routers) 22 | serviceList.addAll(services) 23 | } 24 | 25 | fun saveRouterParam(symbols: List) { 26 | routerParams.addAll(symbols) 27 | } 28 | } -------------------------------------------------------------------------------- /plugin/module-plugin-ksp/src/main/kotlin/cn/mycommons/module_plugin/ksp/model/model.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.module_plugin.ksp.model 2 | 3 | import cn.mycommons.modulebase.annotations.RouterParam 4 | import com.google.devtools.ksp.symbol.KSClassDeclaration 5 | import com.google.devtools.ksp.symbol.KSPropertyDeclaration 6 | import com.google.devtools.ksp.symbol.KSType 7 | 8 | 9 | data class RouterConfig( 10 | val uri: String, 11 | val clazzPackage: String, 12 | val clazzName: String, 13 | ) { 14 | 15 | override fun toString(): String { 16 | return "RouterConfig(uri='$uri', clazzPackage='$clazzPackage', clazzName='$clazzName')" 17 | } 18 | } 19 | 20 | data class ServiceConfig( 21 | val self: KSClassDeclaration, 22 | val parent: KSType, 23 | ) { 24 | 25 | override fun toString(): String { 26 | return "ServiceConfig(clazz=$self, annotation=${parent})" 27 | } 28 | } 29 | 30 | data class RouterParamConfig( 31 | val field: KSPropertyDeclaration, 32 | val annotation: RouterParam, 33 | ) { 34 | 35 | override fun toString(): String { 36 | return "RouterParamConfig(field=$field, annotation=$annotation)" 37 | } 38 | } -------------------------------------------------------------------------------- /plugin/module-plugin-ksp/src/main/kotlin/cn/mycommons/module_plugin/ksp/process/ModuleConfigProcess.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.module_plugin.ksp.process 2 | 3 | import cn.mycommons.module_plugin.ksp.KspConsts 4 | import cn.mycommons.module_plugin.ksp.PluginContextKit 5 | import cn.mycommons.module_plugin.ksp.model.RouterConfig 6 | import cn.mycommons.module_plugin.ksp.model.ServiceConfig 7 | import cn.mycommons.module_plugin.ksp.util.LogKit 8 | import cn.mycommons.modulebase.annotations.Implements 9 | import cn.mycommons.modulebase.annotations.Router 10 | import com.google.devtools.ksp.KspExperimental 11 | import com.google.devtools.ksp.getAnnotationsByType 12 | import com.google.devtools.ksp.processing.CodeGenerator 13 | import com.google.devtools.ksp.processing.Dependencies 14 | import com.google.devtools.ksp.symbol.KSClassDeclaration 15 | import com.google.devtools.ksp.symbol.KSType 16 | import com.squareup.kotlinpoet.FileSpec 17 | import com.squareup.kotlinpoet.FunSpec 18 | import com.squareup.kotlinpoet.TypeSpec 19 | import com.squareup.kotlinpoet.typeNameOf 20 | 21 | class ModuleConfigProcess(private val codeGenerator: CodeGenerator) { 22 | 23 | @OptIn(KspExperimental::class) 24 | fun process(routers: List, services: List) { 25 | val routerList = mutableListOf() 26 | routers.forEach { 27 | val an = it.getAnnotationsByType(Router::class).firstOrNull() 28 | if (an != null) { 29 | val config = RouterConfig(an.uri, it.packageName.asString(), it.simpleName.asString()) 30 | routerList.add(config) 31 | } 32 | } 33 | 34 | val serviceList = mutableListOf() 35 | services.forEach { 36 | val an = it.filterAnnotationsByType(Implements::class).firstOrNull() 37 | if (an != null) { 38 | val parent = an.getArgument(Implements::parent.name) 39 | if (parent != null && parent is KSType) { 40 | val config = ServiceConfig(it, parent) 41 | serviceList.add(config) 42 | } 43 | } 44 | } 45 | 46 | genRouterConfig(routerList, serviceList) 47 | } 48 | 49 | private fun genRouterConfig( 50 | routerList: MutableList, 51 | serviceList: MutableList, 52 | ) { 53 | val genClassName = "${PluginContextKit.moduleName}__ModuleConfigGen" 54 | val genPackageName = "${KspConsts.INTERNAL_PKG}.gen" 55 | 56 | val os = codeGenerator.createNewFile(Dependencies(false), genPackageName, genClassName) 57 | val fs = FileSpec.builder(genPackageName, genClassName) 58 | .addType( 59 | TypeSpec.classBuilder(genClassName) 60 | .addFunction(genMethodRouter(routerList)) 61 | .addFunction(genMethodService(serviceList)) 62 | .addKdoc("generate by router ksp") 63 | .build() 64 | ).apply { 65 | routerList.forEach { addImport(it.clazzPackage, it.clazzName) } 66 | }.build() 67 | 68 | os.bufferedWriter().use { fs.writeTo(it) } 69 | } 70 | 71 | @OptIn(ExperimentalStdlibApi::class) 72 | private fun genMethodRouter(list: List): FunSpec { 73 | val lines = mutableListOf() 74 | lines.add("val map = HashMap>()") 75 | list.forEach { 76 | val item = """ 77 | map["${it.uri}"] = ${it.clazzPackage}.${it.clazzName}::class.java 78 | """.trimIndent() 79 | lines.add(item) 80 | } 81 | lines.add("return map") 82 | 83 | return FunSpec.builder("router") 84 | .addKdoc("router config") 85 | .addCode(lines.joinToString("\n")) 86 | .returns(typeNameOf>>()) 87 | .build() 88 | } 89 | 90 | @OptIn(ExperimentalStdlibApi::class) 91 | private fun genMethodService(list: List): FunSpec { 92 | val lines = mutableListOf() 93 | lines.add("val map = HashMap, Class<*>>()") 94 | list.forEach { 95 | val parent = 96 | "${it.parent.declaration.packageName.asString()}.${it.parent.declaration.simpleName.asString()}" 97 | val self = "${it.self.packageName.asString()}.${it.self.simpleName.asString()}" 98 | 99 | val item = """ 100 | map[${parent}::class.java] = ${self}::class.java 101 | """.trimIndent() 102 | lines.add(item) 103 | } 104 | lines.add("return map") 105 | 106 | return FunSpec.builder("service") 107 | .addKdoc("module config") 108 | .addCode(lines.joinToString("\n")) 109 | .returns(typeNameOf, Class<*>>>()) 110 | .build() 111 | } 112 | } -------------------------------------------------------------------------------- /plugin/module-plugin-ksp/src/main/kotlin/cn/mycommons/module_plugin/ksp/process/RouterParamKSVisitorVoid.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.module_plugin.ksp.process 2 | 3 | import cn.mycommons.module_plugin.ksp.util.LogKit 4 | import com.google.devtools.ksp.symbol.KSClassDeclaration 5 | import com.google.devtools.ksp.symbol.KSFile 6 | import com.google.devtools.ksp.symbol.KSVisitorVoid 7 | 8 | class RouterParamKSVisitorVoid : KSVisitorVoid() { 9 | 10 | override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) { 11 | LogKit.warn("visitClassDeclaration: ${classDeclaration.simpleName.asString()}") 12 | } 13 | 14 | override fun visitFile(file: KSFile, data: Unit) { 15 | LogKit.warn("visitFile: ${file}") 16 | } 17 | } -------------------------------------------------------------------------------- /plugin/module-plugin-ksp/src/main/kotlin/cn/mycommons/module_plugin/ksp/process/RouterParamProcess.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.module_plugin.ksp.process 2 | 3 | import cn.mycommons.module_plugin.ksp.model.RouterParamConfig 4 | import cn.mycommons.module_plugin.ksp.util.LogKit 5 | import cn.mycommons.modulebase.annotations.RouterParam 6 | import com.google.devtools.ksp.KspExperimental 7 | import com.google.devtools.ksp.getAnnotationsByType 8 | import com.google.devtools.ksp.processing.CodeGenerator 9 | import com.google.devtools.ksp.processing.Dependencies 10 | import com.google.devtools.ksp.symbol.KSClassDeclaration 11 | import com.squareup.kotlinpoet.ClassName 12 | import com.squareup.kotlinpoet.FileSpec 13 | import com.squareup.kotlinpoet.FunSpec 14 | import com.squareup.kotlinpoet.TypeSpec 15 | 16 | class RouterParamProcess(private val codeGenerator: CodeGenerator) { 17 | 18 | @OptIn(KspExperimental::class) 19 | fun process(symbols: List) { 20 | if (symbols.isEmpty()) { 21 | return 22 | } 23 | symbols.forEach { 24 | val fields = it.getAllProperties() 25 | .filter { p -> 26 | // LogKit.warn("p = $p, pa = ${p.annotations.toList()}") 27 | // p.annotations.any { a -> 28 | // a.shortName.asString() == RouterParam::class.java.simpleName 29 | // } 30 | p.getAnnotationsByType(RouterParam::class).toList().isNotEmpty() 31 | } 32 | .map { p -> 33 | val param = p.getAnnotationsByType(RouterParam::class).first() 34 | // val ann = p.annotations.first { a -> a.shortName.asString() == RouterParam::class.java.simpleName } 35 | RouterParamConfig(p, param) 36 | } 37 | .toList() 38 | 39 | LogKit.warn("class: $it, fields = $fields") 40 | genRouterParamInject(it, fields) 41 | } 42 | } 43 | 44 | private fun genRouterParamInject( 45 | clazz: KSClassDeclaration, 46 | fields: List, 47 | ) { 48 | val genClassName = "${clazz.simpleName.asString()}__RouterInject" 49 | val genPackageName = clazz.packageName.asString() 50 | val t = ClassName(clazz.packageName.asString(), clazz.simpleName.asString()) 51 | 52 | val os = codeGenerator.createNewFile(Dependencies(false), genPackageName, genClassName) 53 | val fs = FileSpec.builder(genPackageName, genClassName) 54 | .addType( 55 | TypeSpec.objectBuilder(genClassName) 56 | .addFunction( 57 | FunSpec.builder("inject") 58 | .addParameter("obj", t) 59 | .addCode(genConfigMethodBody(fields)) 60 | .addAnnotation(JvmStatic::class) 61 | .build() 62 | ) 63 | .addFunction( 64 | FunSpec.builder("uninject") 65 | .addParameter("obj", t) 66 | .addCode("") 67 | .addAnnotation(JvmStatic::class) 68 | .build() 69 | ) 70 | .addKdoc("generate by router ksp") 71 | .build() 72 | ).build() 73 | 74 | os.bufferedWriter().use { fs.writeTo(it) } 75 | } 76 | 77 | private fun genConfigMethodBody(list: List): String { 78 | val sb = StringBuilder() 79 | // config["${it.uri}"] = ${it.clazzPackage}.${it.clazzName}::class.java 80 | list.forEach { 81 | val item = """ 82 | obj.${it.field.simpleName.asString()} = obj.intent.getStringExtra("${it.annotation.name}") 83 | """.trimIndent() 84 | sb.append(item).append("\n") 85 | } 86 | return sb.toString() 87 | } 88 | } -------------------------------------------------------------------------------- /plugin/module-plugin-ksp/src/main/kotlin/cn/mycommons/module_plugin/ksp/process/ksp.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.module_plugin.ksp.process 2 | 3 | import com.google.devtools.ksp.KspExperimental 4 | import com.google.devtools.ksp.symbol.KSAnnotated 5 | import com.google.devtools.ksp.symbol.KSAnnotation 6 | import com.google.devtools.ksp.symbol.KSValueArgument 7 | import kotlin.reflect.KClass 8 | 9 | @KspExperimental 10 | fun KSAnnotated.filterAnnotationsByType(kClass: KClass): Sequence { 11 | return annotations.filter { 12 | it.annotationType.resolve().declaration.qualifiedName?.asString() == kClass.qualifiedName 13 | && it.shortName.getShortName() == kClass.simpleName 14 | } 15 | } 16 | 17 | fun KSAnnotation.getArgument(name: String): KSValueArgument? { 18 | return arguments.firstOrNull { it.name?.asString() == name } 19 | } -------------------------------------------------------------------------------- /plugin/module-plugin-ksp/src/main/kotlin/cn/mycommons/module_plugin/ksp/util/logs.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.module_plugin.ksp.util 2 | 3 | import com.google.devtools.ksp.processing.KSPLogger 4 | 5 | object LogKit { 6 | 7 | lateinit var logger: KSPLogger 8 | 9 | fun setup(logger: KSPLogger) { 10 | this.logger = logger 11 | } 12 | 13 | fun info(msg: String) { 14 | logger.info(msg) 15 | } 16 | 17 | fun warn(msg: String) { 18 | logger.warn(msg) 19 | } 20 | 21 | fun error(msg: String) { 22 | logger.error(msg) 23 | } 24 | } -------------------------------------------------------------------------------- /plugin/module-plugin-ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider: -------------------------------------------------------------------------------- 1 | cn.mycommons.module_plugin.ksp.ModuleProcessorProvider -------------------------------------------------------------------------------- /plugin/module-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /plugin/module-plugin/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | // apply plugin: 'groovy' 3 | apply plugin: 'java-gradle-plugin' 4 | apply plugin: 'kotlin' // version '1.5.31' 5 | 6 | buildscript { 7 | // ext.kotlin_version = '1.5.31' 8 | ext.kotlin_version = '2.0.0' 9 | repositories { 10 | maven { url 'https://maven.aliyun.com/repository/public/' } 11 | mavenLocal() 12 | google() 13 | mavenCentral() 14 | } 15 | 16 | dependencies { 17 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 18 | } 19 | } 20 | 21 | repositories { 22 | maven { url 'https://maven.aliyun.com/repository/public/' } 23 | mavenLocal() 24 | google() 25 | mavenCentral() 26 | // maven { url uri("./../repo") } 27 | } 28 | 29 | sourceCompatibility = "11" 30 | targetCompatibility = "11" 31 | 32 | compileKotlin { 33 | kotlinOptions { 34 | jvmTarget = "11" 35 | } 36 | } 37 | 38 | compileTestKotlin { 39 | kotlinOptions { 40 | jvmTarget = "11" 41 | } 42 | } 43 | 44 | // def version_build_gradle = "7.4.2" 45 | def version_build_gradle = libs.versions.androidAgp.get() 46 | def version_android_tool = "30.0.3" 47 | 48 | group "cn.mycommons" 49 | 50 | dependencies { 51 | implementation fileTree(dir: 'libs', include: ['*.jar']) 52 | 53 | implementation gradleApi() 54 | // implementation localGroovy() 55 | 56 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 57 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 58 | 59 | compileOnly "com.android.tools:common:$version_android_tool" 60 | compileOnly "com.android.tools:sdk-common:$version_android_tool" 61 | compileOnly "com.android.tools:annotations:$version_android_tool" 62 | compileOnly "com.android.tools.build:gradle:$version_build_gradle" 63 | // implementation "com.android.tools.build:gradle:7.0.4" 64 | // compileOnly 'com.google.guava:guava:31.0.1-jre' 65 | 66 | implementation 'org.javassist:javassist:3.20.0-GA' 67 | 68 | implementation("org.ow2.asm:asm-util:9.6") 69 | 70 | // compile project(':java-base') 71 | implementation "cn.mycommons:java-base" 72 | } 73 | 74 | gradlePlugin { 75 | plugins { 76 | moduleplugin { 77 | id = 'moduleplugin' 78 | implementationClass = 'cn.mycommons.module_plugin.ModulePlugin' 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /plugin/module-plugin/gradle: -------------------------------------------------------------------------------- 1 | ../../gradle -------------------------------------------------------------------------------- /plugin/module-plugin/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /plugin/module-plugin/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /plugin/module-plugin/settings.gradle: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | versionCatalogs { 3 | libs { from(files("../../build.versions.toml")) } 4 | } 5 | } -------------------------------------------------------------------------------- /plugin/module-plugin/src/main/kotlin/cn/mycommons/module_plugin/ModulePlugin.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.module_plugin 2 | 3 | import cn.mycommons.module_plugin.core.v2.ModuleProcessTask 4 | import com.android.build.api.variant.AndroidComponentsExtension 5 | import org.gradle.api.Plugin 6 | import org.gradle.api.Project 7 | 8 | /** 9 | * ImplementsPlugin

10 | * Created by xiaqiulei on 2017-05-15. 11 | */ 12 | class ModulePlugin : Plugin { 13 | 14 | override fun apply(project: Project) { 15 | // val app = project.extensions.getByType(AppExtension::class.java) 16 | // app.registerTransform(ModulePluginTransform(project)) 17 | 18 | val ac = project.extensions.getByType(AndroidComponentsExtension::class.java) 19 | ModuleProcessTask.setup(project, ac) 20 | } 21 | } -------------------------------------------------------------------------------- /plugin/module-plugin/src/main/kotlin/cn/mycommons/module_plugin/core/Consts.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.module_plugin.core 2 | 3 | object Consts { 4 | const val IMPLEMENTS_MANAGER = "cn/mycommons/modulebase/annotations/ImplementsManager.class" 5 | const val IMPLEMENTS_MANAGER_NAME = "cn.mycommons.modulebase.annotations.ImplementsManager" 6 | } -------------------------------------------------------------------------------- /plugin/module-plugin/src/main/kotlin/cn/mycommons/module_plugin/core/ImplementRecord.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.module_plugin.core 2 | 3 | import cn.mycommons.modulebase.annotations.Implements 4 | import javassist.CtClass 5 | 6 | /** 7 | * Entry

8 | * Created by xiaqiulei on 2017-05-15. 9 | */ 10 | data class ImplementRecord( 11 | val anImplements: Implements, 12 | val ctClass: CtClass, 13 | ) -------------------------------------------------------------------------------- /plugin/module-plugin/src/main/kotlin/cn/mycommons/module_plugin/core/PluginKit.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.module_plugin.core 2 | 3 | import java.io.File 4 | import java.util.jar.JarFile 5 | 6 | object PluginKit { 7 | 8 | fun isImplementsManager(file: File): Boolean { 9 | return JarFile(file).getEntry(Consts.IMPLEMENTS_MANAGER) != null 10 | } 11 | 12 | fun genConfigMethod(list: List): List { 13 | val config = parseConfig(list) 14 | return genMethodBody(config) 15 | } 16 | 17 | private fun parseConfig(list: List): LinkedHashMap { 18 | val config = linkedMapOf() 19 | list.forEach { 20 | val str = it.anImplements.toString() 21 | val parent = str.substring(str.indexOf("(") + 1, str.indexOf(")")) 22 | .replace("parent=", "").replace(".class", "") 23 | 24 | // 收集所有的接口以及实现类的路径 25 | config[parent] = it.ctClass.name 26 | } 27 | return config 28 | } 29 | 30 | private fun genMethodBody(config: Map): List { 31 | return mutableListOf().apply { 32 | add("{") 33 | add("\tCONFIG = new java.util.HashMap();") 34 | config.forEach { 35 | add("\tCONFIG.put(${it.key}.class, ${it.value}.class);") 36 | } 37 | add("}") 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /plugin/module-plugin/src/main/kotlin/cn/mycommons/module_plugin/core/v2/ModuleProcessTask.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.module_plugin.core.v2 2 | 3 | import cn.mycommons.module_plugin.core.Consts 4 | import cn.mycommons.module_plugin.core.ImplementRecord 5 | import cn.mycommons.module_plugin.core.PluginKit 6 | import cn.mycommons.modulebase.annotations.Implements 7 | import com.android.build.api.artifact.ScopedArtifact 8 | import com.android.build.api.variant.AndroidComponentsExtension 9 | import com.android.build.api.variant.ScopedArtifacts 10 | import javassist.ClassPool 11 | import javassist.CtClass 12 | import org.gradle.api.DefaultTask 13 | import org.gradle.api.Project 14 | import org.gradle.api.file.Directory 15 | import org.gradle.api.file.RegularFile 16 | import org.gradle.api.file.RegularFileProperty 17 | import org.gradle.api.provider.ListProperty 18 | import org.gradle.api.tasks.InputFiles 19 | import org.gradle.api.tasks.OutputFile 20 | import org.gradle.api.tasks.TaskAction 21 | import org.slf4j.Logger 22 | import org.slf4j.LoggerFactory 23 | import java.io.File 24 | import java.io.FileOutputStream 25 | import java.util.jar.JarEntry 26 | import java.util.jar.JarFile 27 | import java.util.jar.JarOutputStream 28 | 29 | abstract class ModuleProcessTask : DefaultTask() { 30 | 31 | companion object { 32 | 33 | fun setup(project: Project, ac: AndroidComponentsExtension<*, *, *>) { 34 | ac.onVariants { 35 | val name = "modularProcess${it.name.replaceFirstChar { c -> c.uppercaseChar() }}" 36 | val taskProvider = project.tasks.register(name, ModuleProcessTask::class.java) 37 | it.artifacts.forScope(ScopedArtifacts.Scope.ALL) 38 | .use(taskProvider) 39 | .toTransform( 40 | ScopedArtifact.CLASSES, 41 | ModuleProcessTask::allJars, 42 | ModuleProcessTask::allDirectories, 43 | ModuleProcessTask::output, 44 | ) 45 | } 46 | } 47 | 48 | } 49 | 50 | @get:InputFiles 51 | abstract val allJars: ListProperty 52 | 53 | @get:InputFiles 54 | abstract val allDirectories: ListProperty 55 | 56 | @get:OutputFile 57 | abstract val output: RegularFileProperty 58 | 59 | @TaskAction 60 | fun taskAction() { 61 | // 记录所有的符合扫描条件的记录 62 | val implementsList = mutableListOf() 63 | // ImplementsManager 注解所在的jar文件 64 | var managerJar: File? = null 65 | val pool = ClassPool(ClassPool.getDefault()) 66 | pool.appendSystemPath() 67 | 68 | allDirectories.get().forEach { d -> 69 | pool.appendClassPath(d.asFile.absolutePath) 70 | d.asFile.walkTopDown().filter { it.name.endsWith(".class") }.forEach { 71 | val rel = it.toRelativeString(d.asFile).replace("/", ".") 72 | // log.error("allDirectories.scanClass: $it -> $rel") 73 | 74 | if (rel.endsWith(".class")) { 75 | val cc: CtClass = pool.get(rel.replace(".class", "")) 76 | checkCollectClass(cc, implementsList) 77 | } 78 | } 79 | } 80 | 81 | allJars.get().forEach { 82 | val file = it.asFile 83 | pool.appendClassPath(file.absolutePath) 84 | 85 | if (managerJar == null && PluginKit.isImplementsManager(file)) { 86 | managerJar = file 87 | } 88 | 89 | for (entry in JarFile(file).entries()) { 90 | val path = entry.name.replace("/", ".") 91 | // log.error("allJars.scanClass: $it -> $path") 92 | if (path.endsWith(".class")) { 93 | val cc = pool.get(path.replace(".class", "")) 94 | checkCollectClass(cc, implementsList) 95 | } 96 | } 97 | } 98 | 99 | logger.quiet("implementsList: ${implementsList.size}") 100 | implementsList.forEach { 101 | logger.quiet("implementsList: $it") 102 | } 103 | 104 | val outputSet = mutableSetOf() // 已经添加过的文件 105 | val tmpOutput = File.createTempFile("${name}-", ".jar") 106 | logger.quiet("tmpOutput = $tmpOutput") 107 | val tmpOutputJar = JarOutputStream(FileOutputStream(tmpOutput)) 108 | 109 | allDirectories.get().forEach { d -> 110 | for (file in d.asFile.walk()) { 111 | if (file.isFile) { 112 | val entryName = file.toRelativeString(d.asFile) 113 | if (outputSet.contains(entryName)) { 114 | logger.error("skip entry: $entryName in $d") 115 | continue 116 | } 117 | addClass(d, file, tmpOutputJar) 118 | outputSet.add(entryName) 119 | } 120 | } 121 | } 122 | 123 | allJars.get().forEach { file -> 124 | val jarFile = JarFile(file.asFile) 125 | for (e in jarFile.entries().iterator()) { 126 | if (outputSet.contains(e.name)) { 127 | if (!e.isDirectory) { 128 | logger.error("skip entry: ${e.name} in $file") 129 | } 130 | continue 131 | } 132 | tmpOutputJar.putNextEntry(JarEntry(e.name)) 133 | val data = if (e.name == Consts.IMPLEMENTS_MANAGER) { 134 | modifyClass(pool.get(Consts.IMPLEMENTS_MANAGER_NAME), implementsList) 135 | } else { 136 | jarFile.getInputStream(e).readBytes() 137 | } 138 | tmpOutputJar.write(data) 139 | tmpOutputJar.closeEntry() 140 | outputSet.add(e.name) 141 | } 142 | jarFile.close() 143 | } 144 | 145 | tmpOutputJar.close() 146 | output.get().asFile.let { 147 | if (it.exists()) { 148 | it.delete() 149 | } 150 | tmpOutput.copyTo(it) 151 | tmpOutput.delete() 152 | } 153 | } 154 | 155 | // 如果class中的类包含注解则先收集起来 156 | private fun checkCollectClass(cc: CtClass, implementsList: MutableList) { 157 | kotlin.runCatching { 158 | val an = cc.getAnnotation(Implements::class.java) as Implements? 159 | if (an != null) { 160 | implementsList.add(ImplementRecord(an, cc)) 161 | } 162 | } 163 | } 164 | 165 | private fun modifyClass(cc: CtClass, implementsList: List): ByteArray { 166 | // 修改class,在class中插入静态代码块,做初始化 167 | val body = PluginKit.genConfigMethod(implementsList) 168 | logger.quiet("body = " + body.joinToString("\n")) 169 | 170 | if (cc.isFrozen) { 171 | cc.defrost() 172 | } 173 | cc.makeClassInitializer().setBody(body.joinToString("\n")) 174 | 175 | return cc.toBytecode() 176 | } 177 | 178 | private fun addClass(directory: Directory, file: File, jos: JarOutputStream) { 179 | val rel = file.toRelativeString(directory.asFile) 180 | jos.putNextEntry(JarEntry(rel)) 181 | jos.write(file.readBytes()) 182 | jos.closeEntry() 183 | } 184 | } -------------------------------------------------------------------------------- /plugin/module-plugin/src/main/kotlin/cn/mycommons/module_plugin/util/file.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.module_plugin.util 2 | 3 | import java.io.File 4 | 5 | 6 | fun File.safeRename(dest: File) { 7 | kotlin.runCatching { 8 | dest.safeDelete() 9 | renameTo(dest) 10 | } 11 | } 12 | 13 | fun File?.safeDelete() { 14 | if (this != null && exists()) { 15 | if (isDirectory) { 16 | deleteRecursively() 17 | } else { 18 | delete() 19 | } 20 | } 21 | } 22 | 23 | fun File?.isEmptyDir(): Boolean { 24 | if (this != null && this.isDirectory) { 25 | return list().isNullOrEmpty() 26 | } 27 | return false 28 | } 29 | 30 | fun File?.isNotEmptyDir(): Boolean { 31 | return !this.isEmptyDir() 32 | } 33 | -------------------------------------------------------------------------------- /plugin/module-plugin/src/main/resources/META-INF/gradle-plugins/cn.mycommons.module_plugin.properties: -------------------------------------------------------------------------------- 1 | implementation-class=cn.mycommons.module_plugin.ModulePlugin -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | versionCatalogs { 3 | libs { from(files("build.versions.toml")) } 4 | } 5 | } 6 | 7 | includeBuild('common/java-base') 8 | 9 | if (Boolean.parseBoolean(PLUGIN_DEV)) { 10 | includeBuild("plugin/module-plugin") 11 | includeBuild("plugin/module-plugin-ksp") 12 | } 13 | 14 | include ':common:implements-process' 15 | 16 | 17 | include ':test-plugin:test-plugin-lib' 18 | include ':test-plugin:test-plugin-app' 19 | 20 | 21 | include ':test-module:module-base', ':test-module:module-service', ':test-module:module-component' 22 | include ':test-module:module-user', ':test-module:module-order', ':test-module:module-shopping' 23 | include ':test-module:app' -------------------------------------------------------------------------------- /test-module/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /test-module/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | // apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | android { 7 | namespace 'cn.mycommons.androidmodular' 8 | compileSdk libs.versions.compileSdkVersion.get().toInteger() 9 | 10 | defaultConfig { 11 | applicationId "cn.mycommons.androidmodular" 12 | minSdkVersion libs.versions.minSdkVersion.get().toInteger() 13 | targetSdkVersion libs.versions.targetSdkVersion.get().toInteger() 14 | versionCode 1 15 | versionName "1.0" 16 | } 17 | 18 | dataBinding { 19 | enabled = true 20 | } 21 | 22 | signingConfigs { 23 | test1 { 24 | def keystorePropertiesFile = project.file("test1.properties") 25 | def keystoreProperties = new Properties() 26 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 27 | 28 | storeFile project.file(keystoreProperties['storeFile']) 29 | storePassword keystoreProperties['storePassword'] 30 | keyAlias keystoreProperties['keyAlias'] 31 | keyPassword keystoreProperties['keyPassword'] 32 | } 33 | } 34 | 35 | buildTypes { 36 | release { 37 | signingConfig signingConfigs.test1 38 | minifyEnabled true 39 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 40 | } 41 | } 42 | 43 | compileOptions { 44 | sourceCompatibility JavaVersion.VERSION_11 45 | targetCompatibility JavaVersion.VERSION_11 46 | } 47 | 48 | kotlinOptions { 49 | jvmTarget = '11' 50 | } 51 | } 52 | 53 | 54 | dependencies { 55 | implementation fileTree(dir: 'libs', include: ['*.jar']) 56 | // kapt "com.android.databinding:compiler:$rootProject.ext.gradle_version" 57 | 58 | // common 59 | implementation project(':test-module:module-base') 60 | implementation project(':test-module:module-component') 61 | implementation project(':test-module:module-service') 62 | // api "cn.mycommons:ModuleBase:$LIB_VERSION" 63 | // api "cn.mycommons:ModuleComponent:$LIB_VERSION" 64 | // api "cn.mycommons:ModuleService:$LIB_VERSION" 65 | 66 | // biz 67 | implementation project(':test-module:module-user') 68 | implementation project(':test-module:module-order') 69 | implementation project(':test-module:module-shopping') 70 | } 71 | 72 | apply plugin: 'moduleplugin' -------------------------------------------------------------------------------- /test-module/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/xiaqiulei/Documents/dev/android_sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /test-module/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test-module/app/src/main/java/cn/mycommons/androidmodular/InjectHelper.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.androidmodular 2 | 3 | import cn.mycommons.androidmodular.app.AppContext 4 | import cn.mycommons.androidmodular.app.getAppContext 5 | import cn.mycommons.modulebase.IModuleConfig 6 | 7 | /** 8 | * InjectHelper

9 | * Created by xiaqiulei on 2017-05-14. 10 | */ 11 | object InjectHelper { 12 | 13 | val appContext: AppContext get() = getAppContext() 14 | 15 | val iModuleConfig: IModuleConfig get() = appContext.getModuleConfig() 16 | 17 | fun getInstance(tClass: Class): T? { 18 | val config = iModuleConfig 19 | val implementClass = config.getServiceImplementClass(tClass) 20 | if (implementClass != null) { 21 | try { 22 | return implementClass.newInstance() 23 | } catch (e: Exception) { 24 | e.printStackTrace() 25 | } 26 | 27 | } 28 | return null 29 | } 30 | 31 | // fun getInstanceByDatabase(tClass: Class): T? { 32 | // val database = `Implement_$$_Database`() 33 | // val implementClass = database.getServiceImplementClass(tClass) 34 | // if (implementClass != null) { 35 | // try { 36 | // return implementClass.newInstance() 37 | // } catch (e: Exception) { 38 | // e.printStackTrace() 39 | // } 40 | // 41 | // } 42 | // return null 43 | // } 44 | } -------------------------------------------------------------------------------- /test-module/app/src/main/java/cn/mycommons/androidmodular/RouterHelper.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.androidmodular 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | 6 | /** 7 | * RouterHelper

8 | * Created by xiaqiulei on 2017-05-14. 9 | */ 10 | object RouterHelper { 11 | 12 | fun dispatch(context: Context, uri: String) { 13 | val config = InjectHelper.iModuleConfig 14 | val activityClass = config.getRouterActivity(uri) 15 | if (activityClass != null) { 16 | val intent = Intent(context, activityClass) 17 | context.startActivity(intent) 18 | } else { 19 | val process = config.getRouterProcess(uri) 20 | if (process != null) { 21 | process.proeces(uri) 22 | } else { 23 | throw RuntimeException("can not dispatch uri = $uri") 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /test-module/app/src/main/java/cn/mycommons/androidmodular/app/AppContext.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.androidmodular.app 2 | 3 | import android.app.Application 4 | 5 | import cn.mycommons.modulebase.IModuleConfig 6 | 7 | /** 8 | * AppContext

9 | * Created by xiaqiulei on 2017-05-14. 10 | */ 11 | var context: AppContext? = null 12 | 13 | fun getAppContext(): AppContext { 14 | return context!! 15 | } 16 | 17 | class AppContext : Application() { 18 | 19 | ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// 20 | //// 21 | ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | private var lifeCycleManager: ModuleLifeCycleManager? = null 24 | 25 | override fun onCreate() { 26 | super.onCreate() 27 | context = this 28 | 29 | lifeCycleManager = ModuleLifeCycleManager(this) 30 | lifeCycleManager!!.onCreate() 31 | } 32 | 33 | override fun onTerminate() { 34 | super.onTerminate() 35 | 36 | lifeCycleManager!!.onTerminate() 37 | } 38 | 39 | fun getModuleConfig(): IModuleConfig = lifeCycleManager!!.getModuleConfig() 40 | } -------------------------------------------------------------------------------- /test-module/app/src/main/java/cn/mycommons/androidmodular/app/ModuleConfig.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.androidmodular.app 2 | 3 | import android.app.Activity 4 | import cn.mycommons.modulebase.IModuleConfig 5 | import cn.mycommons.modulebase.IRouterProcess 6 | import java.util.* 7 | 8 | /** 9 | * ModuleConfig

10 | * Created by xiaqiulei on 2017-05-14. 11 | */ 12 | class ModuleConfig internal constructor() : IModuleConfig { 13 | 14 | private val activityRouterConfig: MutableMap> 15 | private val routerProcessConfig: MutableMap 16 | private val serviceConfig: MutableMap, Class<*>> 17 | 18 | init { 19 | activityRouterConfig = HashMap() 20 | routerProcessConfig = HashMap() 21 | serviceConfig = HashMap() 22 | } 23 | 24 | override fun registerRouter(uri: String, activityClass: Class) { 25 | activityRouterConfig[uri] = activityClass 26 | } 27 | 28 | override fun getRouterActivity(uri: String): Class? { 29 | return activityRouterConfig[uri] 30 | } 31 | 32 | override fun registerRouter(uri: String, routerProcess: IRouterProcess) { 33 | routerProcessConfig.put(uri, routerProcess) 34 | } 35 | 36 | override fun getRouterProcess(uri: String): IRouterProcess? { 37 | return routerProcessConfig[uri] 38 | } 39 | 40 | override fun registerService(serviceClass: Class, implementClass: Class) { 41 | serviceConfig.put(serviceClass, implementClass) 42 | } 43 | 44 | override fun getServiceImplementClass(serviceClass: Class): Class { 45 | return serviceConfig[serviceClass] as Class 46 | } 47 | } -------------------------------------------------------------------------------- /test-module/app/src/main/java/cn/mycommons/androidmodular/app/ModuleLifeCycleManager.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.androidmodular.app 2 | 3 | import android.app.Application 4 | import cn.mycommons.modulebase.IModuleConfig 5 | import cn.mycommons.modulebase.IModuleLifeCycle 6 | import cn.mycommons.moduleorder.OrderModuleLifeCycle 7 | import cn.mycommons.moduleshopping.ShoppingModuleLifeCycle 8 | import cn.mycommons.moduleuser.UserModuleLifeCycle 9 | import java.util.* 10 | 11 | /** 12 | * ModuleLifeCycleManager

13 | * Created by xiaqiulei on 2017-05-14. 14 | */ 15 | internal class ModuleLifeCycleManager(application: Application) { 16 | 17 | private val moduleConfig: ModuleConfig = ModuleConfig() 18 | private val moduleLifeCycleList: MutableList 19 | 20 | init { 21 | moduleLifeCycleList = ArrayList() 22 | moduleLifeCycleList.add(UserModuleLifeCycle(application)) 23 | moduleLifeCycleList.add(OrderModuleLifeCycle(application)) 24 | moduleLifeCycleList.add(ShoppingModuleLifeCycle(application)) 25 | } 26 | 27 | fun onCreate() { 28 | for (lifeCycle in moduleLifeCycleList) { 29 | lifeCycle.onCreate(moduleConfig) 30 | } 31 | } 32 | 33 | fun onTerminate() { 34 | for (lifeCycle in moduleLifeCycleList) { 35 | lifeCycle.onTerminate() 36 | } 37 | } 38 | 39 | fun getModuleConfig(): IModuleConfig { 40 | return moduleConfig 41 | } 42 | } -------------------------------------------------------------------------------- /test-module/app/src/main/java/cn/mycommons/androidmodular/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.mycommons.androidmodular.ui 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import android.widget.Toast 6 | import androidx.databinding.DataBindingUtil 7 | import cn.mycommons.androidmodular.InjectHelper 8 | import cn.mycommons.androidmodular.R 9 | import cn.mycommons.androidmodular.BuildConfig 10 | import cn.mycommons.androidmodular.RouterHelper 11 | import cn.mycommons.androidmodular.databinding.ActivityMainBinding 12 | import cn.mycommons.modulebase.base.BaseActivity 13 | import cn.mycommons.moduleservice.IUserService 14 | import cn.mycommons.moduleuser.UserActivity__RouterInject 15 | 16 | class MainActivity : BaseActivity() { 17 | 18 | val TAG = "MainActivity" 19 | 20 | private lateinit var binding: ActivityMainBinding 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | binding = DataBindingUtil.setContentView(this, R.layout.activity_main) 25 | 26 | binding.button.setOnClickListener { 27 | val userService = InjectHelper.getInstance(IUserService::class.java) 28 | if (userService != null) { 29 | Toast.makeText(getContext(), userService.getUserName(), Toast.LENGTH_SHORT).show() 30 | } 31 | } 32 | binding.btnGotoUser.setOnClickListener { RouterHelper.dispatch(getContext(), "app://user") } 33 | 34 | Log.i(TAG, "onCreate: ${UserActivity__RouterInject::class.java}") 35 | Log.i(TAG, "BuildConfig: ${BuildConfig::class.java}") 36 | } 37 | } -------------------------------------------------------------------------------- /test-module/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 |