commit afa0ee5c2ec825fb2c7987097f1243d137be1276 Author: fankesyooni Date: Tue Oct 7 07:39:24 2025 +0800 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d9b9b1a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,35 @@ +# noinspection EditorConfigKeyCorrectness +[{*.kt,*.kts}] +ktlint_standard_annotation = disabled +ktlint_standard_filename = disabled +ktlint_standard_wrapping = disabled +ktlint_standard_import-ordering = enabled +ktlint_standard_max-line-length = disabled +ktlint_standard_multiline-if-else = disabled +ktlint_standard_argument-list-wrapping = disabled +ktlint_standard_parameter-list-wrapping = disabled +ktlint_standard_trailing-comma-on-declaration-site = disabled +ktlint_function_signature_body_expression_wrapping = multiline +ktlint_standard_string-template-indent = disabled +ktlint_standard_function-signature = disabled +ktlint_standard_trailing-comma-on-call-site = disabled +ktlint_standard_multiline-expression-wrapping = disabled +ktlint_standard_no-empty-first-line-in-class-body = disabled +ktlint_standard_if-else-wrapping = disabled +ktlint_standard_if-else-bracing = disabled +ktlint_standard_statement-wrapping = disabled +ktlint_standard_blank-line-before-declaration = disabled +ktlint_standard_no-empty-file = disabled +ktlint_standard_property-naming = disabled +ktlint_standard_function-naming = disabled +ktlint_standard_chain-method-continuation = disabled +ktlint_standard_class-signature = disabled +ktlint_standard_condition-wrapping = disabled +ktlint_standard_class-signature = disabled +ktlint_standard_no-trailing-spaces = disabled +ktlint_standard_multiline-loop = disabled +ij_continuation_indent_size = 2 +indent_size = 4 +indent_style = space +insert_final_newline = false +max_line_length = 150 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c43ffa3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,120 @@ +## Fully .gtignore for IntelliJ, Android Studio and Gradle based Java projects +## References: +## - https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +## - https://github.com/android/platform-samples/blob/main/.gitignore + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +.idea/.name +.idea/artifacts +.idea/compiler.xml +.idea/jarRepositories.xml +.idea/modules.xml +.idea/*.iml +.idea/modules +.idea/caches +.idea/material_theme** +.idea/other.xml +*.iml +*.ipr + +# Kotlin +.kotlin +.idea/kotlinc.xml + +# Misc +.idea/misc.xml +.idea/markdown.xml + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# Android studio 3.1+ additional +.idea/deployment*.xml +.idea/assetWizardSettings.xml +.idea/androidTestResultsUserPreferences.xml + +# Android projects +.idea/AndroidProjectSystem.xml +.idea/deviceManager.xml +**/local.properties +/captures +.externalNativeBuild +.cxx + +# Gradle projects +.gradle +build/ + +# Mkdocs temporary serving folder +docs-gen +site +*.bak +.idea/appInsightsSettings.xml + +# Discord plugin for IntelliJ +.idea/discord.xml + +# Copilot for IntelliJ +.idea/copilot** + +# Mac OS +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/fileTemplates/includes/copyright-name.template b/.idea/fileTemplates/includes/copyright-name.template new file mode 100644 index 0000000..ad0a243 --- /dev/null +++ b/.idea/fileTemplates/includes/copyright-name.template @@ -0,0 +1 @@ +2019 HighCapable \ No newline at end of file diff --git a/.idea/fileTemplates/includes/license-content.template b/.idea/fileTemplates/includes/license-content.template new file mode 100644 index 0000000..68616e9 --- /dev/null +++ b/.idea/fileTemplates/includes/license-content.template @@ -0,0 +1,13 @@ + * Apache License Version 2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. \ No newline at end of file diff --git a/.idea/fileTemplates/includes/open-source-license-header.template b/.idea/fileTemplates/includes/open-source-license-header.template new file mode 100644 index 0000000..6f41ea1 --- /dev/null +++ b/.idea/fileTemplates/includes/open-source-license-header.template @@ -0,0 +1,13 @@ +/* + * #parse("project-name") - #parse("project-description") + + * Copyright (C) #parse("copyright-name") + + * #parse("project-url") + + * + #parse("license-content") + + * + * This file is created by $USER on $DATE. + */ \ No newline at end of file diff --git a/.idea/fileTemplates/includes/project-description.template b/.idea/fileTemplates/includes/project-description.template new file mode 100644 index 0000000..8ef8496 --- /dev/null +++ b/.idea/fileTemplates/includes/project-description.template @@ -0,0 +1 @@ +Companion to Moshi with more practical features. \ No newline at end of file diff --git a/.idea/fileTemplates/includes/project-name.template b/.idea/fileTemplates/includes/project-name.template new file mode 100644 index 0000000..ad9c29a --- /dev/null +++ b/.idea/fileTemplates/includes/project-name.template @@ -0,0 +1 @@ +Moshi Companion \ No newline at end of file diff --git a/.idea/fileTemplates/includes/project-url.template b/.idea/fileTemplates/includes/project-url.template new file mode 100644 index 0000000..8128549 --- /dev/null +++ b/.idea/fileTemplates/includes/project-url.template @@ -0,0 +1 @@ +https://github.com/HighCapable/moshi-companion \ No newline at end of file diff --git a/.idea/fileTemplates/internal/Kotlin Annotation.kt b/.idea/fileTemplates/internal/Kotlin Annotation.kt new file mode 100644 index 0000000..0e2b92b --- /dev/null +++ b/.idea/fileTemplates/internal/Kotlin Annotation.kt @@ -0,0 +1,6 @@ +#parse("open-source-license-header") + +#if (${PACKAGE_NAME} != "")package ${PACKAGE_NAME} +#end + +annotation class ${NAME} diff --git a/.idea/fileTemplates/internal/Kotlin Class.kt b/.idea/fileTemplates/internal/Kotlin Class.kt new file mode 100644 index 0000000..4112dfb --- /dev/null +++ b/.idea/fileTemplates/internal/Kotlin Class.kt @@ -0,0 +1,7 @@ +#parse("open-source-license-header") + +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME} + +#end +class ${NAME} { +} \ No newline at end of file diff --git a/.idea/fileTemplates/internal/Kotlin Data Class.kt b/.idea/fileTemplates/internal/Kotlin Data Class.kt new file mode 100644 index 0000000..db701e7 --- /dev/null +++ b/.idea/fileTemplates/internal/Kotlin Data Class.kt @@ -0,0 +1,6 @@ +#parse("open-source-license-header") + +#if (${PACKAGE_NAME} != "")package ${PACKAGE_NAME} +#end + +data class ${NAME}() diff --git a/.idea/fileTemplates/internal/Kotlin Enum.kt b/.idea/fileTemplates/internal/Kotlin Enum.kt new file mode 100644 index 0000000..a1fa97d --- /dev/null +++ b/.idea/fileTemplates/internal/Kotlin Enum.kt @@ -0,0 +1,7 @@ +#parse("open-source-license-header") + +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME} + +#end +enum class ${NAME} { +} \ No newline at end of file diff --git a/.idea/fileTemplates/internal/Kotlin File.kt b/.idea/fileTemplates/internal/Kotlin File.kt new file mode 100644 index 0000000..321d3a5 --- /dev/null +++ b/.idea/fileTemplates/internal/Kotlin File.kt @@ -0,0 +1,5 @@ +#parse("open-source-license-header") + +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME} + +#end diff --git a/.idea/fileTemplates/internal/Kotlin Interface.kt b/.idea/fileTemplates/internal/Kotlin Interface.kt new file mode 100644 index 0000000..de3a20b --- /dev/null +++ b/.idea/fileTemplates/internal/Kotlin Interface.kt @@ -0,0 +1,7 @@ +#parse("open-source-license-header") + +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME} + +#end +interface ${NAME} { +} \ No newline at end of file diff --git a/.idea/fileTemplates/internal/Kotlin Object.kt b/.idea/fileTemplates/internal/Kotlin Object.kt new file mode 100644 index 0000000..e9475be --- /dev/null +++ b/.idea/fileTemplates/internal/Kotlin Object.kt @@ -0,0 +1,7 @@ +#parse("open-source-license-header") + +#if (${PACKAGE_NAME} != "")package ${PACKAGE_NAME} +#end + +object ${NAME} { +} diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..506d1e2 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README-zh-CN.md b/README-zh-CN.md new file mode 100644 index 0000000..1059489 --- /dev/null +++ b/README-zh-CN.md @@ -0,0 +1,86 @@ +# Moshi Companion + +[![GitHub license](https://img.shields.io/github/license/HighCapable/moshi-companion?color=blue&style=flat-square)](https://github.com/HighCapable/moshi-companion/blob/main/LICENSE) +[![Telegram](https://img.shields.io/badge/discussion-Telegram-blue.svg?logo=telegram&style=flat-square)](https://t.me/HighCapable) +[![Telegram](https://img.shields.io/badge/discussion%20dev-Telegram-blue.svg?logo=telegram&style=flat-square)](https://t.me/HighCapable_Dev) +[![QQ](https://img.shields.io/badge/discussion%20dev-QQ-blue.svg?logo=tencent-qq&logoColor=red&style=flat-square)](https://qm.qq.com/cgi-bin/qm/qr?k=Pnsc5RY6N2mBKFjOLPiYldbAbprAU3V7&jump_from=webapi&authKey=X5EsOVzLXt1dRunge8ryTxDRrh9/IiW1Pua75eDLh9RE3KXE+bwXIYF5cWri/9lf) + +为 Moshi 提供更多实用功能的伴侣。 + +[English](README.md) | 简体中文 + +| LOGO | [HighCapable](https://github.com/HighCapable) | +|-------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------| + +这个项目属于上述组织,**点击上方链接关注这个组织**,发现更多好项目。 + +## 这是什么 + +这是一个针对 [Moshi](https://github.com/square/moshi) 项目进行功能性优化的工具,使 Moshi 能够在 Android 项目启用 R8 后,不再依赖于 +`Class.forName` 寻找 `moshi-kotlin-codegen` 生成的适配器类,从而实现对实体类的名称、字段的完全混淆,提升代码安全性和降低 APK 体积。 + +## 项目动机 + +在 Android 项目中使用 Moshi 进行 JSON 序列化和反序列化时,在使用 `@JsonClass(generateAdapter = true)` 注解的实体类时,Moshi 会通过 `Class.forName` +方法动态加载 `moshi-codegen` 生成的适配器类。如果项目启用了 R8 进行代码混淆和优化,Moshi 默认的混淆规则是保留实体类和适配器类的名称不被混淆,这样能够正确反射,但是会导致代码安全性降低和 +APK 体积增大。 + +于是我对 Moshi 的适配器生成原理进行了探索,我很认可这种生成手写代码的高性能解决方案。通过研究,我认为将 `moshi-kotlin-codegen` 生成的适配器手动注册到 +Moshi +中来使得类名能够混淆是一个可行的解决方案,于是我曾作为这个想法向 Moshi 项目提出了 [PR](https://github.com/square/moshi/pull/2002) +,但是不得不承认修改项目本身可能会造成一些不必要的维护问题,不一定符合所有人的需求,而且有一些代码生成的性能问题需要解决,本着不对项目本身进行侵入的原则,我现在选择将这个想法独立出来,作为一个单独的项目进行维护。 + +所以正如我的 PR 提出的方案一样,目前的实现方案是通过读取整个项目的 `@JsonClass(generateAdapter = true)` 注解,获取所有需要生成适配器的实体类并创建 +`AdapterRegistry`,然后在运行时通过 `Moshi.Builder` 手动注册这些适配器,并且改进了混淆规则的生成。 + +## 开始使用 + +- [点击这里](docs/guide-zh-CN.md) 查看使用文档 + +## 更新日志 + +- [点击这里](docs/changelog-zh-CN.md) 查看历史更新日志 + +## 项目推广 + + +
+

嘿,还请君留步!👋

+

这里有 Android 开发工具、UI 设计、Gradle 插件、Xposed 模块和实用软件等相关项目。

+

如果下方的项目能为你提供帮助,不妨为我点个 star 吧!

+

所有项目免费、开源,遵循对应开源许可协议。

+

→ 查看更多关于我的项目,请点击这里 ←

+
+ +## Star History + +![Star History Chart](https://api.star-history.com/svg?repos=HighCapable/moshi-companion&type=Date) + +## 第三方开源使用声明 + +- [Moshi](https://github.com/square/moshi) +- [Gson](https://github.com/google/gson) + +## 许可证 + +- [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +``` +Apache License Version 2.0 + +Copyright (C) 2019 HighCapable + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` + +版权所有 © 2019 HighCapable \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..932c234 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Moshi Companion + +[![GitHub license](https://img.shields.io/github/license/HighCapable/moshi-companion?color=blue&style=flat-square)](https://github.com/HighCapable/moshi-companion/blob/main/LICENSE) +[![Telegram](https://img.shields.io/badge/discussion-Telegram-blue.svg?logo=telegram&style=flat-square)](https://t.me/HighCapable) +[![Telegram](https://img.shields.io/badge/discussion%20dev-Telegram-blue.svg?logo=telegram&style=flat-square)](https://t.me/HighCapable_Dev) +[![QQ](https://img.shields.io/badge/discussion%20dev-QQ-blue.svg?logo=tencent-qq&logoColor=red&style=flat-square)](https://qm.qq.com/cgi-bin/qm/qr?k=Pnsc5RY6N2mBKFjOLPiYldbAbprAU3V7&jump_from=webapi&authKey=X5EsOVzLXt1dRunge8ryTxDRrh9/IiW1Pua75eDLh9RE3KXE+bwXIYF5cWri/9lf) + +Companion to Moshi with more practical features. + +English | [简体中文](README-zh-CN.md) + +| LOGO | [HighCapable](https://github.com/HighCapable) | +|-------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------| + +This project belongs to the above-mentioned organization, **click the link above to follow this organization** and discover more good projects. + +## What's this + +This is a functional optimization tool for the [Moshi](https://github.com/square/moshi) project. It enables Moshi to +work seamlessly with R8 in Android projects without relying on `Class.forName` to locate adapter classes generated by `moshi-kotlin-codegen`. This +allows for +complete obfuscation of entity class names and fields, enhancing code security and reducing APK size. + +## Motivation + +When using Moshi for JSON serialization and deserialization in Android projects, entity classes annotated with `@JsonClass(generateAdapter = true)` +cause Moshi to dynamically load adapter classes generated by `moshi-kotlin-codegen` through the `Class.forName` method. If R8 code obfuscation and +optimization are enabled in the project, Moshi's default obfuscation rules preserve the names of entity classes and adapter classes from being +obfuscated to ensure proper reflection. However, this leads to reduced code security and increased APK size. + +I explored Moshi's adapter generation principles and greatly appreciated this high-performance solution of generating handwritten code. Through +research, I believed that manually registering adapters generated by `moshi-codegen` into Moshi to allow class name obfuscation was a viable solution. +I once proposed this idea as a [PR](https://github.com/square/moshi/pull/2002) to the Moshi project. However, modifying the project itself could cause +unnecessary maintenance issues and might not meet everyone's needs. Additionally, there are some code generation performance issues that need to be +addressed. Following the principle of non-intrusive project modification, I have now chosen to extract this idea as an independent project for +maintenance. + +Therefore, as proposed in my PR, the current implementation reads `@JsonClass(generateAdapter = true)` annotations throughout the entire project, +obtains all entity classes that need adapter generation, creates an `AdapterRegistry`, and then manually registers these adapters through +`Moshi.Builder` at runtime, while also improving the generation of obfuscation rules. + +## Get Started + +- [Click here](docs/guide.md) to view the documentation + +## Changelog + +- [Click here](docs/changelog.md) to view the historical changelog + +## Promotion + + +
+

Hey, please stay! 👋

+

Here are related projects such as Android development tools, UI design, Gradle plugins, Xposed Modules and practical software.

+

If the project below can help you, please give me a star!

+

All projects are free, open source, and follow the corresponding open source license agreement.

+

→ To see more about my projects, please click here ←

+
+ +## Star History + +![Star History Chart](https://api.star-history.com/svg?repos=HighCapable/moshi-companion&type=Date) + +## Third-Party Open Source Usage Statement + +- [Moshi](https://github.com/square/moshi) +- [Gson](https://github.com/google/gson) + +## License + +- [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) + +``` +Apache License Version 2.0 + +Copyright (C) 2019 HighCapable + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` + +Copyright © 2019 HighCapable \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..7ac22ba --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,24 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile + +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.ksp) apply false + alias(libs.plugins.maven.publish) apply false +} + +allprojects { + tasks.withType().configureEach { + compilerOptions { + jvmTarget = JvmTarget.JVM_17 + freeCompilerArgs.addAll( + "-opt-in=kotlin.ExperimentalStdlibApi", + "-Xno-param-assertions", + "-Xno-call-assertions", + "-Xno-receiver-assertions" + ) + } + } +} \ No newline at end of file diff --git a/companion-api/build.gradle.kts b/companion-api/build.gradle.kts new file mode 100644 index 0000000..6674d91 --- /dev/null +++ b/companion-api/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.maven.publish) +} + +group = property.project.groupName +version = property.project.version + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +kotlin { + jvmToolchain(17) + compilerOptions { + freeCompilerArgs = listOf( + "-Xno-param-assertions", + "-Xno-call-assertions", + "-Xno-receiver-assertions" + ) + } +} + +dependencies { + implementation(libs.moshi.kotlin) + implementation(libs.kavaref.core) + implementation(libs.kavaref.extension) +} \ No newline at end of file diff --git a/companion-api/src/main/kotlin/com/highcapable/moshi/companion/api/AdapterRegistryFactory.kt b/companion-api/src/main/kotlin/com/highcapable/moshi/companion/api/AdapterRegistryFactory.kt new file mode 100644 index 0000000..f1588d3 --- /dev/null +++ b/companion-api/src/main/kotlin/com/highcapable/moshi/companion/api/AdapterRegistryFactory.kt @@ -0,0 +1,121 @@ +/* + * Moshi Companion - Companion to Moshi with more practical features. + * Copyright (C) 2019 HighCapable + * https://github.com/HighCapable/moshi-companion + * + * Apache License Version 2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file is created by fankes on 2025/10/2. + */ +package com.highcapable.moshi.companion.api + +import com.highcapable.kavaref.KavaRef.Companion.resolve +import com.highcapable.kavaref.extension.classOf +import com.highcapable.kavaref.extension.toClassOrNull +import com.highcapable.kavaref.resolver.ConstructorResolver +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +/** + * [MoshiCompanion] registered [JsonAdapter.Factory] implementation that loads + * generated [JsonAdapter]s from the given [MoshiCompanion.AdapterRegistry]. + */ +internal class AdapterRegistryFactory(private val adapterRegistry: MoshiCompanion.AdapterRegistry) : JsonAdapter.Factory { + + override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { + val rawType = type.toClassOrNull() ?: return null + + // Check if there is annotation for `@JsonClass` and `generateAdapter = true`. + val jsonClass = rawType.getAnnotation(classOf()) + if (jsonClass == null || !jsonClass.generateAdapter) return null + + var possiblyFoundAdapter: Class>? = null + + return try { + val adapterClass = adapterRegistry.adapters.firstNotNullOfOrNull { (regType, regAdapter) -> + if (Types.equals(rawType, regType)) + regAdapter + else null + } ?: run { + // Fallback to reflective lookup. + val rawAdapterClassName = Types.generatedJsonAdapterName(rawType.name) + rawAdapterClassName.toClassOrNull>() ?: throw ClassNotFoundException() + } + possiblyFoundAdapter = adapterClass + + val constructor: ConstructorResolver> + val args: Array + + if (type is ParameterizedType) { + val typeArgs = type.actualTypeArguments + + // Common case first. + val twoParams = adapterClass.resolve() + .optional(silent = true) + .constructor { + parameters(Moshi::class, Array::class) + }.firstOrNull() + val oneParam = adapterClass.resolve() + .optional(silent = true) + .constructor { parameters(Moshi::class) } + .firstOrNull() + + constructor = twoParams ?: oneParam ?: throw NoSuchMethodException() + args = if (twoParams != null) + arrayOf(moshi, typeArgs) + else arrayOf(moshi) + } else { + val oneParam = adapterClass.resolve() + .optional(silent = true) + .constructor { parameters(Moshi::class) } + .firstOrNull() + val noParams = adapterClass.resolve() + .optional(silent = true) + .constructor { emptyParameters() } + .firstOrNull() + + constructor = oneParam ?: noParams ?: throw NoSuchMethodException() + args = if (oneParam != null) + arrayOf(moshi) + else emptyArray() + } + + constructor.createQuietly(*args)?.nullSafe() + } catch (_: ClassNotFoundException) { + // If it is not found at all, return null and let other factory handle it. + null + } catch (e: NoSuchMethodException) { + if (possiblyFoundAdapter != null && type !is ParameterizedType && possiblyFoundAdapter.typeParameters.isNotEmpty()) + throw RuntimeException( + "Failed to find the generated JsonAdapter constructor for '$type'. " + + "Suspiciously, the type was not parameterized but the target class " + + "'${possiblyFoundAdapter.canonicalName}' is generic. " + + "Consider using Types#newParameterizedType() to define these missing type variables.", + e + ) + else throw RuntimeException( + "Failed to find the generated JsonAdapter constructor for $type. " + + "Target adapter class is '${possiblyFoundAdapter?.canonicalName}'.", + e + ) + } catch (e: Exception) { + throw RuntimeException("Failed to create JsonAdapter for $type.", e) + } + } +} \ No newline at end of file diff --git a/companion-api/src/main/kotlin/com/highcapable/moshi/companion/api/MoshiCompanion.kt b/companion-api/src/main/kotlin/com/highcapable/moshi/companion/api/MoshiCompanion.kt new file mode 100644 index 0000000..3082319 --- /dev/null +++ b/companion-api/src/main/kotlin/com/highcapable/moshi/companion/api/MoshiCompanion.kt @@ -0,0 +1,105 @@ +/* + * Moshi Companion - Companion to Moshi with more practical features. + * Copyright (C) 2019 HighCapable + * https://github.com/HighCapable/moshi-companion + * + * Apache License Version 2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file is created by fankes on 2025/10/2. + */ +package com.highcapable.moshi.companion.api + +import com.highcapable.moshi.companion.api.MoshiCompanion.AdapterRegistry +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Moshi.Builder +import java.lang.reflect.Type + +/** + * Moshi Companion core API. + */ +object MoshiCompanion { + + /** + * Add an [AdapterRegistry] on this [Moshi.Builder] instance to register all [JsonAdapter]s + * declared in the [AdapterRegistry.adapters] map when [Moshi.Builder.build] is called. + * @see Moshi.Builder.addRegistry + * @param builder the instance. + * @param adapterRegistry the [AdapterRegistry] instance to set. + * @return [Moshi.Builder] + */ + @JvmStatic + fun addRegistry(builder: Builder, adapterRegistry: AdapterRegistry) = apply { + builder.add(AdapterRegistryFactory(adapterRegistry)) + } + + /** + * A registry of [JsonAdapter]s to be registered with a [Moshi] instance. + * + * To use, create a subclass and implement [adapters]. Then add an instance of your subclass on + * [Moshi.Builder.addRegistry]. + * + * Example: + * ``` + * class MyAdapterRegistry : MoshiCompanion.AdapterRegistry { + * + * override val adapters = mapOf( + * MyType::class.java to MyTypeJsonAdapter::class.java, + * MyType.SubType::class.java to MyType_SubTypeJsonAdapter::class.java + * ) + * } + * + * val myRegistry = MyAdapterRegistry() + * + * val builder = Moshi.Builder().addRegistry(myRegistry) + * val moshi = builder.build() + * ``` + */ + interface AdapterRegistry { + + /** + * A map of [Type] to [JsonAdapter] to be registered. + */ + val adapters: Map>> + } +} + +/** + * Add an [AdapterRegistry] on this [Moshi.Builder] instance to register all [JsonAdapter]s + * declared in the [AdapterRegistry.adapters] map when [Moshi.Builder.build] is called. + * @see MoshiCompanion.addRegistry + * @receiver [Moshi.Builder] instance. + * @param adapterRegistry the [AdapterRegistry] instance to set. + * @return [Moshi.Builder] + */ +@JvmSynthetic +fun Builder.addRegistry(adapterRegistry: AdapterRegistry) = apply { + add(AdapterRegistryFactory(adapterRegistry)) +} + +/** + * Returns a JSON adapter for reified type parameter [T], creating it if necessary. + * + * Usage: + * + * ```kotlin + * val moshi = Moshi.Builder().build() + * val adapter = moshi.typeAdapter>() + * ``` + * @receiver [Moshi] instance. + * @see typeRef + * @return [JsonAdapter]<[T]> + */ +inline fun Moshi.typeAdapter(): JsonAdapter = adapter(typeRef().type) \ No newline at end of file diff --git a/companion-api/src/main/kotlin/com/highcapable/moshi/companion/api/TypeRef.kt b/companion-api/src/main/kotlin/com/highcapable/moshi/companion/api/TypeRef.kt new file mode 100644 index 0000000..52f19c7 --- /dev/null +++ b/companion-api/src/main/kotlin/com/highcapable/moshi/companion/api/TypeRef.kt @@ -0,0 +1,96 @@ +/* + * Moshi Companion - Companion to Moshi with more practical features. + * Copyright (C) 2019 HighCapable + * https://github.com/HighCapable/moshi-companion + * + * Apache License Version 2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file is created by fankes on 2025/10/6. + */ +@file:Suppress("unused", "MemberVisibilityCanBePrivate") + +package com.highcapable.moshi.companion.api + +import com.highcapable.kavaref.extension.classOf +import com.highcapable.kavaref.extension.toClass +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +/** + * Type reference class for getting generic parameter [T] type. + * + * The purpose of this class is to retain erased generics at runtime. + * @see typeRef + */ +abstract class TypeRef { + + /** + * Get the generic parameter [T] type. + * @return [Type] + */ + val type by lazy { + when (val superclass = javaClass.genericSuperclass) { + is ParameterizedType -> + if (superclass.rawType == classOf>()) + superclass.actualTypeArguments.firstOrNull() ?: error("Type argument cannot be null.") + else error("Must only create direct subclasses of TypeRef.") + classOf>() -> error("TypeRef must be created with a type argument: object : TypeRef<...>() {}.") + else -> error("Must only create direct subclasses of TypeRef.") + } + } + + /** + * Get the raw class type of the generic parameter [T]. + * @return [Class] + */ + val rawType by lazy { + when (val currentType = type) { + is Class<*> -> currentType + is ParameterizedType -> currentType.toClass() + else -> classOf() + } + } + + /** + * Checks if the specified [other] type can be assigned to this type. + * @param other the type to check. + * @return `true` if the specified type can be assigned to this type, `false` otherwise. + */ + fun isAssignableFrom(other: Type) = when { + type is Class<*> && other is Class<*> -> rawType.isAssignableFrom(other) + else -> type == other + } + + override fun toString() = type.toString() + override fun equals(other: Any?) = other is TypeRef<*> && type == other.type + override fun hashCode() = type.hashCode() +} + +/** + * Create a [TypeRef] instance with the reified type parameter [T]. + * + * Usage: + * + * ```kotlin + * val typeRef = typeRef>() + * // This will be of type `List`. + * val type = typeRef.type + * // This will be of type `List`. + * val rawType = typeRef.rawType + * ``` + * @see TypeRef + * @return [TypeRef]<[T]> + */ +inline fun typeRef() = object : TypeRef() {} \ No newline at end of file diff --git a/companion-codegen/build.gradle.kts b/companion-codegen/build.gradle.kts new file mode 100644 index 0000000..7611e58 --- /dev/null +++ b/companion-codegen/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.ksp) + alias(libs.plugins.maven.publish) +} + +group = property.project.groupName +version = property.project.version + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +kotlin { + jvmToolchain(17) + compilerOptions { + freeCompilerArgs = listOf( + "-Xno-param-assertions", + "-Xno-call-assertions", + "-Xno-receiver-assertions" + ) + } +} + +dependencies { + compileOnly(libs.ksp.api) + + ksp(libs.auto.service.ksp) + + implementation(libs.moshi.kotlin) + implementation(libs.auto.service.annotations) + implementation(libs.kotlinpoet) + implementation(libs.kotlinpoet.ksp) +} \ No newline at end of file diff --git a/companion-codegen/src/main/kotlin/com/highcapable/moshi/companion/codegen/DeclaredSymbol.kt b/companion-codegen/src/main/kotlin/com/highcapable/moshi/companion/codegen/DeclaredSymbol.kt new file mode 100644 index 0000000..9288b11 --- /dev/null +++ b/companion-codegen/src/main/kotlin/com/highcapable/moshi/companion/codegen/DeclaredSymbol.kt @@ -0,0 +1,48 @@ +/* + * Moshi Companion - Companion to Moshi with more practical features. + * Copyright (C) 2019 HighCapable + * https://github.com/HighCapable/moshi-companion + * + * Apache License Version 2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file is created by fankes on 2025/10/2. + */ +@file:Suppress("unused", "MemberVisibilityCanBePrivate") + +package com.highcapable.moshi.companion.codegen + +object DeclaredSymbol { + + const val MOSHI_PACKAGE_NAME = "com.squareup.moshi" + + const val MOSHI_CLASS_NAME = "Moshi" + const val MOSHI_CLASS = "$MOSHI_PACKAGE_NAME.$MOSHI_CLASS_NAME" + + const val JSON_ANNOTATION_CLASS_NAME = "JsonClass" + const val JSON_ANNOTATION_CLASS = "$MOSHI_PACKAGE_NAME.$JSON_ANNOTATION_CLASS_NAME" + + const val JSON_ADAPTER_CLASS_NAME = "JsonAdapter" + const val JSON_ADAPTER_CLASS = "$MOSHI_PACKAGE_NAME.$JSON_ADAPTER_CLASS_NAME" + + const val MOSHI_COMPANION_API_PACKAGE_NAME = "com.highcapable.moshi.companion.api" + + const val MOSHI_COMPANION_CLASS_NAME = "MoshiCompanion" + const val MOSHI_COMPANION_CLASS = "$MOSHI_COMPANION_API_PACKAGE_NAME.$MOSHI_COMPANION_CLASS_NAME" + + const val TYPE_REF_CLASS_NAME = "TypeRef" + const val TYPE_REF_CLASS = "$MOSHI_COMPANION_API_PACKAGE_NAME.$TYPE_REF_CLASS_NAME" + + const val ADAPTER_REGISTRY_CLASS_NAME = "AdapterRegistry" +} \ No newline at end of file diff --git a/companion-codegen/src/main/kotlin/com/highcapable/moshi/companion/codegen/MoshiCompanionProcessor.kt b/companion-codegen/src/main/kotlin/com/highcapable/moshi/companion/codegen/MoshiCompanionProcessor.kt new file mode 100644 index 0000000..245ae09 --- /dev/null +++ b/companion-codegen/src/main/kotlin/com/highcapable/moshi/companion/codegen/MoshiCompanionProcessor.kt @@ -0,0 +1,51 @@ +/* + * Moshi Companion - Companion to Moshi with more practical features. + * Copyright (C) 2019 HighCapable + * https://github.com/HighCapable/moshi-companion + * + * Apache License Version 2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file is created by fankes on 2025/10/2. + */ +@file:Suppress("unused") + +package com.highcapable.moshi.companion.codegen + +import com.google.auto.service.AutoService +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider +import com.google.devtools.ksp.symbol.KSAnnotated +import com.highcapable.moshi.companion.codegen.subprocessor.AdapterRegistryGenerator + +@AutoService(SymbolProcessorProvider::class) +class MoshiCompanionProcessor : SymbolProcessorProvider { + + override fun create(environment: SymbolProcessorEnvironment) = object : SymbolProcessor { + + private val subProcessor = listOf( + AdapterRegistryGenerator(environment) + ) + + override fun process(resolver: Resolver) = emptyList().let { startProcess(resolver); it } + + private fun startProcess(resolver: Resolver) { + subProcessor.forEach { + it.startProcess(resolver) + } + } + } +} \ No newline at end of file diff --git a/companion-codegen/src/main/kotlin/com/highcapable/moshi/companion/codegen/Options.kt b/companion-codegen/src/main/kotlin/com/highcapable/moshi/companion/codegen/Options.kt new file mode 100644 index 0000000..ed4d282 --- /dev/null +++ b/companion-codegen/src/main/kotlin/com/highcapable/moshi/companion/codegen/Options.kt @@ -0,0 +1,64 @@ +/* + * Moshi Companion - Companion to Moshi with more practical features. + * Copyright (C) 2019 HighCapable + * https://github.com/HighCapable/moshi-companion + * + * Apache License Version 2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file is created by fankes on 2025/10/3. + */ +package com.highcapable.moshi.companion.codegen + +object Options { + + /** + * This boolean processing option is used to read the parameters of the Moshi + * ontology project "moshi-kotlin-codegen", which should be configured by the user as "false". + */ + const val OPTION_GENERATE_PROGUARD_RULES_MOSHI = "moshi.generateProguardRules" + + /** + * This string processing option can change the package name of the generated AdapterRegistry implementation class. + * Default is `com.highcapable.moshi.companion.generated` + no repetition package name hash. + */ + const val OPTION_GENERATE_ADAPTER_REGISTRY_PACKAGE_NAME = "moshi-companion.generateAdapterRegistryPackageName" + + /** + * This string processing option can change the class name of the generated AdapterRegistry implementation class. + * Default is `DefaultMoshiAdapterRegistry`. + */ + const val OPTION_GENERATE_ADAPTER_REGISTRY_CLASS_NAME = "moshi-companion.generateAdapterRegistryClassName" + + /** + * This boolean processing option can change the access modifier of the generated AdapterRegistry implementation class. + * If true, the class will have internal access; if false, it will have public access. + * This is disabled by default. + */ + const val OPTION_GENERATE_ADAPTER_REGISTRY_RESTRICTED_ACCESS = "moshi-companion.generateAdapterRegistryRestrictedAccess" + + /** + * This boolean processing option can disable proguard rule generation. + * Normally, this is not recommended unless end-users build their own JsonAdapter look-up tool. + * This is enabled by default. + */ + const val OPTION_GENERATE_PROGUARD_RULES = "moshi-companion.generateProguardRules" + + /** + * This boolean processing option can enable keeping enum classes values method in proguard rules. + * Normally, this is not recommended unless you are sure that you don't need enum values method. + * This is enabled by default. + */ + const val OPTION_PROGUARD_RULES_KEEP_ENUM_CLASSES = "moshi-companion.proguardRulesKeepEnumClasses" +} \ No newline at end of file diff --git a/companion-codegen/src/main/kotlin/com/highcapable/moshi/companion/codegen/extension/MoshiCompationExtenstion.kt b/companion-codegen/src/main/kotlin/com/highcapable/moshi/companion/codegen/extension/MoshiCompationExtenstion.kt new file mode 100644 index 0000000..4dc7e86 --- /dev/null +++ b/companion-codegen/src/main/kotlin/com/highcapable/moshi/companion/codegen/extension/MoshiCompationExtenstion.kt @@ -0,0 +1,40 @@ +/* + * Moshi Companion - Companion to Moshi with more practical features. + * Copyright (C) 2019 HighCapable + * https://github.com/HighCapable/moshi-companion + * + * Apache License Version 2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file is created by fankes on 2025/10/2. + */ +package com.highcapable.moshi.companion.codegen.extension + +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.getAnnotationsByType +import com.google.devtools.ksp.symbol.KSAnnotated +import java.security.MessageDigest + +@OptIn(KspExperimental::class) +inline fun KSAnnotated.findAnnotationWithType() = getAnnotationsByType(T::class).firstOrNull() + +object HashString { + + private val md = MessageDigest.getInstance("SHA-256") + + fun generate(input: String, length: Int = 16): String { + val digest = md.digest(input.toByteArray()) + return digest.joinToString("") { "%02x".format(it) }.take(length) + } +} \ No newline at end of file diff --git a/companion-codegen/src/main/kotlin/com/highcapable/moshi/companion/codegen/subprocessor/AdapterRegistryGenerator.kt b/companion-codegen/src/main/kotlin/com/highcapable/moshi/companion/codegen/subprocessor/AdapterRegistryGenerator.kt new file mode 100644 index 0000000..d3ddd6b --- /dev/null +++ b/companion-codegen/src/main/kotlin/com/highcapable/moshi/companion/codegen/subprocessor/AdapterRegistryGenerator.kt @@ -0,0 +1,326 @@ +/* + * Moshi Companion - Companion to Moshi with more practical features. + * Copyright (C) 2019 HighCapable + * https://github.com/HighCapable/moshi-companion + * + * Apache License Version 2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file is created by fankes on 2025/10/2. + */ +package com.highcapable.moshi.companion.codegen.subprocessor + +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSDeclaration +import com.google.devtools.ksp.symbol.KSFile +import com.highcapable.moshi.companion.codegen.DeclaredSymbol +import com.highcapable.moshi.companion.codegen.Options +import com.highcapable.moshi.companion.codegen.extension.HashString +import com.highcapable.moshi.companion.codegen.extension.findAnnotationWithType +import com.highcapable.moshi.companion.codegen.subprocessor.base.BaseSymbolProcessor +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.STAR +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.WildcardTypeName +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.ksp.toClassName +import com.squareup.kotlinpoet.ksp.writeTo +import com.squareup.moshi.JsonClass +import java.io.OutputStreamWriter +import java.nio.charset.StandardCharsets + +class AdapterRegistryGenerator(override val environment: SymbolProcessorEnvironment) : BaseSymbolProcessor(environment) { + + private companion object { + + private const val ADAPTER_REGISTRY_PACKAGE_NAME_SUFFIX = "generated" + private const val DEFAULT_ADAPTER_REGISTRY_PACKAGE_NAME_PREFIX = "com.highcapable.moshi.companion" + private const val DEFAULT_ADAPTER_REGISTRY_CLASS_NAME = "DefaultMoshiAdapterRegistry" + + val JsonAdapterClass = ClassName(DeclaredSymbol.MOSHI_PACKAGE_NAME, DeclaredSymbol.JSON_ADAPTER_CLASS_NAME) + val AdapterRegistryClass = ClassName(DeclaredSymbol.MOSHI_COMPANION_API_PACKAGE_NAME, DeclaredSymbol.MOSHI_COMPANION_CLASS_NAME) + .nestedClass(DeclaredSymbol.ADAPTER_REGISTRY_CLASS_NAME) + val TypeClass = ClassName("java.lang.reflect", "Type") + } + + private val generateAdapterRegistryPackageName = environment.options[Options.OPTION_GENERATE_ADAPTER_REGISTRY_PACKAGE_NAME] + private val generateAdapterRegistryClassName = environment.options[Options.OPTION_GENERATE_ADAPTER_REGISTRY_CLASS_NAME] + private val generateAdapterRegistryRestrictedAccess = environment.options[Options.OPTION_GENERATE_ADAPTER_REGISTRY_RESTRICTED_ACCESS] + ?.toBooleanStrictOrNull() ?: false + private val generateProguardRulesMoshi = environment.options[Options.OPTION_GENERATE_PROGUARD_RULES_MOSHI]?.toBooleanStrictOrNull() + private val generateProguardRules = environment.options[Options.OPTION_GENERATE_PROGUARD_RULES]?.toBooleanStrictOrNull() ?: true + private val keepEnumClasses = environment.options[Options.OPTION_PROGUARD_RULES_KEEP_ENUM_CLASSES]?.toBooleanStrictOrNull() ?: true + + private val adapterRegistrations = mutableListOf() + private var originatingFile: KSFile? = null + + override fun startProcess(resolver: Resolver) { + processRegistrations(resolver) + generateCodeFile() + generateProguardFile() + } + + private fun processRegistrations(resolver: Resolver) { + resolver.getSymbolsWithAnnotation(JsonClass::class.qualifiedName!!).forEach { type -> + // For the smart cast. + if (type !is KSDeclaration) return@forEach + + // Use the first obtained source file as the dependency of the generated file. + if (originatingFile == null) + originatingFile = type.containingFile ?: return@forEach + + val jsonClassAnnotation = type.findAnnotationWithType() ?: return@forEach + + if (!jsonClassAnnotation.generateAdapter) return@forEach + + runCatching { + // Collect adapter registration information. + if (type is KSClassDeclaration) { + val targetClassName = type.toClassName() + val adapterClassName = ClassName( + targetClassName.packageName, + "${targetClassName.simpleNames.joinToString("_")}JsonAdapter" + ) + val hasTypeParameters = type.typeParameters.isNotEmpty() + + adapterRegistrations += AdapterRegistration( + targetClass = targetClassName, + adapterClass = adapterClassName, + hasTypeParameters = hasTypeParameters + ) + } + }.onFailure { + logger.error("Error preparing ${type.simpleName.asString()}\n${it.stackTraceToString()}") + } + } + } + + private fun generateCodeFile() { + if (adapterRegistrations.isNotEmpty()) { + val fileSpec = generateAdapterRegistry() + + try { + fileSpec.writeTo(codeGenerator, aggregating = true) + } catch (_: FileAlreadyExistsException) { + // Ignored, FileAlreadyExistsException can happen if multiple compilations. + } + } + } + + private fun generateProguardFile() { + if (generateProguardRules) require(generateProguardRulesMoshi == false) { + "Proguard rules generation for Moshi is enabled. " + + "Please disable it by setting the \"${Options.OPTION_GENERATE_PROGUARD_RULES_MOSHI}\" option to \"false\" " + + "in your build configuration to avoid duplicate rules." + } + + if (generateProguardRules) { + val config = ProguardConfig(adapterRegistrations, keepEnumClasses) + + try { + config.writeTo(codeGenerator) + } catch (_: FileAlreadyExistsException) { + // Ignored, FileAlreadyExistsException can happen if multiple compilations. + } + } + } + + private fun generateAdapterRegistry(): FileSpec { + // Get the top-level package name of the project. + val packageName = run { + adapterRegistrations + .filterByPackageNameFirstOrNull() + ?.targetClass?.packageName + } ?: "default" // fallback package name + + val registryPackageName = (generateAdapterRegistryPackageName ?: run { + val packageHash = HashString.generate(packageName) + "$DEFAULT_ADAPTER_REGISTRY_PACKAGE_NAME_PREFIX.r$packageHash" + }) + ".$ADAPTER_REGISTRY_PACKAGE_NAME_SUFFIX" + + val adaptersMapBuilder = CodeBlock.builder() + adaptersMapBuilder.add("\nmapOf(") + + adapterRegistrations.forEachIndexed { index, registration -> + if (index > 0) adaptersMapBuilder.add(",") + adaptersMapBuilder.add( + "\n %T::class.java to %T::class.java", + registration.targetClass, + registration.adapterClass + ) + } + + if (adapterRegistrations.isNotEmpty()) + adaptersMapBuilder.add("\n") + + adaptersMapBuilder.add(")") + + val adaptersProperty = PropertySpec.builder( + name = "adapters", + Map::class.asClassName().parameterizedBy( + TypeClass, Class::class.asClassName().parameterizedBy( + WildcardTypeName.producerOf(JsonAdapterClass.parameterizedBy(STAR)) + ) + ) + ).apply { + addModifiers(KModifier.OVERRIDE) + initializer(adaptersMapBuilder.build()) + }.build() + + val adapterRegistryClassName = generateAdapterRegistryClassName ?: DEFAULT_ADAPTER_REGISTRY_CLASS_NAME + val registryClass = TypeSpec.classBuilder(adapterRegistryClassName).apply { + if (generateAdapterRegistryRestrictedAccess) + addModifiers(KModifier.INTERNAL) + else addModifiers(KModifier.PUBLIC) + addSuperinterface(AdapterRegistryClass) + addProperty(adaptersProperty) + }.build() + + val fileSpec = FileSpec.builder(registryPackageName, adapterRegistryClassName).apply { + addFileComment( + """ + This file is auto generated by Moshi Companion CodeGen. + **DO NOT EDIT THIS FILE MANUALLY** + """.trimIndent() + ) + addType(registryClass) + }.build() + + return fileSpec + } + + private fun List.filterByPackageNameFirstOrNull(): AdapterRegistration? { + val registrations = filter { it.targetClass.packageName.isNotEmpty() } + if (registrations.isEmpty()) return null + + // Count the number of prefixes for each class name. + val prefixCount = mutableMapOf() + val splitMap = registrations.associateWith { it.targetClass.packageName.split(".") } + + splitMap.values.forEach { parts -> + val prefix = parts.take(3).joinToString(".") + prefixCount[prefix] = (prefixCount[prefix] ?: 0) + 1 + } + + // Find the prefix with the least number of occurrences. + val oddPrefix = prefixCount.minByOrNull { it.value }?.key + + // Check whether there are different class names. + val oddClassName = splitMap.filter { it.value.take(3).joinToString(".") == oddPrefix }.keys.firstOrNull() + if (oddClassName != null && prefixCount[oddPrefix] == 1) return oddClassName + + // If there is no different style, return the shortest class name. + return registrations.minByOrNull { it.targetClass.packageName.length } + } + + private fun ProguardConfig.writeTo(codeGenerator: CodeGenerator) { + val originatingFile = originatingFile ?: return + + val file = codeGenerator.createNewFile( + dependencies = Dependencies(aggregating = true, originatingFile), + packageName = "", + fileName = outputFilePathWithoutExtension, + extensionName = "pro" + ) + OutputStreamWriter(file, StandardCharsets.UTF_8).use(::writeTo) + } + + private data class AdapterRegistration( + val targetClass: ClassName, + val adapterClass: ClassName, + val hasTypeParameters: Boolean + ) + + private data class ProguardConfig( + private val registrations: List, + private val keepEnumClasses: Boolean + ) { + + private companion object { + + const val DEFAULT_OUTPUT_FILE_PATH = "META-INF/proguard/moshi-companion" + } + + val outputFilePathWithoutExtension + get() = "$DEFAULT_OUTPUT_FILE_PATH-r${HashString.generate(registrations.first().targetClass.canonicalName)}" + + fun writeTo(out: Appendable) = out.run { + // Keep the `DefaultConstructorMarker` class, needed for synthetic constructors with default parameters. + appendLine("-keepnames class kotlin.jvm.internal.DefaultConstructorMarker") + appendLine() + + // Keep the `JsonClass` annotation on the target class. + appendLine("-keep,allowobfuscation @${DeclaredSymbol.JSON_ANNOTATION_CLASS} class *") + appendLine() + + // Keep the `TypeRef` class and its subclasses. + appendLine("-keep,allowobfuscation class ${DeclaredSymbol.TYPE_REF_CLASS} {") + appendLine(" ;") + appendLine(" ;") + appendLine("}") + appendLine() + + appendLine("-keep,allowobfuscation class * extends ${DeclaredSymbol.TYPE_REF_CLASS}") + appendLine() + + // Keep generic signatures. + appendLine("-keepattributes Signature") + appendLine() + + // Keep the enum values method. + if (keepEnumClasses) { + appendLine("-keepclassmembers enum * {") + appendLine(" public static **[] values();") + appendLine(" public static ** valueOf(java.lang.String);") + appendLine(" public static ;") + appendLine("}") + appendLine() + } + + registrations.forEach { registration -> + val targetName = registration.targetClass.reflectionName() + val adapterCanonicalName = ClassName( + registration.targetClass.packageName, + registration.adapterClass.simpleName + ).canonicalName + + // Keep the constructor for Moshi's reflective lookup. + appendLine("-if class $targetName") + appendLine("-keepclassmembers class $adapterCanonicalName {") + + if (registration.hasTypeParameters) + appendLine(" public (${DeclaredSymbol.MOSHI_CLASS}, ${TypeClass.canonicalName}[]);") + else appendLine(" public (${DeclaredSymbol.MOSHI_CLASS});") + + appendLine("}") + appendLine() + + // Keep the synthetic constructor if the target class has default parameter values. + appendLine("-keepclassmembers class $targetName {") + appendLine(" public synthetic (...);") + appendLine("}") + appendLine() + } + } + } +} \ No newline at end of file diff --git a/companion-codegen/src/main/kotlin/com/highcapable/moshi/companion/codegen/subprocessor/base/BaseSymbolProcessor.kt b/companion-codegen/src/main/kotlin/com/highcapable/moshi/companion/codegen/subprocessor/base/BaseSymbolProcessor.kt new file mode 100644 index 0000000..d568cc7 --- /dev/null +++ b/companion-codegen/src/main/kotlin/com/highcapable/moshi/companion/codegen/subprocessor/base/BaseSymbolProcessor.kt @@ -0,0 +1,33 @@ +/* + * Moshi Companion - Companion to Moshi with more practical features. + * Copyright (C) 2019 HighCapable + * https://github.com/HighCapable/moshi-companion + * + * Apache License Version 2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file is created by fankes on 2025/10/2. + */ +package com.highcapable.moshi.companion.codegen.subprocessor.base + +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment + +abstract class BaseSymbolProcessor(protected open val environment: SymbolProcessorEnvironment) { + + protected val logger by lazy { environment.logger } + protected val codeGenerator by lazy { environment.codeGenerator } + + abstract fun startProcess(resolver: Resolver) +} \ No newline at end of file diff --git a/docs/changelog-zh-CN.md b/docs/changelog-zh-CN.md new file mode 100644 index 0000000..542bc89 --- /dev/null +++ b/docs/changelog-zh-CN.md @@ -0,0 +1,5 @@ +# 更新日志 + +## 1.0.0 | 2025.10.07 + +- 首个版本提交至 Maven \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..00d53a2 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,5 @@ +# Changelog + +## 1.0.0 | 2025.10.07 + +- The first version is submitted to Maven \ No newline at end of file diff --git a/docs/guide-zh-CN.md b/docs/guide-zh-CN.md new file mode 100644 index 0000000..7e450a7 --- /dev/null +++ b/docs/guide-zh-CN.md @@ -0,0 +1,207 @@ +# Moshi Companion 使用文档 + +![Maven Central](https://img.shields.io/maven-central/v/com.highcapable.moshi.companion/companion-api?logo=apachemaven&logoColor=orange&style=flat-square) + +在开始使用之前,建议你仔细阅读此文档,以便你能更好地了解它的作用方式与功能。 + +你可以在项目的根目录找到 samples 中的 Demo,并参考此文档食用效果更佳。 + +## 开始之前 + +此项目的主要功能是为 [Moshi](https://github.com/square/moshi) 提供伴侣功能,核心功能依赖于 Moshi 项目核心完成,你需要使用 Moshi 的 `moshi-kotlin` 和 `moshi-kotlin-codegen` 依赖来生成适配器类。 + +此项目的目的是将 `moshi-kotlin-codegen` 生成的适配器类生成 "实体类 → 适配器类" 的映射注册到 `AdapterRegistry` 中并创建自定义的 `JsonAdapter` 设置到 `Moshi.Builder`,从而避免 Moshi 通过 `Class.forName` 反射查找适配器类,达到完全混淆实体类名称和字段的目的,同时性能将由 O(n) 提升到 O(1)。 + +此项目主要专注于 Android 项目,在纯 Kotlin/Java 项目中依然可以使用。 + +## 快速开始 + +首先,在你的 Android/Kotlin/Java 项目中添加依赖,我们推荐直接使用 Gradle 的 Version Catalog 功能来管理依赖版本: + +> `gradle/libs.versions.toml` + +```toml +[versions] +agp = "8.13.0" +# Kotlin 相关依赖版本可从 https://kotlinlang.org/docs/releases.html 获取 +kotlin = "2.2.20" +ksp = "2.2.20-2.0.3" +# Moshi 的版本建议使用 Moshi GitHub README 提供的版本 +moshi = "1.15.2" +moshi-companion = "" + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } + +[libraries] +moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } +moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } +moshi-companion-api = { module = "com.highcapable.moshi.companion:companion-api", version.ref = "moshi-companion" } +moshi-companion-codegen = { module = "com.highcapable.moshi.companion:companion-codegen", version.ref = "moshi-companion" } +``` + +将上述 `` 的版本替换为顶部显示的最新版本号。 + +然后,在你需要使用 Moshi 的 Gradle 项目配置文件 `build.gradle` 或 `build.gradle.kts` 中添加如下配置: + +```kotlin +plugins { + // 如果是 Android 项目 + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + // KSP + alias(libs.plugins.kotlin.ksp) +} + +dependencies { + // Moshi 相关依赖 + implementation(libs.moshi.kotlin) + ksp(libs.moshi.kotlin.codegen) + + // Moshi Companion 相关依赖 + implementation(libs.moshi.companion.api) + // Moshi Companion Codegen 需要在 moshi-kotlin-codegen 之后添加,请注意顺序 + ksp(libs.moshi.companion.codegen) +} +``` + +然后我们需要确保关闭 Moshi 的混淆规则生成功能,因为混淆规则已经被 Moshi Companion 接管。 + +```kotlin +ksp { + arg("moshi.generateProguardRules", "false") +} +``` + +如果你没有关闭 Moshi 的混淆规则生成功能,Moshi Companion 将会在编译时抛出异常提示你进行修正。 + +至此,你已经完成了 Moshi Companion 的依赖添加和配置,如果你是在使用 Moshi 的现有项目中添加 Moshi Companion,你只需要引入上述步骤中的 `moshi-companion-api` 和 `moshi-companion-codegen` 依赖,并添加关闭混淆规则生成的配置即可。 + +## 注册适配器 + +Moshi Companion 通过读取项目中所有使用 `@JsonClass(generateAdapter = true)` 注解的类来生成 `AdapterRegistry`,你需要在运行时通过 `Moshi.Builder` 手动注册这些适配器。 + +在执行过一次 Gradle 构建后,Moshi Companion 会在项目的 `build/generated/ksp` 目录下生成 `AdapterRegistry` 类,生成的默认格式为扫描到不重复的一个存在 `@JsonClass(generateAdapter = true)` 注解的包名,使用这个包名转换为 16 位的 Hash 作为当前模块的唯一标识,并生成如下格式的包名: + +``` +com.highcapable.moshi.companion.r + 16 位 Hash + generated +``` + +形如: + +``` +com.highcapable.moshi.companion.r1dd1c7f2a95790d7.generated +``` + +`AdapterRegistry` 的类名固定为 `DefaultMoshiAdapterRegistry`,实现了 `AdapterRegistry` 接口。 + +在使用 Moshi 时,你可以非常简单地使用扩展函数 `addRegistry` 将这个生成的类注册到 `Moshi.Builder` 中: + +```kotlin +val moshi = Moshi.Builder() + .addRegistry(DefaultMoshiAdapterRegistry()) + .build() +``` + +然后,你就可以愉快地继续使用 Moshi 进行 JSON 的序列化和反序列化了,完全不受 R8 的混淆和优化影响,类名可以完全做到安全混淆、压缩体积、减少反射和暴露风险。 + +## 高级用法 + +如果你需要自定义 `AdapterRegistry` 的类名和包名,可以通过在 `build.gradle` 或 `build.gradle.kts` 中添加如下 KSP 参数来实现: + +```kotlin +ksp { + // 自定义 AdapterRegistry 的包名,如果是 Android 项目,推荐直接使用 "android.namespace" + arg("moshi-companion.generateAdapterRegistryPackageName", "com.yourdomain.yourpackage.generated") + // 自定义 AdapterRegistry 的类名 + arg("moshi-companion.generateAdapterRegistryClassName", "YourCustomMoshiAdapterRegistry") +} +``` + +形如: + +``` +com.yourdomain.yourpackage.generated.YourCustomMoshiAdapterRegistry +``` + +如果你会维护一个专注于数据模型的模块化项目,我们建议像上述示例这样固定生成的 `AdapterRegistry` 包名和类名,防止自动生成的内容不符合你的项目需求。 + +如果你需要生成仅用于当前项目可访问的 `AdapterRegistry`,可以通过添加如下 KSP 参数来实现: + +```kotlin +ksp { + arg("moshi-companion.generateAdapterRegistryRestrictedAccess", "true") +} +``` + +这样,生成的 `AdapterRegistry` 类将被设置为 `internal`,只能在当前模块中访问,从而避免被其它项目误用或滥用。 + +Moshi Companion 自动生成的混淆规则包含了对 Enum 类的键值混淆保护功能,默认情况下仅对类名进行混淆。 + +请注意 Moshi 的默认行为是对 `@JsonClass(generateAdapter = false)` 注解的 Enum 类才不会进行混淆,在使用 Moshi Companion 后,所有 Enum 类均会被保护不被混淆。 + +如果你不需要这个功能,可以通过添加如下 KSP 参数来关闭 (不建议关闭): + +```kotlin +ksp { + arg("moshi-companion.proguardRulesKeepEnumClasses", "false") +} +``` + +如果你不希望 Moshi Companion 自动生成任何混淆规则,也可以通过添加如下 KSP 参数来关闭 (不建议关闭): + +```kotlin +ksp { + arg("moshi-companion.generateProguardRules", "false") +} +``` + +## 扩展 API + +Moshi Companion 提供了 `TypeRef` 类来简化 `Types.newParameterizedType` 的使用,它的启发和实践来自老牌 [Gson](https://github.com/google/gson) 项目的 `TypeToken`,你可以通过继承 `TypeRef` 来创建一个类型引用,然后通过 `type` 属性获取 `Type` 对象。 + +```kotlin +val typeRef = typeRef>() +// 获取 Type 对象,即 List +val type = typeRef.type +// 获取原始对象,即 List +val rawType = typeRef.rawType +``` + +你可以直接将获取到的 `type` 对象传递给 `Moshi.adapter` 方法来获取对应的 `JsonAdapter`。 + +```kotlin +val adapter = moshi.adapter>(typeRef.type) +``` + +当然,你也可以不需要写的这么复杂,你可以直接使用 Moshi Companion 提供的 `typeAdapter` 扩展函数来简化这个过程: + +```kotlin +val adapter = moshi.typeAdapter>() +// 对比原版写法 +val type = Types.newParameterizedType(List::class.java, YourDataClass::class.java) +val adapter = moshi.adapter>(type) +``` + +`TypeRef` 已经在 Moshi Companion 默认生成的混淆规则中被处理,完全不受 R8 的混淆和优化影响,同样地,泛型类的类名可以完全做到安全混淆、压缩体积、减少反射和暴露风险。 + +## 故障排查 + +如果你没有禁用 Moshi Companion 的混淆规则生成,但是混淆规则没有被加入到 `shrink-rules`,你可以在 R8 结束后检查生成的 `configuration.txt` 文件,查看是否包含 "JsonAdapter"。 + +目前在 Android 项目中这个问题可能出现在项目主模块 (例如 "app"),如果混淆规则没生效,请查看 `build/generated/ksp/release/resources/META-INF/proguard/` 下有没有规则文件,如果存在,那么请在 `build.gradle.kts` 的 `buildTypes` 中添加如下配置: + +```kotlin +release { + isMinifyEnabled = true + + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", + // 指定 Moshi Companion 生成的混淆规则文件 + file("build/generated/ksp/release/resources/META-INF/proguard/").listFiles()!!.first() + ) +} +``` \ No newline at end of file diff --git a/docs/guide.md b/docs/guide.md new file mode 100644 index 0000000..cb24269 --- /dev/null +++ b/docs/guide.md @@ -0,0 +1,207 @@ +# Moshi Companion Documentation + +![Maven Central](https://img.shields.io/maven-central/v/com.highcapable.moshi.companion/companion-api?logo=apachemaven&logoColor=orange&style=flat-square) + +Before you start using it, it is recommended that you read this document carefully so that you can better understand how it works and its functions. + +You can find the demo in samples in the root directory of the project, and refer to this document for better use. + +## Before You Begin + +The main function of this project is to provide companion features for [Moshi](https://github.com/square/moshi). The core functionality depends on the Moshi project core. You need to use Moshi's `moshi-kotlin` and `moshi-kotlin-codegen` dependencies to generate adapter classes. + +The purpose of this project is to generate "Entity Class → Adapter Class" mappings for the adapter classes generated by `moshi-kotlin-codegen` and register them to the `AdapterRegistry`, then create a custom `JsonAdapter` and set it to the `Moshi.Builder`. This avoids Moshi using `Class.forName` reflection to find adapter classes, achieving complete obfuscation of entity class names and fields, while improving performance from O(n) to O(1). + +This project primarily focuses on Android projects but can still be used in pure Kotlin/Java projects. + +## Quick Start + +First, add dependencies to your Android/Kotlin/Java project. We recommend using Gradle's Version Catalog feature to manage dependency versions: + +> `gradle/libs.versions.toml` + +```toml +[versions] +agp = "8.13.0" +# Kotlin related dependency versions can be obtained from https://kotlinlang.org/docs/releases.html +kotlin = "2.2.20" +ksp = "2.2.20-2.0.3" +# Moshi version is recommended to use the version provided by Moshi GitHub README +moshi = "1.15.2" +moshi-companion = "" + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } + +[libraries] +moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } +moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } +moshi-companion-api = { module = "com.highcapable.moshi.companion:companion-api", version.ref = "moshi-companion" } +moshi-companion-codegen = { module = "com.highcapable.moshi.companion:companion-codegen", version.ref = "moshi-companion" } +``` + +Replace the above `` with the latest version number shown at the top. + +Then, add the following configuration to your Gradle project configuration file `build.gradle` or `build.gradle.kts` where you need to use Moshi: + +```kotlin +plugins { + // For Android projects + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + // KSP + alias(libs.plugins.kotlin.ksp) +} + +dependencies { + // Moshi related dependencies + implementation(libs.moshi.kotlin) + ksp(libs.moshi.kotlin.codegen) + + // Moshi Companion related dependencies + implementation(libs.moshi.companion.api) + // Moshi Companion Codegen needs to be added after moshi-kotlin-codegen, please note the order + ksp(libs.moshi.companion.codegen) +} +``` + +Then we need to ensure that Moshi's ProGuard rules generation is disabled, as ProGuard rules are now managed by Moshi Companion. + +```kotlin +ksp { + arg("moshi.generateProguardRules", "false") +} +``` + +If you don't disable Moshi's ProGuard rules generation, Moshi Companion will throw an exception at compile time prompting you to make corrections. + +At this point, you have completed the dependency addition and configuration of Moshi Companion. If you are adding Moshi Companion to an existing project that uses Moshi, you only need to introduce the `moshi-companion-api` and `moshi-companion-codegen` dependencies from the steps above and add the configuration to disable ProGuard rules generation. + +## Registering Adapters + +Moshi Companion generates `AdapterRegistry` by reading all classes in the project that use the `@JsonClass(generateAdapter = true)` annotation. You need to manually register these adapters through `Moshi.Builder` at runtime. + +After performing a Gradle build, Moshi Companion will generate an `AdapterRegistry` class in the `build/generated/ksp` directory of the project. The default generated format uses a unique package name found with the `@JsonClass(generateAdapter = true)` annotation, converts this package name to a 16-character hash as the unique identifier for the current module, and generates a package name in the following format: + +``` +com.highcapable.moshi.companion.r + 16-character hash + generated +``` + +Such as: + +``` +com.highcapable.moshi.companion.r1dd1c7f2a95790d7.generated +``` + +The class name of `AdapterRegistry` is fixed as `DefaultMoshiAdapterRegistry` and implements the `AdapterRegistry` interface. + +When using Moshi, you can very simply use the extension function `addRegistry` to register this generated class to `Moshi.Builder`: + +```kotlin +val moshi = Moshi.Builder() + .addRegistry(DefaultMoshiAdapterRegistry()) + .build() +``` + +Then, you can happily continue using Moshi for JSON serialization and deserialization, completely unaffected by R8's obfuscation and optimization. Class names can be safely obfuscated, reducing size, minimizing reflection, and reducing exposure risks. + +## Advanced Usage + +If you need to customize the class name and package name of `AdapterRegistry`, you can achieve this by adding the following KSP parameters in `build.gradle` or `build.gradle.kts`: + +```kotlin +ksp { + // Customize the package name of AdapterRegistry, if it's an Android project, it's recommended to directly use "android.namespace" + arg("moshi-companion.generateAdapterRegistryPackageName", "com.yourdomain.yourpackage.generated") + // Customize the class name of AdapterRegistry + arg("moshi-companion.generateAdapterRegistryClassName", "YourCustomMoshiAdapterRegistry") +} +``` + +Such as: + +``` +com.yourdomain.yourpackage.generated.YourCustomMoshiAdapterRegistry +``` + +If you maintain a modular project focused on data models, we recommend fixing the generated `AdapterRegistry` package name and class name as shown in the example above to prevent automatically generated content from not meeting your project requirements. + +If you need to generate an `AdapterRegistry` that is only accessible to the current project, you can achieve this by adding the following KSP parameter: + +```kotlin +ksp { + arg("moshi-companion.generateAdapterRegistryRestrictedAccess", "true") +} +``` + +This way, the generated `AdapterRegistry` class will be set to `internal` and can only be accessed within the current module, avoiding misuse or abuse by other projects. + +Moshi Companion's automatically generated ProGuard rules include obfuscation protection for Enum class key values. By default, only class names are obfuscated. + +Please note that Moshi's default behavior is to not obfuscate Enum classes only when they are annotated with `@JsonClass(generateAdapter = false)`. When using Moshi Companion, all Enum classes will be protected from obfuscation. + +If you don't need this feature, you can disable it by adding the following KSP parameter (not recommended to disable): + +```kotlin +ksp { + arg("moshi-companion.proguardRulesKeepEnumClasses", "false") +} +``` + +If you don't want Moshi Companion to automatically generate any ProGuard rules, you can also disable it by adding the following KSP parameter (not recommended to disable): + +```kotlin +ksp { + arg("moshi-companion.generateProguardRules", "false") +} +``` + +## Extension API + +Moshi Companion provides a `TypeRef` class to simplify the use of `Types.newParameterizedType`. Its inspiration and practice come from the veteran [Gson](https://github.com/google/gson) project's `TypeToken`. You can create a type reference by extending `TypeRef`, then get the `Type` object through the `type` property. + +```kotlin +val typeRef = typeRef>() +// Get the Type object, i.e., List +val type = typeRef.type +// Get the raw object, i.e., List +val rawType = typeRef.rawType +``` + +You can directly pass the obtained `type` object to the `Moshi.adapter` method to get the corresponding `JsonAdapter`. + +```kotlin +val adapter = moshi.adapter>(typeRef.type) +``` + +Of course, you don't need to write it so complexly. You can directly use the `typeAdapter` extension function provided by Moshi Companion to simplify this process: + +```kotlin +val adapter = moshi.typeAdapter>() +// Compare with the original approach +val type = Types.newParameterizedType(List::class.java, YourDataClass::class.java) +val adapter = moshi.adapter>(type) +``` + +`TypeRef` has been handled in the ProGuard rules generated by default by Moshi Companion and is completely unaffected by R8's obfuscation and optimization. Similarly, generic class names can be safely obfuscated, reducing size, minimizing reflection, and reducing exposure risks. + +## Troubleshooting + +If you haven't disabled Moshi Companion's ProGuard rules generation, but the ProGuard rules are not included in `shrink-rules`, you can check the generated `configuration.txt` file after R8 finishes to see if it contains "JsonAdapter". + +Currently in Android projects, this issue may occur in the main project module (e.g., "app"). If the ProGuard rules don't take effect, please check if there are rule files under `build/generated/ksp/release/resources/META-INF/proguard/`. If they exist, please add the following configuration in the `buildTypes` section of `build.gradle.kts`: + +```kotlin +release { + isMinifyEnabled = true + + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", + // Specify the ProGuard rules file generated by Moshi Companion + file("build/generated/ksp/release/resources/META-INF/proguard/").listFiles()!!.first() + ) +} +``` \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..fd435f1 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,33 @@ +# Compiler Configuration +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +android.nonTransitiveRClass=true +kotlin.code.style=official +# Project Configuration +project.name=moshi-companion +project.url=https://github.com/HighCapable/moshi-companion +project.groupName=com.highcapable.moshi.companion +project.version="1.0.0" +project.android.compileSdk=36 +project.android.minSdk=21 +project.android.targetSdk=36 +project.samples-app.packageName=${project.groupName}.demo +project.samples-app.versionName=universal +project.samples-app.versionCode=1 +# Maven Publish Configuration +SONATYPE_HOST=CENTRAL_PORTAL +RELEASE_SIGNING_ENABLED=true +# Maven POM Configuration +POM_NAME=moshi-companion +POM_DESCRIPTION=Companion to Moshi with more practical features. +POM_URL=https://github.com/HighCapable/moshi-companion +POM_LICENSE_NAME=Apache License 2.0 +POM_LICENSE_URL=https://github.com/HighCapable/moshi-companion/blob/main/LICENSE +POM_LICENSE_DIST=repo +POM_SCM_URL=https://github.com/HighCapable/moshi-companion +POM_SCM_CONNECTION=scm:git:git://github.com/HighCapable/moshi-companion.git +POM_SCM_DEV_CONNECTION=scm:git:ssh://github.com/HighCapable/moshi-companion.git +POM_DEVELOPER_ID=0 +POM_DEVELOPER_NAME=fankes +POM_DEVELOPER_EMAIL=qzmmcn@163.com +POM_DEVELOPER_URL=https://github.com/fankes \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..54eeba1 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,41 @@ +[versions] +agp = "8.13.0" +kotlin = "2.0.10" +ksp = "2.0.10-1.0.24" +auto-service-annotations = "1.1.1" +auto-service-ksp = "1.2.0" +kavaref-core = "1.0.2" +kavaref-extension = "1.0.1" +kotlinpoet = "2.2.0" +moshi = "1.15.2" +maven-publish = "0.34.0" +appcompat = "1.7.1" +coreKtx = "1.17.0" +material = "1.11.0" +junit = "4.13.2" +androidx-test-ext-junit = "1.3.0" +androidx-test-espresso-core = "3.7.0" + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" } + +[libraries] +ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } +auto-service-annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "auto-service-annotations" } +auto-service-ksp = { module = "dev.zacsweers.autoservice:auto-service-ksp", version.ref = "auto-service-ksp" } +kavaref-core = { module = "com.highcapable.kavaref:kavaref-core", version.ref = "kavaref-core" } +kavaref-extension = { module = "com.highcapable.kavaref:kavaref-extension", version.ref = "kavaref-extension" } +moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } +moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } +kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } +kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoet" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +google-material = { module = "com.google.android.material:material", version.ref = "material" } +junit = { module = "junit:junit", version.ref = "junit" } +androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext-junit" } +androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso-core" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..1efc52d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Oct 02 00:48:46 CST 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/app/build.gradle.kts b/samples/app/build.gradle.kts new file mode 100644 index 0000000..fa1209a --- /dev/null +++ b/samples/app/build.gradle.kts @@ -0,0 +1,68 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.ksp) +} + +android { + namespace = property.project.samples.app.packageName + compileSdk = property.project.android.compileSdk + + defaultConfig { + applicationId = property.project.samples.app.packageName + minSdk = property.project.android.minSdk + targetSdk = property.project.android.targetSdk + versionCode = property.project.samples.app.versionCode + versionName = property.project.samples.app.versionName + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + signingConfig = signingConfigs.getByName("debug") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + buildFeatures { + buildConfig = true + viewBinding = true + } + lint { checkReleaseBuilds = false } +} + +ksp { + // Proguard rules that need to be generated with codegen + // are disabled and handed over to companion-codegen for processing. + arg("moshi.generateProguardRules", "false") + + arg("moshi-companion.generateAdapterRegistryPackageName", android.namespace!!) + arg("moshi-companion.generateAdapterRegistryClassName", "SampleAdapterRegistry") +} + +dependencies { + implementation(projects.companionApi) + + implementation(libs.moshi.kotlin) + + // Moshi's codegen must be ensured to be executed before. + ksp(libs.moshi.kotlin.codegen) + ksp(projects.companionCodegen) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.google.material) + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.test.espresso.core) +} \ No newline at end of file diff --git a/samples/app/proguard-rules.pro b/samples/app/proguard-rules.pro new file mode 100644 index 0000000..97127cd --- /dev/null +++ b/samples/app/proguard-rules.pro @@ -0,0 +1,39 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +-ignorewarnings +-optimizationpasses 10 +-dontusemixedcaseclassnames +-dontoptimize +-verbose +-overloadaggressively +-allowaccessmodification +-adaptclassstrings +-adaptresourcefilenames +-adaptresourcefilecontents + +-dontwarn java.lang.reflect.** + +-assumenosideeffects class kotlin.jvm.internal.Intrinsics { + public static *** throwUninitializedProperty(...); + public static *** throwUninitializedPropertyAccessException(...); +} \ No newline at end of file diff --git a/samples/app/src/androidTest/java/com/highcapable/moshi/companion/demo/ExampleInstrumentedTest.kt b/samples/app/src/androidTest/java/com/highcapable/moshi/companion/demo/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..534f899 --- /dev/null +++ b/samples/app/src/androidTest/java/com/highcapable/moshi/companion/demo/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.highcapable.moshi.companion.demo + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.highcapable.yukihookapi", appContext.packageName) + } +} \ No newline at end of file diff --git a/samples/app/src/main/AndroidManifest.xml b/samples/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a520e1c --- /dev/null +++ b/samples/app/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/app/src/main/java/com/highcapable/moshi/companion/demo/MainActivity.kt b/samples/app/src/main/java/com/highcapable/moshi/companion/demo/MainActivity.kt new file mode 100644 index 0000000..b78f6fa --- /dev/null +++ b/samples/app/src/main/java/com/highcapable/moshi/companion/demo/MainActivity.kt @@ -0,0 +1,125 @@ +/* + * Moshi Companion - Companion to Moshi with more practical features. + * Copyright (C) 2019 HighCapable + * https://github.com/HighCapable/moshi-companion + * + * Apache License Version 2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file is created by fankes on 2025/10/2. + */ +package com.highcapable.moshi.companion.demo + +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.highcapable.moshi.companion.api.addRegistry +import com.highcapable.moshi.companion.api.typeAdapter +import com.highcapable.moshi.companion.demo.entity.FoodType +import com.highcapable.moshi.companion.demo.entity.MyFood +import com.highcapable.moshi.companion.demo.entity.ResponseData +import com.highcapable.moshi.companion.demo.generated.SampleAdapterRegistry +import com.squareup.moshi.Moshi + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + enableEdgeToEdge() + setContentView(R.layout.activity_main) + + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + + testData() + } + + private fun testData() { + val moshi = Moshi.Builder() + .addRegistry(SampleAdapterRegistry()) + .build() + + val responseData = ResponseData( + code = 200, + message = "Success", + data = listOf( + "Test1", + "Test2", + "Test3" + ) + ) + + val respAdapter = moshi.typeAdapter>>() + val respJson = respAdapter.indent(" ").toJson(responseData) + + val fromRespJson = """ + { + "code": 200, + "message": "Success", + "data": [ + "Test1", + "Test2", + "Test3" + ] + } + """.trimIndent() + val fromRespData = respAdapter.fromJson(fromRespJson) + + val myFood = MyFood( + type = FoodType.Fruit, + ).apply { + data = MyFood.Info( + name = "Apple", + desc = "A sweet red fruit", + number = listOf(1, 2, 3) + ) + } + + val foodAdapter = moshi.typeAdapter() + val foodJson = foodAdapter.indent(" ").toJson(myFood) + + val fromFoodJson = """ + { + "type": "Fruit", + "data": { + "name": "Apple", + "desc": "A sweet red fruit", + "number": [1, 2, 3] + } + } + """.trimIndent() + val fromFoodData = foodAdapter.fromJson(fromFoodJson) + + MaterialAlertDialogBuilder(this) + .setTitle("Moshi Companion Test Data") + .setMessage( + "ResponseData to JSON:\n$respJson\n\n" + + "ResponseData from JSON:\n$fromRespData\n\n" + + "MyFood to JSON:\n$foodJson\n\n" + + "MyFood from JSON:\n$fromFoodData" + ) + .setPositiveButton("OK") { _, _ -> + finish() + } + .setCancelable(false) + .show() + } +} \ No newline at end of file diff --git a/samples/app/src/main/java/com/highcapable/moshi/companion/demo/entity/TestData.kt b/samples/app/src/main/java/com/highcapable/moshi/companion/demo/entity/TestData.kt new file mode 100644 index 0000000..697da07 --- /dev/null +++ b/samples/app/src/main/java/com/highcapable/moshi/companion/demo/entity/TestData.kt @@ -0,0 +1,57 @@ +/* + * Moshi Companion - Companion to Moshi with more practical features. + * Copyright (C) 2019 HighCapable + * https://github.com/HighCapable/moshi-companion + * + * Apache License Version 2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * This file is created by fankes on 2025/10/7. + */ +package com.highcapable.moshi.companion.demo.entity + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ResponseData( + val code: Int, + val message: String, + val data: T? +) + +@JsonClass(generateAdapter = true) +data class MyFood( + override var type: FoodType = FoodType.Meat +) : Food() { + + @JsonClass(generateAdapter = true) + data class Info( + val name: String, + val desc: String, + val number: List + ) +} + +abstract class Food { + + abstract var type: FoodType + + var data: T? = null +} + +enum class FoodType { + Fruit, + Vegetable, + Meat +} \ No newline at end of file diff --git a/samples/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/samples/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/samples/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/samples/app/src/main/res/drawable/ic_launcher_background.xml b/samples/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/samples/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/app/src/main/res/layout/activity_main.xml b/samples/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..86a5d97 --- /dev/null +++ b/samples/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/samples/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/samples/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/samples/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/samples/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/samples/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/samples/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/samples/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/samples/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/samples/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/samples/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/samples/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/samples/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/samples/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/samples/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/samples/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/samples/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/samples/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/samples/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/samples/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/samples/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/samples/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/samples/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/samples/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/samples/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/samples/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/samples/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/samples/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/samples/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/samples/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/samples/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/samples/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/samples/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/samples/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/samples/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/samples/app/src/main/res/values-night/themes.xml b/samples/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..31132cf --- /dev/null +++ b/samples/app/src/main/res/values-night/themes.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/samples/app/src/main/res/values/colors.xml b/samples/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..c8524cd --- /dev/null +++ b/samples/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/samples/app/src/main/res/values/strings.xml b/samples/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..175b5b9 --- /dev/null +++ b/samples/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Moshi Companion + \ No newline at end of file diff --git a/samples/app/src/main/res/values/themes.xml b/samples/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..d4f4848 --- /dev/null +++ b/samples/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + + +