Initial commit

This commit is contained in:
2025-10-07 07:39:24 +08:00
commit afa0ee5c2e
74 changed files with 3140 additions and 0 deletions

35
.editorconfig Normal file
View File

@@ -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

120
.gitignore vendored Normal file
View File

@@ -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

8
.idea/.gitignore generated vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1 @@
2019 HighCapable

View File

@@ -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.

View File

@@ -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.
*/

View File

@@ -0,0 +1 @@
Companion to Moshi with more practical features.

View File

@@ -0,0 +1 @@
Moshi Companion

View File

@@ -0,0 +1 @@
https://github.com/HighCapable/moshi-companion

View File

@@ -0,0 +1,6 @@
#parse("open-source-license-header")
#if (${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
#end
annotation class ${NAME}

View File

@@ -0,0 +1,7 @@
#parse("open-source-license-header")
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
#end
class ${NAME} {
}

View File

@@ -0,0 +1,6 @@
#parse("open-source-license-header")
#if (${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
#end
data class ${NAME}()

View File

@@ -0,0 +1,7 @@
#parse("open-source-license-header")
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
#end
enum class ${NAME} {
}

View File

@@ -0,0 +1,5 @@
#parse("open-source-license-header")
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
#end

View File

@@ -0,0 +1,7 @@
#parse("open-source-license-header")
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
#end
interface ${NAME} {
}

View File

@@ -0,0 +1,7 @@
#parse("open-source-license-header")
#if (${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
#end
object ${NAME} {
}

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="UnstableApiUsage" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

10
.idea/migrations.xml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

17
.idea/runConfigurations.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

201
LICENSE Normal file
View File

@@ -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.

86
README-zh-CN.md Normal file
View File

@@ -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) | 简体中文
| <img src="https://github.com/HighCapable/.github/blob/main/img-src/logo.jpg?raw=true" width = "30" height = "30" alt="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) 查看历史更新日志
## 项目推广
<!--suppress HtmlDeprecatedAttribute -->
<div align="center">
<h2>嘿,还请君留步!👋</h2>
<h3>这里有 Android 开发工具、UI 设计、Gradle 插件、Xposed 模块和实用软件等相关项目。</h3>
<h3>如果下方的项目能为你提供帮助,不妨为我点个 star 吧!</h3>
<h3>所有项目免费、开源,遵循对应开源许可协议。</h3>
<h1><a href="https://github.com/fankes/fankes/blob/main/project-promote/README-zh-CN.md">→ 查看更多关于我的项目,请点击这里 ←</a></h1>
</div>
## 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

92
README.md Normal file
View File

@@ -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)
| <img src="https://github.com/HighCapable/.github/blob/main/img-src/logo.jpg?raw=true" width = "30" height = "30" alt="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
<!--suppress HtmlDeprecatedAttribute -->
<div align="center">
<h2>Hey, please stay! 👋</h2>
<h3>Here are related projects such as Android development tools, UI design, Gradle plugins, Xposed Modules and practical software. </h3>
<h3>If the project below can help you, please give me a star! </h3>
<h3>All projects are free, open source, and follow the corresponding open source license agreement. </h3>
<h1><a href="https://github.com/fankes/fankes/blob/main/project-promote/README.md">→ To see more about my projects, please click here ←</a></h1>
</div>
## 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

24
build.gradle.kts Normal file
View File

@@ -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<KotlinJvmCompile>().configureEach {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
freeCompilerArgs.addAll(
"-opt-in=kotlin.ExperimentalStdlibApi",
"-Xno-param-assertions",
"-Xno-call-assertions",
"-Xno-receiver-assertions"
)
}
}
}

View File

@@ -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)
}

View File

@@ -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<Annotation>, moshi: Moshi): JsonAdapter<*>? {
val rawType = type.toClassOrNull() ?: return null
// Check if there is annotation for `@JsonClass` and `generateAdapter = true`.
val jsonClass = rawType.getAnnotation(classOf<JsonClass>())
if (jsonClass == null || !jsonClass.generateAdapter) return null
var possiblyFoundAdapter: Class<out JsonAdapter<*>>? = 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<JsonAdapter<*>>() ?: throw ClassNotFoundException()
}
possiblyFoundAdapter = adapterClass
val constructor: ConstructorResolver<out JsonAdapter<*>>
val args: Array<Any>
if (type is ParameterizedType) {
val typeArgs = type.actualTypeArguments
// Common case first.
val twoParams = adapterClass.resolve()
.optional(silent = true)
.constructor {
parameters(Moshi::class, Array<Type>::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)
}
}
}

View File

@@ -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<Type, Class<out JsonAdapter<*>>>
}
}
/**
* 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<List<String>>()
* ```
* @receiver [Moshi] instance.
* @see typeRef
* @return [JsonAdapter]<[T]>
*/
inline fun <reified T : Any> Moshi.typeAdapter(): JsonAdapter<T> = adapter(typeRef<T>().type)

View File

@@ -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<T : Any> {
/**
* Get the generic parameter [T] type.
* @return [Type]
*/
val type by lazy {
when (val superclass = javaClass.genericSuperclass) {
is ParameterizedType ->
if (superclass.rawType == classOf<TypeRef<*>>())
superclass.actualTypeArguments.firstOrNull() ?: error("Type argument cannot be null.")
else error("Must only create direct subclasses of TypeRef.")
classOf<TypeRef<*>>() -> 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<Any>()
}
}
/**
* 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<List<String>>()
* // This will be of type `List<String>`.
* val type = typeRef.type
* // This will be of type `List`.
* val rawType = typeRef.rawType
* ```
* @see TypeRef
* @return [TypeRef]<[T]>
*/
inline fun <reified T : Any> typeRef() = object : TypeRef<T>() {}

View File

@@ -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)
}

View File

@@ -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"
}

View File

@@ -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<KSAnnotated>().let { startProcess(resolver); it }
private fun startProcess(resolver: Resolver) {
subProcessor.forEach {
it.startProcess(resolver)
}
}
}
}

View File

@@ -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"
}

View File

@@ -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 <reified T : Annotation> 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)
}
}

View File

@@ -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<AdapterRegistration>()
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<JsonClass>() ?: 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<AdapterRegistration>.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<String, Int>()
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<AdapterRegistration>,
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(" <fields>;")
appendLine(" <methods>;")
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 <fields>;")
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 <init>(${DeclaredSymbol.MOSHI_CLASS}, ${TypeClass.canonicalName}[]);")
else appendLine(" public <init>(${DeclaredSymbol.MOSHI_CLASS});")
appendLine("}")
appendLine()
// Keep the synthetic constructor if the target class has default parameter values.
appendLine("-keepclassmembers class $targetName {")
appendLine(" public synthetic <init>(...);")
appendLine("}")
appendLine()
}
}
}
}

View File

@@ -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)
}

5
docs/changelog-zh-CN.md Normal file
View File

@@ -0,0 +1,5 @@
# 更新日志
## 1.0.0 | 2025.10.07
- 首个版本提交至 Maven

5
docs/changelog.md Normal file
View File

@@ -0,0 +1,5 @@
# Changelog
## 1.0.0 | 2025.10.07
- The first version is submitted to Maven

207
docs/guide-zh-CN.md Normal file
View File

@@ -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 = "<version>"
[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" }
```
将上述 `<version>` 的版本替换为顶部显示的最新版本号。
然后,在你需要使用 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<List<YourDataClass>>()
// 获取 Type 对象,即 List<YourDataClass>
val type = typeRef.type
// 获取原始对象,即 List
val rawType = typeRef.rawType
```
你可以直接将获取到的 `type` 对象传递给 `Moshi.adapter` 方法来获取对应的 `JsonAdapter`
```kotlin
val adapter = moshi.adapter<List<YourDataClass>>(typeRef.type)
```
当然,你也可以不需要写的这么复杂,你可以直接使用 Moshi Companion 提供的 `typeAdapter` 扩展函数来简化这个过程:
```kotlin
val adapter = moshi.typeAdapter<List<YourDataClass>>()
// 对比原版写法
val type = Types.newParameterizedType(List::class.java, YourDataClass::class.java)
val adapter = moshi.adapter<List<YourDataClass>>(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()
)
}
```

207
docs/guide.md Normal file
View File

@@ -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 = "<version>"
[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 `<version>` 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<List<YourDataClass>>()
// Get the Type object, i.e., List<YourDataClass>
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<List<YourDataClass>>(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<List<YourDataClass>>()
// Compare with the original approach
val type = Types.newParameterizedType(List::class.java, YourDataClass::class.java)
val adapter = moshi.adapter<List<YourDataClass>>(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()
)
}
```

33
gradle.properties Normal file
View File

@@ -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

41
gradle/libs.versions.toml Normal file
View File

@@ -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" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -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

234
gradlew vendored Executable file
View File

@@ -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" "$@"

89
gradlew.bat vendored Normal file
View File

@@ -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

View File

@@ -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)
}

39
samples/app/proguard-rules.pro vendored Normal file
View File

@@ -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(...);
}

View File

@@ -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)
}
}

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.YukiHookApp">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -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<ResponseData<List<String>>>()
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<MyFood>()
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()
}
}

View File

@@ -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<T>(
val code: Int,
val message: String,
val data: T?
)
@JsonClass(generateAdapter = true)
data class MyFood(
override var type: FoodType = FoodType.Meat
) : Food<MyFood.Info>() {
@JsonClass(generateAdapter = true)
data class Info(
val name: String,
val desc: String,
val number: List<Int>
)
}
abstract class Food<T : Any> {
abstract var type: FoodType
var data: T? = null
}
enum class FoodType {
Fruit,
Vegetable,
Meat
}

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.YukiHook" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Moshi Companion</string>
</resources>

View File

@@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.YukiHook" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.YukiHookApp" parent="Base.Theme.YukiHook" />
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -0,0 +1,17 @@
package com.highcapable.moshi.companion.demo
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

44
settings.gradle.kts Normal file
View File

@@ -0,0 +1,44 @@
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
gradlePluginPortal()
}
}
plugins {
id("com.highcapable.sweetproperty") version "1.0.8"
}
sweetProperty {
global {
sourcesCode {
includeKeys(
"^project\\..*\$".toRegex(),
"^gradle\\..*\$".toRegex()
)
isEnableRestrictedAccess = true
}
}
rootProject {
all {
isEnable = false
}
}
}
rootProject.name = "moshi-companion"
include(":samples:app")
include(":companion-api", ":companion-codegen")