Repository: ssseasonnn/Butterfly Branch: master Commit: 29857872ee8b Files: 221 Total size: 299.7 KB Directory structure: gitextract_u_htjb9w/ ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README.zh.md ├── annotation/ │ ├── .gitignore │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── zlc/ │ └── season/ │ └── butterfly/ │ ├── annotation/ │ │ ├── Annotations.kt │ │ └── EvadeData.kt │ └── module/ │ └── Module.kt ├── build.gradle.kts ├── buildLogic/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ └── src/ │ └── main/ │ └── kotlin/ │ ├── BuildLogicPlugin.kt │ └── zlc/ │ └── season/ │ └── buildlogic/ │ └── base/ │ ├── AndroidExtensions.kt │ ├── BaseExtensions.kt │ └── MavenExtensions.kt ├── butterfly/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── zlc/ │ │ └── season/ │ │ └── butterfly/ │ │ ├── Butterfly.kt │ │ ├── ButterflyCore.kt │ │ ├── DestinationHandler.kt │ │ ├── action/ │ │ │ └── Action.kt │ │ ├── core/ │ │ │ ├── BackStackEntryManager.kt │ │ │ ├── EvadeManager.kt │ │ │ ├── GroupEntryManager.kt │ │ │ ├── InterceptorManager.kt │ │ │ ├── ModuleManager.kt │ │ │ └── NavigatorManager.kt │ │ ├── entities/ │ │ │ ├── BackStackEntry.kt │ │ │ ├── DestinationData.kt │ │ │ ├── EvadeData.kt │ │ │ └── GroupEntry.kt │ │ ├── interceptor/ │ │ │ ├── DefaultInterceptor.kt │ │ │ └── Interceptor.kt │ │ ├── internal/ │ │ │ ├── ButterflyFragment.kt │ │ │ ├── ButterflyHelper.kt │ │ │ ├── LogUtil.kt │ │ │ └── Util.kt │ │ ├── launcher/ │ │ │ ├── DestinationLauncher.kt │ │ │ └── DestinationLauncherManager.kt │ │ └── navigator/ │ │ ├── ActionNavigator.kt │ │ ├── ActivityNavigator.kt │ │ ├── ErrorNavigator.kt │ │ ├── Navigator.kt │ │ └── fragment/ │ │ ├── DialogFragmentNavigator.kt │ │ ├── FragmentHelper.kt │ │ ├── FragmentNavigator.kt │ │ ├── FragmentParamUpdatable.kt │ │ ├── NavigatorContext.kt │ │ ├── backstack/ │ │ │ ├── BackstackNavigator.kt │ │ │ ├── BackstackNavigatorHelper.kt │ │ │ ├── ClearTopBackstackNavigator.kt │ │ │ ├── SingleTopBackstackNavigator.kt │ │ │ └── StandardBackstackNavigator.kt │ │ └── group/ │ │ └── GroupNavigator.kt │ └── test/ │ └── java/ │ └── zlc/ │ └── season/ │ └── butterfly/ │ └── ExampleUnitTest.kt ├── butterfly-compose/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src/ │ ├── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── zlc/ │ │ └── season/ │ │ └── butterfly/ │ │ └── compose/ │ │ ├── ComposeDestination.kt │ │ ├── ComposeNavigator.kt │ │ ├── ComposeViewModel.kt │ │ ├── DestinationViewModelStoreOwner.kt │ │ ├── Utils.kt │ │ └── navigator/ │ │ ├── ComposeBackStackNavigator.kt │ │ ├── ComposeGroupNavigator.kt │ │ └── ComposeNavigatorHelper.kt │ └── test/ │ └── java/ │ └── zlc/ │ └── season/ │ └── butterfly/ │ └── compose/ │ └── ExampleUnitTest.kt ├── compiler/ │ ├── .gitignore │ ├── build.gradle.kts │ └── src/ │ └── main/ │ ├── java/ │ │ └── zlc/ │ │ └── season/ │ │ └── butterfly/ │ │ └── compiler/ │ │ ├── Entities.kt │ │ ├── ProcessorProvider.kt │ │ ├── generator/ │ │ │ ├── ComposableGenerator.kt │ │ │ └── ModuleClassGenerator.kt │ │ ├── utils/ │ │ │ ├── KspUtils.kt │ │ │ └── Util.kt │ │ └── visitor/ │ │ ├── DestinationAnnotationVisitor.kt │ │ ├── EvadeAnnotationVisitor.kt │ │ └── EvadeImplAnnotationVisitor.kt │ └── resources/ │ └── META-INF/ │ └── services/ │ └── com.google.devtools.ksp.processing.SymbolProcessorProvider ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── plugin/ │ ├── .gitignore │ ├── build.gradle.kts │ └── src/ │ └── main/ │ ├── java/ │ │ └── zlc/ │ │ └── season/ │ │ └── butterfly/ │ │ └── plugin/ │ │ ├── ButterflyPlugin.kt │ │ ├── ModuleClassVisitorFactory.kt │ │ └── Utils.kt │ └── resources/ │ └── META-INF/ │ └── gradle-plugins/ │ └── io.github.ssseasonnn.butterfly.properties ├── samples/ │ ├── app/ │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── proguard-rules.pro │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── zlc/ │ │ │ └── season/ │ │ │ └── butterflydemo/ │ │ │ ├── ComposeBottomNavigationActivity.kt │ │ │ ├── ComposeDemoActivity.kt │ │ │ ├── DemoApplication.kt │ │ │ ├── DestinationTestActivity.kt │ │ │ ├── EvadeTestActivity.kt │ │ │ ├── FragmentBottomNavigationActivity.kt │ │ │ ├── FragmentDemoActivity.kt │ │ │ ├── Home.kt │ │ │ └── MainActivity.kt │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── ic_dashboard_black_24dp.xml │ │ │ ├── ic_home_black_24dp.xml │ │ │ ├── ic_launcher_background.xml │ │ │ └── ic_notifications_black_24dp.xml │ │ ├── drawable-v24/ │ │ │ └── ic_launcher_foreground.xml │ │ ├── layout/ │ │ │ ├── activity_compose_bottom_navigation.xml │ │ │ ├── activity_compose_demo.xml │ │ │ ├── activity_destination_test.xml │ │ │ ├── activity_evade_test.xml │ │ │ ├── activity_fragment_bottom_navigation.xml │ │ │ ├── activity_fragment_demo.xml │ │ │ └── activity_main.xml │ │ ├── menu/ │ │ │ └── bottom_nav_menu.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ └── xml/ │ │ └── network_security_config.xml │ └── modules/ │ ├── base/ │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── consumer-rules.pro │ │ ├── proguard-rules.pro │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── zlc/ │ │ └── season/ │ │ └── base/ │ │ ├── BaseActivity.kt │ │ ├── BaseFragment.kt │ │ └── Destinations.kt │ ├── compose/ │ │ ├── compose_dashboard/ │ │ │ ├── .gitignore │ │ │ ├── build.gradle.kts │ │ │ ├── consumer-rules.pro │ │ │ ├── proguard-rules.pro │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── AndroidManifest.xml │ │ │ └── java/ │ │ │ └── zlc/ │ │ │ └── season/ │ │ │ └── compose/ │ │ │ └── dashboard/ │ │ │ ├── DashboardScreen.kt │ │ │ └── DashboardViewModel.kt │ │ ├── compose_home/ │ │ │ ├── .gitignore │ │ │ ├── build.gradle.kts │ │ │ ├── consumer-rules.pro │ │ │ ├── proguard-rules.pro │ │ │ └── src/ │ │ │ └── main/ │ │ │ ├── AndroidManifest.xml │ │ │ └── java/ │ │ │ └── zlc/ │ │ │ └── season/ │ │ │ └── compose/ │ │ │ └── home/ │ │ │ └── HomeScreen.kt │ │ └── compose_notifications/ │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── consumer-rules.pro │ │ ├── proguard-rules.pro │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── zlc/ │ │ └── season/ │ │ └── compose/ │ │ └── notifications/ │ │ ├── NotificationsScreen.kt │ │ └── NotificationsViewModel.kt │ ├── feature1/ │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── consumer-rules.pro │ │ ├── proguard-rules.pro │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── zlc/ │ │ │ └── season/ │ │ │ └── feature1/ │ │ │ ├── AFragment.kt │ │ │ ├── BFragment.kt │ │ │ ├── CFragment.kt │ │ │ ├── TestAction.kt │ │ │ ├── TestActivity.kt │ │ │ ├── TestBottomSheetDialogFragment.kt │ │ │ ├── TestDialogFragment.kt │ │ │ ├── TestFragment.kt │ │ │ └── TestResultActivity.kt │ │ └── res/ │ │ ├── layout/ │ │ │ ├── activity_test.xml │ │ │ ├── activity_test_result.xml │ │ │ ├── dialog_test.xml │ │ │ ├── fragment_common.xml │ │ │ └── fragment_test.xml │ │ └── values/ │ │ ├── colors.xml │ │ └── strings.xml │ ├── feature2/ │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── consumer-rules.pro │ │ ├── proguard-rules.pro │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ └── java/ │ │ └── zlc/ │ │ └── season/ │ │ └── bar/ │ │ ├── Colors.kt │ │ ├── ComposeScreenA.kt │ │ ├── ComposeScreenB.kt │ │ ├── ComposeScreenC.kt │ │ └── ScreenViewModels.kt │ └── normal/ │ ├── dashboard/ │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── consumer-rules.pro │ │ ├── proguard-rules.pro │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── zlc/ │ │ │ └── season/ │ │ │ └── dashboard/ │ │ │ ├── DashboardFragment.kt │ │ │ └── DashboardViewModel.kt │ │ └── res/ │ │ └── layout/ │ │ └── fragment_dashboard.xml │ ├── home/ │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── consumer-rules.pro │ │ ├── proguard-rules.pro │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── zlc/ │ │ │ └── season/ │ │ │ └── home/ │ │ │ ├── HomeFragment.kt │ │ │ └── HomeImpl.kt │ │ └── res/ │ │ └── layout/ │ │ └── fragment_home.xml │ └── notifications/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── zlc/ │ │ └── season/ │ │ └── notifications/ │ │ ├── NotificationsFragment.kt │ │ └── NotificationsViewModel.kt │ └── res/ │ └── layout/ │ └── fragment_notifications.xml └── settings.gradle.kts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml .idea .gradle /local.properties /.idea/caches /.idea/libraries /.idea/modules.xml /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store /build /captures .externalNativeBuild .cxx local.properties ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. ## [1.2.0] - 2022-11-19 ### Support Compose ## [1.1.0] - 2022-08-17 ### Support fragment navigation ## [1.0.0] - 2022-03-01 ### First version ================================================ FILE: LICENSE ================================================ 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. ================================================ FILE: README.md ================================================ ![](Butterfly.png) [![](https://jitpack.io/v/ssseasonnn/Butterfly.svg)](https://jitpack.io/#ssseasonnn/Butterfly) # Butterfly *Read this in other languages: [中文](README.zh.md), [English](README.md), [Change Log](CHANGELOG.md)* Butterfly is a versatile component-based navigation framework based on `Coroutine + Annotation + Ksp`. Not only does it support navigation for Activity, Fragment, and DialogFragment, but it also supports navigation for Compose pages. Butterfly supports configuring startup modes for Fragment and Composite navigation, including Standard mode navigation ClearTop clears stack top mode navigation and SingleTop reuse mode navigation. Butterfly provides a unified fallback stack for managing all types of page navigation, using standard fallback APIs to reduce developer workload. Butterfly also provides a powerful framework for inter component communication, allowing for true component decoupling without any dependencies between components. ## Feature List The butterfly provides these features: ✅ Support navigation to Activity
✅ Support navigation to Fragment
✅ Support navigation to DialogFragment
✅ Support navigation to Compose
✅ Support navigation to Action
✅ Support parameter transfer and parse
✅ Support interceptor
✅ Support backstack
✅ Support group manage
✅ Support launch mode,such as SingleTop、ClearTop
✅ Support component communicate
## Installation ```kotlin // Add jitpack repository repositories { maven { url("https://jitpack.io") } } ``` ```kotlin // First, declare the KSP plugin in your top level build.gradle.kts file. plugins { id("com.google.devtools.ksp") version "1.8.10-1.0.9" apply false } // Then, enable KSP in your module-level build.gradle.kts file: plugins { id("com.google.devtools.ksp") } // Then, add Butterfly dependencies. dependencies { implementation("com.github.ssseasonnn.Butterfly:butterfly:1.3.0") ksp("com.github.ssseasonnn.Butterfly:compiler:1.3.0") //for compose implementation("com.github.ssseasonnn.Butterfly:butterfly-compose:1.3.0") } ``` ## Basic Usage ### Navigation Butterfly support navigation for Activity、Fragment and DialogFragment and Compose component. ```kotlin @Destination("test/activity") class TestActivity : AppCompatActivity() @Destination("test/fragment") class TestFragment : Fragment() @Destination("test/dialog") class TestDialogFragment : DialogFragment() @Destination("test/compose") @Composable fun HomeScreen() {} //Navigation Butterfly.of(context).navigate("test/xxx") //Navigation and get result Butterfly.of(context).navigate("test/xxx") { val result = it.getStringExtra("result") binding.tvResult.text = result } ``` ### Communication Butterfly implements component-based communication through `Evade` and `EvadeImpl` annotations. And the communication between components without any dependence is supported by dynamic agent technology. For example, two components, `Home` and `Dashboard`, component `Dashboard` need to call the method in the component `Home`: ```mermaid flowchart TD A(Main App) --> B(Component Home) A(Main App) --> C(Component Dashboard) ``` ```kotlin //Define the Api that needs to access the Home in the component Dashboard and add the @ Evade annotation @Evade interface DashboardCallHomeApi { fun showHome(fragmentManager: FragmentManager, container: Int) } //Implement the corresponding Api in the component Home and add the @ EvadeImpl annotation @EvadeImpl class DashboardCallHomeApiImpl { //The implementation class name must end with Impl val TAG = "home_tag" //For the implementation of HomeApi, the method name and parameters must be the same fun showHome(fragmentManager: FragmentManager, container: Int) { val homeFragment = HomeFragment() fragmentManager.beginTransaction() .replace(container, homeFragment, TAG) .commit() } } //You can then invoke the interface in the component Home in the component Dashboard: val dashboardCallHomeApi = Butterfly.evade() dashboardCallHomeApi.showHome(supportFragmentManager, R.id.container) ``` ## Documentation See examples and browse complete documentation at the Butterfly Wiki: [wiki](https://github.com/ssseasonnn/Butterfly/wiki) If you still have questions, feel free to create a new issue. ## License > ``` > Copyright 2022 Season.Zlc > > 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. > ``` ================================================ FILE: README.zh.md ================================================ ![](Butterfly.png) [![](https://jitpack.io/v/ssseasonnn/Butterfly.svg)](https://jitpack.io/#ssseasonnn/Butterfly) # Butterfly *Read this in other languages: [中文](README.zh.md), [English](README.md), [Change Log](CHANGELOG.md)* Butterfly是一个基于`Coroutine + Annotation注解 + Ksp注解处理器`打造的全能的组件化路由框架。 不仅支持Activity、Fragment、DialogFragment的导航,还支持Compose页面的导航。 Butterfly支持为Fragment和Compose的导航配置启动模式, 包括Standard标准模式导航、 ClearTop清除栈顶模式导航以及SingleTop栈顶复用模式导航。 Butterfly提供统一的回退栈对所有类型的页面导航进行管理,使用标准的回退Api,减轻开发者工作负担。 Butterfly还提供强大的组件间通信框架,可在组件间没有任何依赖情况下通信,实现真正的组件解耦。 ## 功能列表 Butterfly提供以下功能: ✅ 支持导航Activity
✅ 支持导航Fragment
✅ 支持导航DialogFragment
✅ 支持导航Compose
✅ 支持导航Action
✅ 支持导航参数传递和解析
✅ 支持拦截器
✅ 支持回退栈
✅ 支持组管理
✅ 支持启动模式,如SingleTop、ClearTop
✅ 支持组件化通信
## 安装 ```kotlin // 添加jitpack仓库 repositories { maven { url("https://jitpack.io") } } ``` ```kotlin // 首先,在顶级build.gradle.kts文件中声明ksp插件 plugins { id("com.google.devtools.ksp") version "1.8.10-1.0.9" apply false } // 然后,在模块级build.gradle.kts文件中启用ksp: plugins { id("com.google.devtools.ksp") } // 然后,添加 Butterfly 依赖项 dependencies { implementation("com.github.ssseasonnn.Butterfly:butterfly:1.3.0") ksp("com.github.ssseasonnn.Butterfly:compiler:1.3.0") //for compose implementation("com.github.ssseasonnn.Butterfly:butterfly-compose:1.3.0") } ``` ## 基本用法 ### 导航 Butterfly支持Activity、Fragment、DialogFragment和Compose组件的导航。 ```kotlin @Destination("test/activity") class TestActivity : AppCompatActivity() @Destination("test/fragment") class TestFragment : Fragment() @Destination("test/dialog") class TestDialogFragment : DialogFragment() @Destination("test/compose") @Composable fun HomeScreen() {} //导航 Butterfly.of(context).navigate("test/xxx") //导航并获取结果 Butterfly.of(context).navigate("test/xxx") { val result = it.getStringExtra("result") binding.tvResult.text = result } ``` ### 组件间通信 Butterfly通过`Evade`和`EvadeImpl`注解实现组件化通信。并且通过动态代理技术支持组件间在无任何依赖的情况下的通信。 ### 1. 在没有依赖的组件之间通信 例如两个组件`Home`和组件`Dashboard`,组件`Dashboard`需要调用组件`Home`中的方法: ```mermaid flowchart TD A(Main App) --> B(Component Home) A(Main App) --> C(Component Dashboard) ``` ```kotlin //在组件Dashboard中定义需要访问Home对应的Api,并添加@Evade注解 @Evade interface DashboardCallHomeApi { fun showHome(fragmentManager: FragmentManager, container: Int) } //在组件Home中实现对应的Api,并添加@EvadeImpl注解 @EvadeImpl class DashboardCallHomeApiImpl { //实现类名必须以Impl结尾 val TAG = "home_tag" //HomeApi的实现,方法名和方法参数必须相同 fun showHome(fragmentManager: FragmentManager, container: Int) { val homeFragment = HomeFragment() fragmentManager.beginTransaction() .replace(container, homeFragment, TAG) .commit() } } //然后就可以在组件Dashboard中调用组件Home中的接口: val dashboardCallHomeApi = Butterfly.evade() dashboardCallHomeApi.showHome(supportFragmentManager, R.id.container) ``` ## 详细文档 查看示例并浏览完整的文档,请访问Butterfly Wiki:[wiki](https://github.com/ssseasonnn/Butterfly/wiki) 如果您仍有疑问,请随意创建新的issue。 ## License > ``` > Copyright 2022 Season.Zlc > > 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. > ``` ================================================ FILE: annotation/.gitignore ================================================ /build ================================================ FILE: annotation/build.gradle.kts ================================================ plugins { id("kotlin") } group = "com.github.ssseasonnn" ================================================ FILE: annotation/src/main/java/zlc/season/butterfly/annotation/Annotations.kt ================================================ package zlc.season.butterfly.annotation @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) annotation class Destination( val route: String ) @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.CLASS) annotation class Evade( val identity: String = "" ) @Retention(AnnotationRetention.BINARY) @Target(AnnotationTarget.CLASS) annotation class EvadeImpl( val singleton: Boolean = true, val identity: String = "" ) ================================================ FILE: annotation/src/main/java/zlc/season/butterfly/annotation/EvadeData.kt ================================================ package zlc.season.butterfly.annotation data class EvadeData(val cls: Class<*>, val singleton: Boolean) ================================================ FILE: annotation/src/main/java/zlc/season/butterfly/module/Module.kt ================================================ package zlc.season.butterfly.module import zlc.season.butterfly.annotation.EvadeData interface Module { fun getDestination(): Map> fun getEvade(): Map> fun getEvadeImpl(): Map } ================================================ FILE: build.gradle.kts ================================================ @file:Suppress("DSL_SCOPE_VIOLATION", "UnstableApiUsage") plugins { id("build.logic") alias(libs.plugins.application) apply false alias(libs.plugins.library) apply false alias(libs.plugins.kotlin) apply false alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.kotlin.kover) apply false alias(libs.plugins.ksp) apply false alias(libs.plugins.hilt) apply false alias(libs.plugins.butterfly) apply false } tasks.register("clean", Delete::class) { delete(rootProject.buildDir) } ================================================ FILE: buildLogic/.gitignore ================================================ /build ================================================ FILE: buildLogic/build.gradle.kts ================================================ plugins { `kotlin-dsl` } java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } dependencies { implementation("com.android.tools.build:gradle:8.0.2") implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10") } gradlePlugin { plugins.register("buildLogicPlugin") { id = "build.logic" implementationClass = "BuildLogicPlugin" } } ================================================ FILE: buildLogic/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: buildLogic/gradlew ================================================ #!/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/HEAD/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 # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # 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*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC3045 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 \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # 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" "$@" ================================================ FILE: buildLogic/gradlew.bat ================================================ @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=. @rem This is normally unused 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% equ 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% equ 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! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: buildLogic/settings.gradle.kts ================================================ @file:Suppress("DSL_SCOPE_VIOLATION", "UnstableApiUsage") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") dependencyResolutionManagement { repositories { google() mavenCentral() mavenLocal() } } ================================================ FILE: buildLogic/src/main/kotlin/BuildLogicPlugin.kt ================================================ import org.gradle.api.Plugin import org.gradle.api.Project import zlc.season.buildlogic.base.setupMaven class BuildLogicPlugin : Plugin { override fun apply(project: Project) { project.subprojects { setupMaven() } } } ================================================ FILE: buildLogic/src/main/kotlin/zlc/season/buildlogic/base/AndroidExtensions.kt ================================================ @file:Suppress("UnstableApiUsage") package zlc.season.buildlogic.base import com.android.build.api.dsl.CommonExtension import com.android.build.gradle.BaseExtension import com.android.build.gradle.LibraryExtension import com.android.build.gradle.internal.dsl.BaseAppModuleExtension import org.gradle.api.JavaVersion import org.gradle.api.Project fun Project.androidApplication(block: BaseAppModuleExtension.() -> Unit = {}) { configBase().applyAs { block() } configPackaging() } fun Project.androidLibrary(block: LibraryExtension.() -> Unit = {}) { configBase().applyAs { block() } configPackaging() } private fun Project.configBase(): BaseExtension { return android.apply { val libs = getLibs() setCompileSdkVersion(libs.getVersion("sdk-compile-version")) defaultConfig { minSdk = libs.getVersion("sdk-min-version") targetSdk = libs.getVersion("sdk-target-version") vectorDrawables { useSupportLibrary = true } testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() } testOptions { unitTests { isReturnDefaultValues = true isIncludeAndroidResources = true } } } } private fun Project.configPackaging() { android.applyAs> { packaging { resources.excludes.add("META-INF/LICENSE.md") resources.excludes.add("META-INF/LICENSE-notice.md") } } } fun Project.enableCompose() { val libs = getLibs() android.apply { buildFeatures.compose = true composeOptions { kotlinCompilerExtensionVersion = libs.getVersionStr("compose-compiler") } } } fun Project.addDefaultConstant(vararg constantPair: Pair) { android.apply { defaultConfig { val addDefaultConstantLambda = createAddDefaultConstantLambda() constantPair.forEach { addDefaultConstantLambda(it.first, it.second) } } } } ================================================ FILE: buildLogic/src/main/kotlin/zlc/season/buildlogic/base/BaseExtensions.kt ================================================ @file:Suppress("TooManyFunctions", "UnstableApiUsage") package zlc.season.buildlogic.base import com.android.build.gradle.BaseExtension import com.android.build.gradle.LibraryExtension import com.android.build.gradle.internal.dsl.BaseAppModuleExtension import com.android.build.gradle.internal.dsl.BuildType import com.android.build.gradle.internal.dsl.DefaultConfig import com.android.build.gradle.internal.dsl.ProductFlavor import org.gradle.api.Project import org.gradle.api.artifacts.Dependency import org.gradle.api.artifacts.VersionCatalog import org.gradle.api.artifacts.VersionCatalogsExtension import org.gradle.api.artifacts.dsl.DependencyHandler import org.gradle.api.plugins.ExtensionAware import org.gradle.api.provider.Provider import org.gradle.kotlin.dsl.getByType import org.gradle.plugin.use.PluginDependency import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions import java.util.Properties fun Project.getApp(): BaseAppModuleExtension { return extensions.getByType() } fun Project.getLibrary(): LibraryExtension { return extensions.getByType() } fun Project.loadProperties(fileName: String): Properties { return Properties().apply { load(rootProject.file(fileName).reader()) } } fun Project.getLibs(): VersionCatalog { return extensions.getByType().named("libs") } fun VersionCatalog.getVersion(name: String): Int { return findVersion(name).get().toString().toInt() } fun VersionCatalog.getVersionStr(name: String): String { return findVersion(name).get().toString() } fun VersionCatalog.getBundle(name: String): Any { return findBundle(name).get() } fun VersionCatalog.getPlugin(name: String): String { return (findPlugin(name).get() as Provider).get().pluginId } internal val Project.android: BaseExtension get() = extensions.findByName("android") as? BaseExtension ?: error("Project '$name' is not an Android module") internal fun BaseExtension.kotlinOptions(block: KotlinJvmOptions.() -> Unit) { (this as ExtensionAware).extensions.configure("kotlinOptions", block) } internal fun DependencyHandler.implementation(dependencyNotation: Any): Dependency? = add("implementation", dependencyNotation) @Suppress("UNCHECKED_CAST") internal fun Any.applyAs(block: T.() -> Unit) { (this as T).block() } internal fun createAddFlavorConstantLambda(): (ProductFlavor, String, String) -> Unit { return { productFlavor, constantName, constantValue -> productFlavor.manifestPlaceholders[constantName] = constantValue productFlavor.buildConfigField("String", constantName, "\"${constantValue}\"") } } internal fun createAddBuildTypeConstantLambda(): (BuildType, String, String) -> Unit { return { buildType, constantName, constantValue -> buildType.manifestPlaceholders[constantName] = constantValue buildType.buildConfigField("String", constantName, "\"${constantValue}\"") } } internal fun DefaultConfig.createAddDefaultConstantLambda(): (String, String) -> Unit { return { constantName, constantValue -> manifestPlaceholders[constantName] = constantValue buildConfigField("String", constantName, "\"${constantValue}\"") } } ================================================ FILE: buildLogic/src/main/kotlin/zlc/season/buildlogic/base/MavenExtensions.kt ================================================ package zlc.season.buildlogic.base import org.gradle.api.Project import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.maven.MavenPublication import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.create fun Project.setupMaven() { afterEvaluate { plugins.withId("com.android.library") { configPublish("release") } plugins.withId("java") { configPublish("java") } } } private fun Project.configPublish(componentName: String) { apply(plugin = "maven-publish") configure { repositories { mavenLocal() } publications { create("maven") { afterEvaluate { from(components.getByName(componentName)) } } } } } ================================================ FILE: butterfly/.gitignore ================================================ /build ================================================ FILE: butterfly/build.gradle.kts ================================================ import zlc.season.buildlogic.base.androidLibrary @Suppress("DSL_SCOPE_VIOLATION") plugins { alias(libs.plugins.library) alias(libs.plugins.kotlin) alias(libs.plugins.kotlin.parcelize) } androidLibrary { namespace = "zlc.season.butterfly" } dependencies { api(project(":annotation")) api(libs.clarity) api(libs.kotlin.coroutines) api(libs.lifecycle.runtime.ktx) api(libs.fragment.ktx) api(libs.core.ktx) implementation(libs.bundles.unit.test) } ================================================ FILE: butterfly/consumer-rules.pro ================================================ ================================================ FILE: butterfly/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle.kts. # # 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 ================================================ FILE: butterfly/src/main/AndroidManifest.xml ================================================ ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/Butterfly.kt ================================================ package zlc.season.butterfly import android.content.Context import androidx.core.os.bundleOf import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.internal.ButterflyHelper object Butterfly { fun of(context: Context = ButterflyHelper.context): DestinationHandler { return DestinationHandler(context) } // avoid inline val EVADE_LAMBDA: (String, Class<*>) -> Any = { identity, cls -> val real = identity.ifEmpty { cls.simpleName } var request = ButterflyCore.queryEvade(real) if (request.className.isEmpty()) { request = request.copy(className = cls.name) } ButterflyCore.dispatchEvade(request) } inline fun evade( identity: String = "", noinline func: (String, Class<*>) -> Any = EVADE_LAMBDA ): T { return func(identity, T::class.java) as T } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/ButterflyCore.kt ================================================ package zlc.season.butterfly import android.content.Context import android.os.Bundle import zlc.season.butterfly.core.EvadeManager import zlc.season.butterfly.core.InterceptorManager import zlc.season.butterfly.core.ModuleManager import zlc.season.butterfly.core.NavigatorManager import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.entities.EvadeData import zlc.season.butterfly.interceptor.Interceptor import zlc.season.butterfly.module.Module object ButterflyCore { private val moduleManager = ModuleManager() private val interceptorManager = InterceptorManager() private val navigatorManager = NavigatorManager() private val evadeManager = EvadeManager() fun getNavigatorManager() = navigatorManager fun addModuleName(moduleName: String) { try { val cls = Class.forName(moduleName) if (Module::class.java.isAssignableFrom(cls)) { val module = cls.getDeclaredConstructor().newInstance() as Module addModule(module) } } catch (ignore: Exception) { //ignore } } fun addModule(module: Module) = moduleManager.addModule(module) fun removeModule(module: Module) = moduleManager.removeModule(module) fun addInterceptor(interceptor: Interceptor) = interceptorManager.addInterceptor(interceptor) fun removeInterceptor(interceptor: Interceptor) = interceptorManager.removeInterceptor(interceptor) fun queryDestination(route: String): String = moduleManager.queryDestination(route) fun queryEvade(identity: String): EvadeData = moduleManager.queryEvade(identity) suspend fun dispatchNavigate( context: Context, destinationData: DestinationData, interceptorManager: InterceptorManager ): Result { // Use global interceptor before var tempDestinationData = if (destinationData.enableGlobalInterceptor) { this.interceptorManager.intercept(context, destinationData) } else { destinationData } // Then use current navigation interceptor tempDestinationData = interceptorManager.intercept(context, tempDestinationData) return navigatorManager.navigate(context, tempDestinationData) } fun popBack(context: Context, bundle: Bundle): DestinationData? { return navigatorManager.popBack(context, bundle) } fun dispatchEvade(evadeData: EvadeData): Any { return evadeManager.dispatch(evadeData) } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/DestinationHandler.kt ================================================ package zlc.season.butterfly import android.content.Context import android.os.Bundle import androidx.core.os.bundleOf import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import zlc.season.butterfly.core.InterceptorManager import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.interceptor.DefaultInterceptor import zlc.season.butterfly.interceptor.Interceptor import zlc.season.butterfly.internal.ButterflyHelper.findActivity import zlc.season.butterfly.internal.key import zlc.season.butterfly.internal.logd import zlc.season.butterfly.internal.parseRoute import zlc.season.butterfly.internal.parseRouteParams import zlc.season.butterfly.launcher.DestinationLauncher import zlc.season.butterfly.launcher.DestinationLauncherManager class DestinationHandler( private val context: Context, private var destinationData: DestinationData = DestinationData(), private val interceptorManager: InterceptorManager = InterceptorManager() ) { companion object { private val EMPTY_CALLBACK: (Result) -> Unit = {} } /** * Set params for destination */ fun params(vararg pair: Pair): DestinationHandler { return apply { destinationData.bundle.putAll(bundleOf(*pair)) } } fun params(bundle: Bundle): DestinationHandler { return apply { destinationData.bundle.putAll(bundle) } } /** * Skip global interceptor for current navigation */ fun skipGlobalInterceptor(): DestinationHandler { return configDestinationData { copy(enableGlobalInterceptor = false) } } /** * Add interceptor for current navigation */ fun addInterceptor(interceptor: Interceptor): DestinationHandler { return apply { interceptorManager.addInterceptor(interceptor) } } fun addInterceptor(interceptor: suspend (Context, DestinationData) -> DestinationData): DestinationHandler { return apply { interceptorManager.addInterceptor(DefaultInterceptor(interceptor)) } } fun container(containerViewId: Int): DestinationHandler { return configDestinationData { copy(containerViewId = containerViewId) } } fun container(containerViewTag: String): DestinationHandler { return configDestinationData { copy(containerViewTag = containerViewTag) } } fun tag(uniqueTag: String): DestinationHandler { return configDestinationData { copy(uniqueTag = uniqueTag) } } fun group(groupId: String): DestinationHandler { return configDestinationData { copy(groupId = groupId) } } fun clearTop(): DestinationHandler { return configDestinationData { copy(clearTop = true) } } fun singleTop(): DestinationHandler { return configDestinationData { copy(singleTop = true) } } fun asRoot(): DestinationHandler { return configDestinationData { copy(isRoot = true) } } fun disableBackStack(): DestinationHandler { return configDestinationData { copy(enableBackStack = false) } } fun addFlag(flag: Int): DestinationHandler { return configDestinationData { copy(flags = flags or flag) } } fun enterAnim(enterAnim: Int): DestinationHandler { return configDestinationData { copy(enterAnim = enterAnim) } } fun exitAnim(exitAnim: Int): DestinationHandler { return configDestinationData { copy(exitAnim = exitAnim) } } fun navigate( route: String, onResult: (Result) -> Unit = EMPTY_CALLBACK ) { navigate(route, bundleOf(), onResult) } fun navigate( route: String, vararg params: Pair, onResult: (Result) -> Unit = EMPTY_CALLBACK ) { navigate(route, bundleOf(*params), onResult) } fun navigate( route: String, params: Bundle, onResult: (Result) -> Unit = EMPTY_CALLBACK ) { if (context is LifecycleOwner) { context.lifecycleScope.launch { if (onResult != EMPTY_CALLBACK) { onResult(awaitNavigateResult(route, params)) } else { awaitNavigate(route, params) } } } else { "Navigate failed, context is not LifecycleOwner!".logd() } } suspend fun awaitNavigate(route: String, params: Bundle) { setupRouteAndParams(route, false, params) ButterflyCore.dispatchNavigate(context, destinationData, interceptorManager) } suspend fun awaitNavigateResult(route: String, params: Bundle): Result { setupRouteAndParams(route, true, params) return ButterflyCore.dispatchNavigate(context, destinationData, interceptorManager) } fun popBack(vararg result: Pair): DestinationData? { return popBack(bundleOf(*result)) } fun popBack(result: Bundle): DestinationData? { return ButterflyCore.popBack(context, result) } private suspend fun setupRouteAndParams( route: String, needResult: Boolean, extraParams: Bundle ) { withContext(Dispatchers.Default) { val parsedRoute = parseRoute(route) val destinationClassName = ButterflyCore.queryDestination(parsedRoute) val params = parseRouteParams(parsedRoute) configDestinationData { bundle.putAll(bundleOf(*params)) bundle.putAll(extraParams) copy( route = parsedRoute, className = destinationClassName, needResult = needResult ) } } } fun getLauncher(context: Context): DestinationLauncher { val agileHandler = configDestinationData { copy(needResult = true) } val activity = context.findActivity() ?: throw IllegalStateException("No Activity founded!") val key = activity.key() var launcher = DestinationLauncherManager.getLauncher(key, destinationData.route) if (launcher == null) { launcher = DestinationLauncher( context, agileHandler.destinationData, agileHandler.interceptorManager ) DestinationLauncherManager.addLauncher(key, launcher) } return launcher } private fun configDestinationData(block: DestinationData.() -> DestinationData): DestinationHandler { return apply { destinationData = destinationData.block() } } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/action/Action.kt ================================================ package zlc.season.butterfly.action import android.content.Context import android.os.Bundle interface Action { fun doAction(context: Context, route: String, data: Bundle = Bundle()) } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/core/BackStackEntryManager.kt ================================================ package zlc.season.butterfly.core import android.app.Activity import android.os.Bundle import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentActivity import zlc.season.butterfly.entities.BackStackEntry import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.internal.ButterflyHelper import zlc.season.butterfly.internal.ButterflyHelper.KEY_DESTINATION_DATA import zlc.season.butterfly.internal.ButterflyHelper.getDestinationData import zlc.season.butterfly.internal.key import zlc.season.butterfly.navigator.fragment.observeFragmentDestroy import zlc.season.claritypotion.ActivityLifecycleCallbacksAdapter class BackStackEntryManager { companion object { private const val KEY_SAVE_STATE = "butterfly_back_stack_state" } /** * Activity as the key to save the BackStackEntry corresponding to each activity. * * like: * { * {ActivityA} -> [BackStackEntryA1, BackStackEntryA2], * {ActivityB} -> [BackStackEntryB1, BackStackEntryB2] * } */ private val backStackEntryMap = mutableMapOf>() init { ButterflyHelper.application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacksAdapter() { override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { // If activity recreated, then restore current activity's entry list. if (savedInstanceState != null) { restoreEntryList(activity, savedInstanceState) } else { // If activity is started by Butterfly, then add it into back stack list. val data = activity.getDestinationData() if (data != null) { addEntry(activity, BackStackEntry(data)) } } // Observe fragment's destroy event to remove FragmentEntry. if (activity is FragmentActivity) { activity.observeFragmentDestroy { val uniqueTag = it.tag if (!uniqueTag.isNullOrEmpty()) { removeEntry(activity, uniqueTag) } } } } override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = saveEntryList(activity, outState) override fun onActivityDestroyed(activity: Activity) = destroyEntryList(activity) }) } @Suppress("deprecation") @Synchronized private fun restoreEntryList(activity: Activity, savedState: Bundle) { val data = savedState.getParcelableArrayList(KEY_SAVE_STATE) if (data != null) { val entryList = data.map { BackStackEntry(it) } getEntryList(activity).addAll(entryList) } } @Synchronized private fun saveEntryList(activity: Activity, outState: Bundle) { val list = backStackEntryMap[activity.key()] if (!list.isNullOrEmpty()) { val savedData = list.mapTo(ArrayList()) { it.destinationData } outState.putParcelableArrayList(KEY_SAVE_STATE, savedData) } } @Synchronized private fun destroyEntryList(activity: Activity) { backStackEntryMap.remove(activity.key()) } @Synchronized private fun removeEntry(activity: Activity, uniqueTag: String) { val find = getEntryList(activity).find { it.destinationData.uniqueTag == uniqueTag } if (find != null) { getEntryList(activity).remove(find) } } @Synchronized fun removeTopEntry(activity: Activity): BackStackEntry? { return getEntryList(activity).removeLastOrNull() } @Synchronized fun removeEntryList(activity: Activity, entryList: List) { getEntryList(activity).removeAll(entryList) } @Synchronized fun addEntry(activity: Activity, entry: BackStackEntry) { val list = getEntryList(activity) list.add(entry) // if (isDialogEntry(entry)) { // list.add(entry) // } else { // val dialogEntry = list.firstOrNull { isDialogEntry(it) } // if (dialogEntry != null) { // val index = list.indexOf(dialogEntry) // list.add(index, entry) // } else { // list.add(entry) // } // } } @Synchronized fun getTopEntry(activity: Activity): BackStackEntry? { val entryList = getEntryList(activity) return if (entryList.isEmpty()) { null } else { return entryList.lastOrNull() } } @Synchronized fun getTopEntryList(activity: Activity, data: DestinationData): MutableList { val result = mutableListOf() val backStackList = getEntryList(activity) val index = backStackList.indexOfLast { it.destinationData.className == data.className } if (index != -1) { for (i in index until backStackList.size) { val entry = backStackList[i] result.add(entry) } } return result } @Synchronized fun getEntryList(activity: Activity): MutableList { var backStackList = backStackEntryMap[activity.key()] if (backStackList == null) { backStackList = mutableListOf() backStackEntryMap[activity.key()] = backStackList } return backStackList } private fun isDialogEntry(entry: BackStackEntry): Boolean { val cls = Class.forName(entry.destinationData.className) return DialogFragment::class.java.isAssignableFrom(cls) } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/core/EvadeManager.kt ================================================ package zlc.season.butterfly.core import zlc.season.butterfly.entities.EvadeData import zlc.season.butterfly.internal.logw import java.lang.reflect.Method import java.lang.reflect.Proxy import java.util.Objects class EvadeManager { private val implObjMap = mutableMapOf() fun dispatch(request: EvadeData): Any { val evadeClass = Class.forName(request.className) if (!check(request)) { "Evade -> $request not found!".logw() return createProxyObj(evadeClass) } val implClass = Class.forName(request.implClassName) val implObj = implClass.getImplObj(request) // check extend if (evadeClass.isAssignableFrom(implClass)) { return implObj } return createProxyObj(evadeClass, implObj) { method, args -> if (args == null) { val findMethod = implClass.getDeclaredMethod(method.name) findMethod.invoke(implObj) } else { val findMethod = implClass.getDeclaredMethod(method.name, *method.parameterTypes) findMethod.invoke(implObj, *args) } } } private fun check(request: EvadeData): Boolean { if (request.implClassName.isEmpty()) { return false } return true } private fun Class<*>.getImplObj(request: EvadeData): Any { return if (request.isSingleton) { realGetImplObj(request) } else { getDeclaredConstructor().newInstance() } } @Synchronized private fun Class<*>.realGetImplObj(request: EvadeData): Any { var find = implObjMap[request.implClassName] if (find == null) { val implObj = getDeclaredConstructor().newInstance() implObjMap[request.implClassName] = implObj find = implObj } return find!! } private fun createProxyObj( cls: Class<*>, implObj: Any = Any(), block: (Method, Array?) -> Any? = { _, _ -> } ): Any { return Proxy.newProxyInstance( Thread.currentThread().contextClassLoader, arrayOf(cls) ) { _, method, args -> try { if (method.name == "hashCode") { Objects.hashCode(implObj) } else if (method.name == "toString") { Objects.toString(implObj) } else if (method.name == "equals") { val other = args[0] if (Objects.hashCode(implObj) == other.hashCode()) { true } else { Objects.equals(implObj, other) } } else { block(method, args) } } catch (e: Exception) { if (e is NoSuchMethodException) { "Evade -> Method ${e.message} not found!".logw() } else { e.logw() } Unit } } } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/core/GroupEntryManager.kt ================================================ package zlc.season.butterfly.core import android.app.Activity import android.os.Bundle import androidx.fragment.app.FragmentActivity import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.entities.GroupEntry import zlc.season.butterfly.internal.key import zlc.season.butterfly.internal.logd import zlc.season.butterfly.navigator.fragment.observeFragmentDestroy import zlc.season.claritypotion.ActivityLifecycleCallbacksAdapter import zlc.season.claritypotion.ClarityPotion.application @Suppress("DEPRECATION") class GroupEntryManager { private val groupEntryMap = mutableMapOf>() companion object { private const val KEY_SAVE_STATE = "butterfly_group_state" } init { application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacksAdapter() { override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { // If activity recreated, then restore current activity's entry list. if (savedInstanceState != null) { restoreEntryList(activity, savedInstanceState) } // Observe fragment's destroy event to remove FragmentEntry. if (activity is FragmentActivity) { activity.observeFragmentDestroy { val uniqueTag = it.tag if (!uniqueTag.isNullOrEmpty()) { removeEntry(activity, uniqueTag) } } } } override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { saveEntryList(activity, outState) } override fun onActivityDestroyed(activity: Activity) { destroyEntryList(activity) } }) } @Synchronized private fun restoreEntryList(activity: Activity, savedState: Bundle) { val data = savedState.getParcelableArrayList(KEY_SAVE_STATE) if (data != null) { val entryList = data.map { GroupEntry(it) } getEntryList(activity).addAll(entryList) } } @Synchronized private fun saveEntryList(activity: Activity, outState: Bundle) { val entryList = groupEntryMap[activity.key()] if (!entryList.isNullOrEmpty()) { val savedData = entryList.mapTo(ArrayList()) { it.destinationData } outState.putParcelableArrayList(KEY_SAVE_STATE, savedData) } } @Synchronized private fun destroyEntryList(activity: Activity) { groupEntryMap.remove(activity.key()) } @Synchronized private fun removeEntry(activity: Activity, uniqueTag: String) { val entryList = getEntryList(activity) val find = entryList.find { it.destinationData.uniqueTag == uniqueTag } if (find != null) { entryList.remove(find) } } @Synchronized fun addEntry(activity: Activity, groupEntry: GroupEntry) { val entryList = getEntryList(activity) entryList.add(groupEntry) } @Synchronized fun getGroupList(activity: Activity, groupId: String): List { val result = mutableListOf() val list = getEntryList(activity) list.forEach { if (it.destinationData.groupId == groupId) { result.add(it) } } return result } @Synchronized private fun getEntryList(activity: Activity): MutableList { var groupList = groupEntryMap[activity.key()] if (groupList == null) { groupList = mutableListOf() groupEntryMap[activity.key()] = groupList } return groupList } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/core/InterceptorManager.kt ================================================ package zlc.season.butterfly.core import android.content.Context import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.interceptor.Interceptor class InterceptorManager { private val interceptorList = mutableListOf() fun addInterceptor(interceptor: Interceptor) { interceptorList.add(interceptor) } fun removeInterceptor(interceptor: Interceptor) { interceptorList.remove(interceptor) } suspend fun intercept(context: Context, destinationData: DestinationData): DestinationData { val temp = mutableListOf() temp.addAll(interceptorList) var tempData = destinationData temp.forEach { if (it.shouldIntercept(context, tempData)) { tempData = it.intercept(context, tempData) } } return tempData } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/core/ModuleManager.kt ================================================ package zlc.season.butterfly.core import zlc.season.butterfly.entities.EvadeData import zlc.season.butterfly.module.Module class ModuleManager { private val modules = mutableListOf() fun addModule(vararg module: Module) { module.forEach { modules.add(it) } } fun removeModule(module: Module) { modules.remove(module) } fun queryDestination(route: String): String { var result = "" modules.forEach { val find = it.getDestination()[route] if (find != null) { result = find.name return@forEach } } return result } fun queryEvade(identity: String): EvadeData { var className = "" var implClassName = "" var isSingleton = true modules.forEach { if (className.isEmpty()) { val evadeMap = it.getEvade() val temp = evadeMap[identity] if (temp != null) { className = temp.name } } if (implClassName.isEmpty()) { val implMap = it.getEvadeImpl() val temp = implMap[identity] if (temp != null) { implClassName = temp.cls.name isSingleton = temp.singleton } } if (className.isNotEmpty() && implClassName.isNotEmpty()) { return@forEach } } return EvadeData(identity, className, implClassName, isSingleton) } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/core/NavigatorManager.kt ================================================ package zlc.season.butterfly.core import android.app.Activity import android.content.Context import android.os.Bundle import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import zlc.season.butterfly.action.Action import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.internal.logd import zlc.season.butterfly.internal.logw import zlc.season.butterfly.navigator.ActionNavigator import zlc.season.butterfly.navigator.ActivityNavigator import zlc.season.butterfly.navigator.fragment.DialogFragmentNavigator import zlc.season.butterfly.navigator.ErrorNavigator import zlc.season.butterfly.navigator.fragment.FragmentNavigator import zlc.season.butterfly.navigator.Navigator class NavigatorManager { companion object { private const val COMPOSE_DESTINATION_CLASS = "zlc.season.butterfly.compose.ComposeDestination" private const val COMPOSE_NAVIGATOR_CLASS = "zlc.season.butterfly.compose.ComposeNavigator" } private val navigatorMaps = LinkedHashMap, Navigator>() private val backStackEntryManager = BackStackEntryManager() private val groupEntryManager = GroupEntryManager() fun getBackStackEntryManager() = backStackEntryManager fun getGroupEntryManager() = groupEntryManager init { navigatorMaps.apply { putAll( listOf( Action::class.java to ActionNavigator, FragmentActivity::class.java to ActivityNavigator(backStackEntryManager), DialogFragment::class.java to DialogFragmentNavigator(backStackEntryManager), Fragment::class.java to FragmentNavigator( backStackEntryManager, groupEntryManager ) ) ) try { val composeDestinationCls = Class.forName(COMPOSE_DESTINATION_CLASS) val composeNavigatorCls = Class.forName(COMPOSE_NAVIGATOR_CLASS) val composeNavigator = composeNavigatorCls.getConstructor( BackStackEntryManager::class.java, GroupEntryManager::class.java ).newInstance(backStackEntryManager, groupEntryManager) as Navigator put(composeDestinationCls, composeNavigator) } catch (e: Exception) { e.logw() } put(Any::class.java, ErrorNavigator) } } suspend fun navigate(context: Context, data: DestinationData): Result { if (data.className.isEmpty()) { "Navigate failed! Could not find destination class: destination=$data".logd() return Result.failure(IllegalStateException("Destination class not found!")) } return findNavigator(data).navigate(context, data) } fun popBack(context: Context, result: Bundle): DestinationData? { val currentActivity = if (context is Activity) { context } else { "Pop back failed! Need an Activity context: currentContext=$context".logd() return null } val topEntry = backStackEntryManager.removeTopEntry(currentActivity) if (topEntry == null) { "Pop back failed! Current activity's backstack can not find any entry!".logd() return null } findNavigator(topEntry.destinationData).popBack(currentActivity, topEntry, result) return topEntry.destinationData } private fun findNavigator(data: DestinationData): Navigator { val cls = Class.forName(data.className) return navigatorMaps[navigatorMaps.keys.find { it.isAssignableFrom(cls) }]!! } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/entities/BackStackEntry.kt ================================================ package zlc.season.butterfly.entities data class BackStackEntry(val destinationData: DestinationData) ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/entities/DestinationData.kt ================================================ package zlc.season.butterfly.entities import android.os.Bundle import android.os.Parcelable import kotlinx.parcelize.Parcelize import zlc.season.butterfly.internal.createDestinationDataTag @Parcelize data class DestinationData( val route: String = "", val className: String = "", val bundle: Bundle = Bundle(), val enterAnim: Int = 0, val exitAnim: Int = 0, val flags: Int = 0, val containerViewId: Int = 0, val containerViewTag: String = "", val needResult: Boolean = false, val enableBackStack: Boolean = true, val enableGlobalInterceptor: Boolean = true, val isRoot: Boolean = false, val clearTop: Boolean = false, val singleTop: Boolean = false, val useReplace: Boolean = false, val groupId: String = "", val uniqueTag: String = createDestinationDataTag() ) : Parcelable ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/entities/EvadeData.kt ================================================ package zlc.season.butterfly.entities data class EvadeData( val identity: String, val className: String, val implClassName: String, val isSingleton: Boolean ) { override fun toString(): String { return """[identity="$identity", className="$className", implClassName="$implClassName", isSingleton="$isSingleton"]""" } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/entities/GroupEntry.kt ================================================ package zlc.season.butterfly.entities data class GroupEntry(val destinationData: DestinationData) ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/interceptor/DefaultInterceptor.kt ================================================ package zlc.season.butterfly.interceptor import android.content.Context import zlc.season.butterfly.entities.DestinationData class DefaultInterceptor( private val interceptor: suspend (Context, DestinationData) -> DestinationData ) : Interceptor { override suspend fun shouldIntercept( context: Context, destinationData: DestinationData ): Boolean { return true } override suspend fun intercept( context: Context, destinationData: DestinationData ): DestinationData { return interceptor(context, destinationData) } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/interceptor/Interceptor.kt ================================================ package zlc.season.butterfly.interceptor import android.content.Context import zlc.season.butterfly.entities.DestinationData interface Interceptor { suspend fun shouldIntercept(context: Context, destinationData: DestinationData): Boolean suspend fun intercept(context: Context, destinationData: DestinationData): DestinationData } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/internal/ButterflyFragment.kt ================================================ package zlc.season.butterfly.internal import android.app.Activity import android.content.Intent import android.os.Bundle import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import kotlinx.coroutines.suspendCancellableCoroutine import zlc.season.butterfly.launcher.DestinationLauncherManager import zlc.season.butterfly.navigator.fragment.addFragment import zlc.season.butterfly.navigator.fragment.awaitFragmentResume import zlc.season.butterfly.navigator.fragment.removeFragment class ButterflyFragment : Fragment() { companion object { private const val KEY_ROUTE = "key_destination_route" private const val KEY_TAG = "key_destination_tag" suspend fun FragmentActivity.startActivityAndAwaitResult( route: String, intent: Intent ): Result { "Await activity result: currentActivity=$this, route=$route".logd() val fragment = ButterflyFragment().apply { arguments = Bundle().apply { putString(KEY_ROUTE, route) } } "Await activity result: add fragment $fragment...".logd() addFragment(fragment) "Await activity result: waiting fragment resume...".logd() awaitFragmentResume(fragment) "Await activity result: waiting activity result...".logd() val result = fragment.startActivityAndAwaitResult(intent) "Await activity result: result=$result.".logd() return Result.success(result) } suspend fun FragmentActivity.awaitFragmentResult( route: String, uniqueTag: String ): Result { "Await fragment result: currentActivity=$this, route=$route, uniqueTag=$uniqueTag".logd() val fragment = ButterflyFragment().apply { arguments = Bundle().apply { putString(KEY_ROUTE, route) putString(KEY_TAG, uniqueTag) } } "Await fragment result: add fragment $fragment...".logd() addFragment(fragment) "Await fragment result: waiting fragment resume...".logd() awaitFragmentResume(fragment) "Await fragment result: waiting fragment result...".logd() val result = fragment.waitFragmentResult() "Await fragment result: result=$result.".logd() return Result.success(result) } } private val route by lazy { arguments?.getString(KEY_ROUTE) ?: "" } private val uniqueTag by lazy { arguments?.getString(KEY_TAG) ?: "" } private val viewModel by lazy { ViewModelProvider(this)[ButterflyViewModel::class.java] } private val launcher = registerForActivityResult(StartActivityForResult()) { val result = if (it.resultCode == Activity.RESULT_OK) { it.data?.extras ?: Bundle() } else { Bundle() } "ButterflyFragment[$this] onActivityResult: result=$result".logd() //set result for callback viewModel.callback.invoke(result) //set result for launcher val launcher = DestinationLauncherManager.getLauncher(requireActivity().key(), route) launcher?.flow?.tryEmit(Result.success(result)) //clear current fragment activity?.removeFragment(this) } /** * Launch activity and wait result. */ suspend fun startActivityAndAwaitResult(intent: Intent): Bundle = suspendCancellableCoroutine { viewModel.callback = { result -> it.resumeWith(Result.success(result)) viewModel.callback = {} } "ButterflyFragment[$this] launch activity and wait result".logd() launcher.launch(intent) it.invokeOnCancellation { viewModel.callback = {} } } suspend fun waitFragmentResult(): Bundle = suspendCancellableCoroutine { viewModel.callback = { result -> it.resumeWith(Result.success(result)) viewModel.callback = {} } "ButterflyFragment[$this] wait fragment result".logd() it.invokeOnCancellation { viewModel.callback = {} } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) "ButterflyFragment[$this] onCreate".logd() if (uniqueTag.isNotEmpty()) { parentFragmentManager.setFragmentResultListener(uniqueTag, this) { requestKey, result -> if (uniqueTag == requestKey) { "ButterflyFragment[$this] onFragmentResult: result=$result".logd() //set result for callback viewModel.callback.invoke(result) //set result for launcher val launcher = DestinationLauncherManager.getLauncher(requireActivity().key(), route) launcher?.flow?.tryEmit(Result.success(result)) parentFragmentManager.clearFragmentResultListener(requestKey) //clear current fragment activity?.removeFragment(this) } } } } override fun onResume() { super.onResume() "ButterflyFragment[$this] onResume".logd() } override fun onDestroy() { super.onDestroy() "ButterflyFragment[$this] onDestroy".logd() viewModel.callback = {} if (uniqueTag.isNotEmpty()) { parentFragmentManager.clearFragmentResultListener(uniqueTag) } } class ButterflyViewModel : ViewModel() { var callback: ((Bundle) -> Unit) = {} } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/internal/ButterflyHelper.kt ================================================ package zlc.season.butterfly.internal import android.app.Activity import android.app.Activity.RESULT_OK import android.app.Application import android.content.Context import android.content.ContextWrapper import android.content.Intent import android.os.Bundle import android.view.ViewGroup import zlc.season.butterfly.entities.DestinationData import zlc.season.claritypotion.ClarityPotion object ButterflyHelper { internal const val KEY_DESTINATION_DATA = "key_butterfly_destination_data" val context: Context get() = activity ?: application val application: Application get() = ClarityPotion.application val activity: Activity? get() = ClarityPotion.activity fun Activity.setActivityResult(bundle: Bundle) { if (bundle.isEmpty) return setResult(RESULT_OK, Intent().apply { putExtras(bundle) }) } fun Activity.contentView(): ViewGroup { return findViewById(android.R.id.content) } fun Context.findActivity(): Activity? { var context = this while (context is ContextWrapper) { if (context is Activity) return context context = context.baseContext } return null } @Suppress("deprecation") fun Activity.getDestinationData(): DestinationData? { return intent.getParcelableExtra(KEY_DESTINATION_DATA) } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/internal/LogUtil.kt ================================================ package zlc.season.butterfly.internal import android.util.Log var enableLog = true val TAG = "Butterfly" fun T.logd(tag: String = ""): T { if (enableLog) { val realTag = tag.ifEmpty { TAG } if (this is Throwable) { Log.d(realTag, this.message ?: "", this) } else { Log.d(realTag, this.toString()) } } return this } fun T.logw(tag: String = ""): T { if (enableLog) { val realTag = tag.ifEmpty { TAG } if (this is Throwable) { Log.w(realTag, this.message ?: "", this) } else { Log.w(realTag, this.toString()) } } return this } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/internal/Util.kt ================================================ package zlc.season.butterfly.internal import android.app.Activity import androidx.core.net.toUri import java.util.* internal fun parseRoute(route: String): String { val index = route.indexOfFirst { it == '?' } return if (index > 0) { route.substring(0, index) } else { route } } internal fun parseRouteParams(route: String): Array> { val uri = route.toUri() val query = uri.query if (query != null) { val result = mutableListOf>() uri.queryParameterNames.forEach { val value = uri.getQueryParameter(it) result.add(it to value) } return result.toTypedArray() } return arrayOf() } internal fun createDestinationDataTag(): String { return UUID.randomUUID().toString().replace("-", "").uppercase(Locale.getDefault()) } internal fun Activity.key(): String { return "${javaClass.canonicalName}@${hashCode()}" } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/launcher/DestinationLauncher.kt ================================================ package zlc.season.butterfly.launcher import android.content.Context import android.os.Bundle import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import zlc.season.butterfly.ButterflyCore import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.core.InterceptorManager class DestinationLauncher( val context: Context, val destinationData: DestinationData, val interceptorManager: InterceptorManager ) { val flow = MutableSharedFlow>(extraBufferCapacity = 1) fun result(): Flow> { return flow } suspend fun launch() { ButterflyCore.dispatchNavigate(context, destinationData, interceptorManager) } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/launcher/DestinationLauncherManager.kt ================================================ package zlc.season.butterfly.launcher import android.app.Activity import android.os.Bundle import zlc.season.butterfly.internal.ButterflyHelper.application import zlc.season.butterfly.internal.key import zlc.season.claritypotion.ActivityLifecycleCallbacksAdapter object DestinationLauncherManager { private const val OLD_ACTIVITY_KEY = "old_activity_key" private val launcherMap = mutableMapOf>() private val saveInstanceStateMap = mutableMapOf() init { application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacksAdapter() { override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { if (savedInstanceState != null) { val oldKey = savedInstanceState.getString(OLD_ACTIVITY_KEY) if (!oldKey.isNullOrEmpty()) { updateKey(oldKey, activity.key()) } } } override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { super.onActivitySaveInstanceState(activity, outState) val key = activity.key() if (containsKey(key)) { outState.putString(OLD_ACTIVITY_KEY, key) } updateSaveInstanceState(key) } override fun onActivityDestroyed(activity: Activity) { super.onActivityDestroyed(activity) handleActivityDestroy(activity.key()) } }) } @Synchronized private fun updateSaveInstanceState(key: String) { saveInstanceStateMap[key] = true } @Synchronized private fun handleActivityDestroy(key: String) { if (saveInstanceStateMap[key] == null) { launcherMap.remove(key) } saveInstanceStateMap.remove(key) } @Synchronized private fun updateKey(oldKey: String, newKey: String) { val oldLauncher = launcherMap.remove(oldKey) oldLauncher?.let { launcherMap[newKey] = oldLauncher } } @Synchronized fun containsKey(key: String): Boolean { return launcherMap[key] != null } @Synchronized fun addLauncher(key: String, launcher: DestinationLauncher) { val list = launcherMap.getOrPut(key) { mutableListOf() } if (list.find { it.destinationData.route == launcher.destinationData.route } == null) { list.add(launcher) } } @Synchronized fun getLauncher(key: String, route: String): DestinationLauncher? { return launcherMap[key]?.find { it.destinationData.route == route } } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/navigator/ActionNavigator.kt ================================================ package zlc.season.butterfly.navigator import android.content.Context import android.os.Bundle import zlc.season.butterfly.action.Action import zlc.season.butterfly.entities.DestinationData object ActionNavigator : Navigator { override suspend fun navigate(context: Context, data: DestinationData): Result { return handleAction(context, data) } private fun handleAction(context: Context, request: DestinationData): Result { val action = createAction(request) action.doAction(context, request.route, request.bundle) return Result.success(Bundle.EMPTY) } private fun createAction(request: DestinationData): Action { val cls = Class.forName(request.className) return cls.getDeclaredConstructor().newInstance() as Action } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/navigator/ActivityNavigator.kt ================================================ package zlc.season.butterfly.navigator import android.app.Activity import android.app.Application import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP import android.os.Bundle import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult.Companion.EXTRA_ACTIVITY_OPTIONS_BUNDLE import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat.makeCustomAnimation import androidx.fragment.app.FragmentActivity import kotlinx.coroutines.suspendCancellableCoroutine import zlc.season.butterfly.entities.BackStackEntry import zlc.season.butterfly.core.BackStackEntryManager import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.internal.ButterflyFragment.Companion.startActivityAndAwaitResult import zlc.season.butterfly.internal.ButterflyHelper.KEY_DESTINATION_DATA import zlc.season.butterfly.internal.ButterflyHelper.getDestinationData import zlc.season.butterfly.internal.ButterflyHelper.setActivityResult import zlc.season.claritypotion.ActivityLifecycleCallbacksAdapter class ActivityNavigator(val backStackEntryManager: BackStackEntryManager) : Navigator { override suspend fun navigate(context: Context, data: DestinationData): Result { if (context is FragmentActivity) { return navigate(context, data) } val intent = createIntent(context, data) context.startActivity(intent, createActivityOptions(context, data)?.toBundle()) context.awaitActivityCreated(data) return Result.success(Bundle.EMPTY) } private suspend fun navigate( activity: FragmentActivity, data: DestinationData ): Result { return if (!data.needResult) { val intent = createIntent(activity, data) activity.startActivity(intent, createActivityOptions(activity, data)?.toBundle()) activity.awaitActivityCreated(data) Result.success(Bundle.EMPTY) } else { val intent = createIntent(activity, data) createActivityOptions(activity, data)?.toBundle()?.let { intent.putExtra(EXTRA_ACTIVITY_OPTIONS_BUNDLE, it) } activity.startActivityAndAwaitResult(data.route, intent) } } override fun popBack(activity: Activity, topEntry: BackStackEntry, bundle: Bundle) { with(activity) { if (topEntry.destinationData.needResult) { setActivityResult(bundle) } finish() } } private fun createIntent(context: Context, data: DestinationData): Intent { val intent = Intent().apply { putExtra(KEY_DESTINATION_DATA, data) setClassName(context.packageName, data.className) putExtras(data.bundle) if (data.clearTop) { addFlags(FLAG_ACTIVITY_CLEAR_TOP) } else if (data.singleTop) { addFlags(FLAG_ACTIVITY_SINGLE_TOP) } if (data.flags != 0) { addFlags(data.flags) } if (context !is Activity) { addFlags(FLAG_ACTIVITY_NEW_TASK) } } return intent } private fun createActivityOptions( context: Context, data: DestinationData ): ActivityOptionsCompat? { return if (data.enterAnim != 0 || data.exitAnim != 0) { makeCustomAnimation(context, data.enterAnim, data.exitAnim) } else { null } } private suspend fun Context.awaitActivityCreated(data: DestinationData) = suspendCancellableCoroutine { val application = this.applicationContext as Application val callback = object : ActivityLifecycleCallbacksAdapter() { override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { val destinationData = activity.getDestinationData() if (destinationData?.uniqueTag == data.uniqueTag) { application.unregisterActivityLifecycleCallbacks(this) if (it.isActive) { it.resumeWith(Result.success(Unit)) } } } } application.registerActivityLifecycleCallbacks(callback) it.invokeOnCancellation { application.unregisterActivityLifecycleCallbacks(callback) } } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/navigator/ErrorNavigator.kt ================================================ package zlc.season.butterfly.navigator import android.content.Context import android.os.Bundle import zlc.season.butterfly.entities.DestinationData object ErrorNavigator : Navigator { override suspend fun navigate( context: Context, data: DestinationData ): Result { return Result.failure(IllegalStateException("Invalid destination data: $data")) } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/navigator/Navigator.kt ================================================ package zlc.season.butterfly.navigator import android.app.Activity import android.content.Context import android.os.Bundle import zlc.season.butterfly.entities.BackStackEntry import zlc.season.butterfly.entities.DestinationData interface Navigator { suspend fun navigate(context: Context, data: DestinationData): Result fun popBack(activity: Activity, topEntry: BackStackEntry, bundle: Bundle) {} } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/navigator/fragment/DialogFragmentNavigator.kt ================================================ package zlc.season.butterfly.navigator.fragment import android.app.Activity import android.content.Context import android.os.Bundle import androidx.fragment.app.FragmentActivity import zlc.season.butterfly.core.BackStackEntryManager import zlc.season.butterfly.entities.BackStackEntry import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.internal.ButterflyFragment.Companion.awaitFragmentResult import zlc.season.butterfly.navigator.Navigator class DialogFragmentNavigator(val backStackEntryManager: BackStackEntryManager) : Navigator { override suspend fun navigate(context: Context, data: DestinationData): Result { return if (context is FragmentActivity) { navigate(context, data) } else { Result.success(Bundle.EMPTY) } } private suspend fun navigate( activity: FragmentActivity, destinationData: DestinationData ): Result { if (destinationData.enableBackStack) { backStackEntryManager.addEntry(activity, BackStackEntry(destinationData)) } activity.showDialogFragment(destinationData) return if (destinationData.needResult) { activity.awaitFragmentResult(destinationData.route, destinationData.uniqueTag) } else { Result.success(Bundle.EMPTY) } } override fun popBack(activity: Activity, topEntry: BackStackEntry, bundle: Bundle) { if (activity !is FragmentActivity) return with(activity) { val find = findDialogFragment(topEntry.destinationData) ?: return if (topEntry.destinationData.needResult) { setFragmentResult(topEntry.destinationData.uniqueTag, bundle) } find.dismissAllowingStateLoss() } } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/navigator/fragment/FragmentHelper.kt ================================================ package zlc.season.butterfly.navigator.fragment import android.os.Bundle import android.view.View import android.view.ViewGroup import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentResultListener import androidx.fragment.app.FragmentTransaction import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.suspendCancellableCoroutine import zlc.season.butterfly.entities.DestinationData private fun FragmentActivity.createFragment(data: DestinationData): Fragment { val fragment = supportFragmentManager.fragmentFactory.instantiate( classLoader, data.className ) fragment.arguments = data.bundle return fragment } internal fun FragmentActivity.createAndShowFragment(data: DestinationData): Fragment { val fragment = createFragment(data) with(supportFragmentManager.beginTransaction()) { setCustomAnimations(data.enterAnim, data.exitAnim, 0, 0) if (data.useReplace) { replace(findContainerViewId(data), fragment, data.uniqueTag) } else { add(findContainerViewId(data), fragment, data.uniqueTag) } commitAllowingStateLoss() } return fragment } /** * Finds the target container view id for a destination within a FragmentActivity. * * @param data Information about the destination, including potential container view id or tag. * @return The id of the container view to use, or [android.R.id.content] if no specific container is found. */ private fun FragmentActivity.findContainerViewId(data: DestinationData): Int { var result = 0 if (data.containerViewId != 0) { result = data.containerViewId } else if (data.containerViewTag.isNotEmpty()) { val containerView = window.decorView.findViewWithTag(data.containerViewTag) if (containerView != null && containerView.id != View.NO_ID) { result = containerView.id } } return if (result != 0) result else android.R.id.content } internal fun FragmentActivity.createDialogFragment(request: DestinationData): DialogFragment { return createFragment(request) as DialogFragment } internal fun FragmentActivity.showDialogFragment(request: DestinationData): DialogFragment { val dialogFragment = createDialogFragment(request) dialogFragment.show(supportFragmentManager, request.uniqueTag) return dialogFragment } internal fun FragmentActivity.findFragment(data: DestinationData): Fragment? { return supportFragmentManager.findFragmentByTag(data.uniqueTag) } internal fun FragmentActivity.findDialogFragment(request: DestinationData): DialogFragment? { return supportFragmentManager.findFragmentByTag(request.uniqueTag) as? DialogFragment } internal fun FragmentActivity.addFragment(fragment: Fragment) { commit { add(fragment, fragment.javaClass.name) } } internal fun FragmentActivity.removeFragment(fragment: Fragment) { commit { remove(fragment) } } internal fun FragmentActivity.removeFragment(tag: String) { val find = supportFragmentManager.findFragmentByTag(tag) if (find != null) { commit { remove(find) } } } internal fun FragmentActivity.hideFragment(fragment: Fragment) { commit { hide(fragment) } } internal fun FragmentActivity.showFragment(fragment: Fragment) { commit { show(fragment) } } private fun FragmentActivity.commit(block: FragmentTransaction.() -> Unit) { val transaction = supportFragmentManager.beginTransaction() transaction.block() transaction.commitAllowingStateLoss() } internal fun FragmentActivity.observeFragmentDestroy(block: (Fragment) -> Unit) { val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() { override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) { block(f) } } supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, false) lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onDestroy(owner: LifecycleOwner) { supportFragmentManager.unregisterFragmentLifecycleCallbacks(fragmentLifecycleCallbacks) lifecycle.removeObserver(this) } }) } suspend fun FragmentActivity.awaitFragmentResume(fragment: Fragment) = suspendCancellableCoroutine { if (fragment.isResumed) { it.resumeWith(Result.success(Unit)) } else { val callback = object : FragmentManager.FragmentLifecycleCallbacks() { override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { if (fragment === f) { supportFragmentManager.unregisterFragmentLifecycleCallbacks(this) if (it.isActive) { it.resumeWith(Result.success(Unit)) } } } } val lifecycleObserver = object : DefaultLifecycleObserver { override fun onDestroy(owner: LifecycleOwner) { supportFragmentManager.unregisterFragmentLifecycleCallbacks(callback) lifecycle.removeObserver(this) } } lifecycle.addObserver(lifecycleObserver) supportFragmentManager.registerFragmentLifecycleCallbacks(callback, false) it.invokeOnCancellation { supportFragmentManager.unregisterFragmentLifecycleCallbacks(callback) lifecycle.removeObserver(lifecycleObserver) } } } internal fun FragmentActivity.awaitFragmentResult(fragment: Fragment, requestKey: String) = callbackFlow { val listener = FragmentResultListener { key, result -> if (requestKey == key) { trySend(Result.success(result)) close() supportFragmentManager.clearFragmentResultListener(requestKey) } } if (!isDestroyed && isActive) { supportFragmentManager.setFragmentResultListener( requestKey, fragment, listener ) } awaitClose { supportFragmentManager.clearFragmentResultListener(requestKey) } } internal fun FragmentActivity.setFragmentResult(requestKey: String, bundle: Bundle) { if (bundle.isEmpty) return supportFragmentManager.setFragmentResult(requestKey, bundle) } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/navigator/fragment/FragmentNavigator.kt ================================================ package zlc.season.butterfly.navigator.fragment import android.app.Activity import android.content.Context import android.os.Bundle import androidx.fragment.app.FragmentActivity import zlc.season.butterfly.core.BackStackEntryManager import zlc.season.butterfly.core.GroupEntryManager import zlc.season.butterfly.entities.BackStackEntry import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.internal.ButterflyFragment.Companion.awaitFragmentResult import zlc.season.butterfly.internal.ButterflyHelper.setActivityResult import zlc.season.butterfly.navigator.Navigator class FragmentNavigator( private val backStackEntryManager: BackStackEntryManager, private val groupEntryManager: GroupEntryManager ) : Navigator { private val navigatorContext = NavigatorContext(backStackEntryManager, groupEntryManager) override suspend fun navigate(context: Context, data: DestinationData): Result { return if (context is FragmentActivity) { navigate(context, data) } else { Result.success(Bundle.EMPTY) } } private suspend fun navigate( activity: FragmentActivity, request: DestinationData ): Result { navigatorContext.navigate(activity, request) return if (request.needResult) { activity.awaitFragmentResult(request.route, request.uniqueTag) } else { Result.success(Bundle.EMPTY) } } override fun popBack(activity: Activity, topEntry: BackStackEntry, bundle: Bundle) { if (activity !is FragmentActivity) return activity.apply { findFragment(topEntry.destinationData)?.let { if (topEntry.destinationData.needResult) { setFragmentResult(topEntry.destinationData.uniqueTag, bundle) } removeFragment(it) } } } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/navigator/fragment/FragmentParamUpdatable.kt ================================================ package zlc.season.butterfly.navigator.fragment import android.os.Bundle interface FragmentParamUpdatable { fun onParamsUpdate(newParams: Bundle) } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/navigator/fragment/NavigatorContext.kt ================================================ package zlc.season.butterfly.navigator.fragment import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import zlc.season.butterfly.core.BackStackEntryManager import zlc.season.butterfly.core.GroupEntryManager import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.navigator.fragment.backstack.ClearTopBackstackNavigator import zlc.season.butterfly.navigator.fragment.backstack.SingleTopBackstackNavigator import zlc.season.butterfly.navigator.fragment.backstack.StandardBackstackNavigator import zlc.season.butterfly.navigator.fragment.group.GroupNavigator class NavigatorContext( backStackEntryManager: BackStackEntryManager, groupEntryManager: GroupEntryManager ) { private val standardBackstackNavigator = StandardBackstackNavigator(backStackEntryManager) private val clearTopBackstackNavigator = ClearTopBackstackNavigator(backStackEntryManager) private val singleTopBackstackNavigator = SingleTopBackstackNavigator(backStackEntryManager) private val groupNavigator = GroupNavigator(groupEntryManager) suspend fun navigate( activity: FragmentActivity, request: DestinationData ): Fragment { return if (request.groupId.isNotEmpty()) { groupNavigate(activity, request) } else { backstackNavigate(activity, request) } } private suspend fun backstackNavigate( activity: FragmentActivity, request: DestinationData ): Fragment { return if (request.clearTop) { clearTopBackstackNavigator.navigate(activity, request) } else if (request.singleTop) { singleTopBackstackNavigator.navigate(activity, request) } else { standardBackstackNavigator.navigate(activity, request) } } private fun groupNavigate( activity: FragmentActivity, request: DestinationData ): Fragment { return groupNavigator.navigate(activity, request) } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/navigator/fragment/backstack/BackstackNavigator.kt ================================================ package zlc.season.butterfly.navigator.fragment.backstack import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import zlc.season.butterfly.core.BackStackEntryManager import zlc.season.butterfly.entities.DestinationData interface BackstackNavigator { suspend fun navigate( activity: FragmentActivity, destinationData: DestinationData ): Fragment } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/navigator/fragment/backstack/BackstackNavigatorHelper.kt ================================================ package zlc.season.butterfly.navigator.fragment.backstack import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.navigator.fragment.findFragment import zlc.season.butterfly.navigator.fragment.showFragment import zlc.season.butterfly.navigator.fragment.FragmentParamUpdatable import zlc.season.butterfly.navigator.fragment.awaitFragmentResume import zlc.season.butterfly.navigator.fragment.createAndShowFragment class BackstackNavigatorHelper { suspend fun createAndShowFragment( activity: FragmentActivity, destinationData: DestinationData ): Fragment { val fragment = activity.createAndShowFragment(destinationData) activity.awaitFragmentResume(fragment) return fragment } suspend fun showFragmentAndUpdateArguments( activity: FragmentActivity, oldData: DestinationData, newData: DestinationData ): Fragment { val target = activity.findFragment(oldData) return if (target == null) { createAndShowFragment(activity, newData) } else { if (target is FragmentParamUpdatable) { target.onParamsUpdate(newData.bundle) } activity.showFragment(target) activity.awaitFragmentResume(target) target } } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/navigator/fragment/backstack/ClearTopBackstackNavigator.kt ================================================ package zlc.season.butterfly.navigator.fragment.backstack import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import zlc.season.butterfly.core.BackStackEntryManager import zlc.season.butterfly.entities.BackStackEntry import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.navigator.fragment.removeFragment class ClearTopBackstackNavigator( val backStackEntryManager: BackStackEntryManager ) : BackstackNavigator { private val backstackNavigatorHelper = BackstackNavigatorHelper() override suspend fun navigate( activity: FragmentActivity, destinationData: DestinationData ): Fragment { // Get the entry list above on the target Destination, including the target Destination. val topEntryList = backStackEntryManager.getTopEntryList(activity, destinationData) // If the list is empty, create and show a new Fragment. return if (topEntryList.isEmpty()) { val fragment = backstackNavigatorHelper.createAndShowFragment( activity, destinationData ) // Add fragment to back stack. if (destinationData.enableBackStack) { backStackEntryManager.addEntry(activity, BackStackEntry(destinationData)) } fragment } else { // Remove all the Fragment above the target Destination. val targetEntry = topEntryList.removeFirst() topEntryList.forEach { activity.removeFragment(it.destinationData.uniqueTag) } backStackEntryManager.removeEntryList(activity, topEntryList) // Show the target Fragment and update its arguments. backstackNavigatorHelper.showFragmentAndUpdateArguments( activity, targetEntry.destinationData, destinationData ) } } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/navigator/fragment/backstack/SingleTopBackstackNavigator.kt ================================================ package zlc.season.butterfly.navigator.fragment.backstack import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import zlc.season.butterfly.core.BackStackEntryManager import zlc.season.butterfly.entities.BackStackEntry import zlc.season.butterfly.entities.DestinationData class SingleTopBackstackNavigator( val backStackEntryManager: BackStackEntryManager ) : BackstackNavigator { private val backstackNavigatorHelper = BackstackNavigatorHelper() override suspend fun navigate( activity: FragmentActivity, destinationData: DestinationData ): Fragment { val topEntry = backStackEntryManager.getTopEntry(activity) // If topEntry is not null and the target Destination is the same as the topEntry, // show the topEntry's fragment and update its arguments. return if ( topEntry != null && topEntry.destinationData.className == destinationData.className ) { backstackNavigatorHelper.showFragmentAndUpdateArguments( activity, topEntry.destinationData, destinationData ) } else { // Create and show a new Fragment. val fragment = backstackNavigatorHelper.createAndShowFragment( activity, destinationData ) // Add fragment to back stack. if (destinationData.enableBackStack) { backStackEntryManager.addEntry(activity, BackStackEntry(destinationData)) } fragment } } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/navigator/fragment/backstack/StandardBackstackNavigator.kt ================================================ package zlc.season.butterfly.navigator.fragment.backstack import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import zlc.season.butterfly.core.BackStackEntryManager import zlc.season.butterfly.entities.BackStackEntry import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.navigator.fragment.awaitFragmentResume import zlc.season.butterfly.navigator.fragment.createAndShowFragment class StandardBackstackNavigator( val backStackEntryManager: BackStackEntryManager ) : BackstackNavigator { override suspend fun navigate( activity: FragmentActivity, destinationData: DestinationData ): Fragment { val fragment = activity.createAndShowFragment(destinationData) activity.awaitFragmentResume(fragment) if (destinationData.enableBackStack) { backStackEntryManager.addEntry(activity, BackStackEntry(destinationData)) } return fragment } } ================================================ FILE: butterfly/src/main/java/zlc/season/butterfly/navigator/fragment/group/GroupNavigator.kt ================================================ package zlc.season.butterfly.navigator.fragment.group import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import zlc.season.butterfly.core.GroupEntryManager import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.entities.GroupEntry import zlc.season.butterfly.navigator.fragment.FragmentParamUpdatable import zlc.season.butterfly.navigator.fragment.createAndShowFragment import zlc.season.butterfly.navigator.fragment.findFragment import zlc.season.butterfly.navigator.fragment.hideFragment import zlc.season.butterfly.navigator.fragment.showFragment class GroupNavigator(private val groupEntryManager: GroupEntryManager) { fun navigate( activity: FragmentActivity, request: DestinationData ): Fragment { val list = groupEntryManager.getGroupList(activity, request.groupId) list.forEach { entity -> activity.findFragment(entity.destinationData)?.also { activity.hideFragment(it) } } val targetEntry = list.find { it.destinationData.className == request.className } val targetFragment = targetEntry?.run { activity.findFragment(targetEntry.destinationData) } return if (targetFragment == null) { if (targetEntry == null) { groupEntryManager.addEntry(activity, GroupEntry(request)) } activity.createAndShowFragment(request) } else { // pass new arguments to fragment if (targetFragment is FragmentParamUpdatable) { targetFragment.onParamsUpdate(request.bundle) } activity.showFragment(targetFragment) targetFragment } } } ================================================ FILE: butterfly/src/test/java/zlc/season/butterfly/ExampleUnitTest.kt ================================================ package zlc.season.butterfly 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) } } ================================================ FILE: butterfly-compose/.gitignore ================================================ /build ================================================ FILE: butterfly-compose/build.gradle.kts ================================================ import zlc.season.buildlogic.base.androidLibrary import zlc.season.buildlogic.base.enableCompose @Suppress("DSL_SCOPE_VIOLATION") plugins { alias(libs.plugins.library) alias(libs.plugins.kotlin) alias(libs.plugins.kotlin.parcelize) } androidLibrary { namespace = "zlc.season.butterfly.compose" enableCompose() } dependencies { api(project(":butterfly")) api(libs.compose.ui) api(libs.compose.runtime) api(libs.compose.viewmodel) implementation(libs.bundles.unit.test) } ================================================ FILE: butterfly-compose/consumer-rules.pro ================================================ ================================================ FILE: butterfly-compose/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle.kts. # # 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 ================================================ FILE: butterfly-compose/src/main/AndroidManifest.xml ================================================ ================================================ FILE: butterfly-compose/src/main/java/zlc/season/butterfly/compose/ComposeDestination.kt ================================================ package zlc.season.butterfly.compose import android.os.Bundle import androidx.compose.runtime.Composable open class ComposeDestination( open val composable: (@Composable () -> Unit)? = null, open val paramsComposable: (@Composable (Bundle) -> Unit)? = null, open val viewModelComposable: (@Composable (Any) -> Unit)? = null, open val paramsViewModelComposable: (@Composable (Bundle, Any) -> Unit)? = null, open val viewModelClass: String = "" ) { constructor() : this( null, null, null, null, "" ) } ================================================ FILE: butterfly-compose/src/main/java/zlc/season/butterfly/compose/ComposeNavigator.kt ================================================ package zlc.season.butterfly.compose import android.app.Activity import android.content.Context import android.os.Bundle import androidx.activity.ComponentActivity import zlc.season.butterfly.compose.Utils.isComposeEntry import zlc.season.butterfly.compose.navigator.ComposeBackStackNavigator import zlc.season.butterfly.compose.navigator.ComposeGroupNavigator import zlc.season.butterfly.compose.navigator.ComposeNavigatorHelper import zlc.season.butterfly.core.BackStackEntryManager import zlc.season.butterfly.core.GroupEntryManager import zlc.season.butterfly.entities.BackStackEntry import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.internal.ButterflyHelper.application import zlc.season.butterfly.internal.ButterflyHelper.contentView import zlc.season.butterfly.internal.logd import zlc.season.butterfly.navigator.Navigator import zlc.season.claritypotion.ActivityLifecycleCallbacksAdapter class ComposeNavigator( private val backStackEntryManager: BackStackEntryManager, private val groupEntryManager: GroupEntryManager ) : Navigator { private val composeGroupNavigator = ComposeGroupNavigator(groupEntryManager) private val composeBackStackNavigator = ComposeBackStackNavigator(backStackEntryManager) init { application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacksAdapter() { override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { super.onActivityCreated(activity, savedInstanceState) if (savedInstanceState != null && activity is ComponentActivity) { activity.contentView().post { val composeViewModel = ComposeViewModel.getInstance(activity.viewModelStore) composeViewModel.getAllDestinationDataFlows().forEach { it.value?.let { navigateDirectly(activity, it) } } } } } }) } override fun popBack(activity: Activity, topEntry: BackStackEntry, bundle: Bundle) { if (activity !is ComponentActivity) { "Compose pop back failed! Need ComponentActivity, current activity is: $activity".logd() return } // Clear the ViewModel for the pop back page ComposeViewModel.getInstance(activity.viewModelStore) .clear(topEntry.destinationData.uniqueTag) // Clear the view content for the pop back page ComposeNavigatorHelper.clearComposeView(activity, topEntry.destinationData) with(activity) { val newTopEntry = backStackEntryManager.getTopEntry(this) if (newTopEntry != null && isComposeEntry(newTopEntry)) { // If new top entry is compose entry, launch new top entry directly navigateDirectly(activity, newTopEntry.destinationData) } } } override suspend fun navigate(context: Context, data: DestinationData): Result { return if (context is ComponentActivity) { navigate(context, data) Result.success(Bundle.EMPTY) } else { "Compose navigation failed! Need ComponentActivity, current activity is: $context".logd() Result.failure(IllegalStateException("Invalid activity!")) } } private fun navigateDirectly(activity: ComponentActivity, data: DestinationData) { ComposeNavigatorHelper.navigate(activity, data) } private fun navigate(activity: ComponentActivity, data: DestinationData) { if (data.groupId.isNotEmpty()) { composeGroupNavigator.navigate(activity, data) } else { composeBackStackNavigator.navigate(activity, data) } } } ================================================ FILE: butterfly-compose/src/main/java/zlc/season/butterfly/compose/ComposeViewModel.kt ================================================ package zlc.season.butterfly.compose import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStore import androidx.lifecycle.get import kotlinx.coroutines.flow.MutableStateFlow import zlc.season.butterfly.entities.DestinationData /** * This ViewModel is used to help Compose Destination Page in creating a ViewModel * based on the fallback stack lifecycle. */ class ComposeViewModel : ViewModel() { /** * Save [ComposeViewId : Flow] */ private val destinationDataFlows = mutableMapOf>() /** * Save [destinationDataTag : ViewModelStore] */ private val viewModelStores = mutableMapOf() fun getAllDestinationDataFlows(): List> { return destinationDataFlows.values.toList() } fun getDestinationDataFlow( viewId: Int, data: DestinationData? = null ): MutableStateFlow { var flow = destinationDataFlows[viewId] if (flow == null) { flow = MutableStateFlow(data) destinationDataFlows[viewId] = flow } return flow } fun getViewModelStore(destinationDataTag: String): ViewModelStore { var viewModelStore = viewModelStores[destinationDataTag] if (viewModelStore == null) { viewModelStore = ViewModelStore() viewModelStores[destinationDataTag] = viewModelStore } return viewModelStore } fun clear(destinationDataTag: String) { val viewModelStore = viewModelStores.remove(destinationDataTag) viewModelStore?.clear() } override fun onCleared() { // clear flow destinationDataFlows.values.forEach { it.value = null } destinationDataFlows.clear() // clear viewModelStore viewModelStores.values.forEach { it.clear() } viewModelStores.clear() } override fun toString(): String { val sb = StringBuilder("ComposeViewModel{") sb.append(Integer.toHexString(System.identityHashCode(this))) sb.append("} ViewModelStores (") val viewModelStoreIterator: Iterator = viewModelStores.keys.iterator() while (viewModelStoreIterator.hasNext()) { sb.append(viewModelStoreIterator.next()) if (viewModelStoreIterator.hasNext()) { sb.append(", ") } } sb.append(')') return sb.toString() } companion object { private val FACTORY: ViewModelProvider.Factory = object : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { return ComposeViewModel() as T } } fun getInstance(viewModelStore: ViewModelStore): ComposeViewModel { return ViewModelProvider(viewModelStore, FACTORY).get() } } } ================================================ FILE: butterfly-compose/src/main/java/zlc/season/butterfly/compose/DestinationViewModelStoreOwner.kt ================================================ package zlc.season.butterfly.compose import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner import zlc.season.butterfly.entities.DestinationData class DestinationViewModelStoreOwner( private val composeViewModel: ComposeViewModel, private val destinationData: DestinationData ) : ViewModelStoreOwner { override val viewModelStore: ViewModelStore get() = composeViewModel.getViewModelStore(destinationData.uniqueTag) @Suppress("unchecked_cast") fun getViewModel(composable: ComposeDestination): ViewModel { val viewModelClass = Class.forName(composable.viewModelClass) as Class return ViewModelProvider(this)[viewModelClass] } } ================================================ FILE: butterfly-compose/src/main/java/zlc/season/butterfly/compose/Utils.kt ================================================ package zlc.season.butterfly.compose import android.app.Activity import android.view.ViewGroup import androidx.compose.ui.platform.ComposeView import zlc.season.butterfly.entities.BackStackEntry import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.entities.GroupEntry import zlc.season.butterfly.internal.ButterflyHelper.contentView object Utils { const val COMPOSE_VIEW_TAG = "Butterfly_Compose_View_Tag" fun isComposeEntry(entry: BackStackEntry): Boolean { val cls = Class.forName(entry.destinationData.className) return ComposeDestination::class.java.isAssignableFrom(cls) } fun isComposeEntry(entry: GroupEntry): Boolean { val cls = Class.forName(entry.destinationData.className) return ComposeDestination::class.java.isAssignableFrom(cls) } fun isActivityEntry(entry: BackStackEntry): Boolean { val cls = Class.forName(entry.destinationData.className) return Activity::class.java.isAssignableFrom(cls) } fun Activity.findContainerView(data: DestinationData): ViewGroup { var result: ViewGroup? = null if (data.containerViewId != 0) { result = findViewById(data.containerViewId) } else if (data.containerViewTag.isNotEmpty()) { result = window.decorView.findViewWithTag(data.containerViewTag) } if (result == null) { result = contentView() } return result } fun Activity.getComposeView(data: DestinationData): ComposeView? { val containerView = findContainerView(data) return containerView.findViewWithTag(COMPOSE_VIEW_TAG) } fun Activity.clearContainerView(data: DestinationData) { val composeView = getComposeView(data) composeView?.setContent { } } fun BackStackEntry.hasCustomContainer() = destinationData.containerViewId != 0 || destinationData.containerViewTag.isNotEmpty() fun BackStackEntry.hasSameCustomContainer(other: BackStackEntry) = destinationData.containerViewId == other.destinationData.containerViewId && destinationData.containerViewTag == other.destinationData.containerViewTag } ================================================ FILE: butterfly-compose/src/main/java/zlc/season/butterfly/compose/navigator/ComposeBackStackNavigator.kt ================================================ package zlc.season.butterfly.compose.navigator import androidx.activity.ComponentActivity import zlc.season.butterfly.compose.ComposeViewModel import zlc.season.butterfly.core.BackStackEntryManager import zlc.season.butterfly.entities.BackStackEntry import zlc.season.butterfly.entities.DestinationData class ComposeBackStackNavigator(private val backStackEntryManager: BackStackEntryManager) { fun navigate(activity: ComponentActivity, data: DestinationData) { if (data.clearTop) { clearTopNavigate(activity, data) } else if (data.singleTop) { singleTopNavigate(activity, data) } else { standardNavigate(activity, data) } } private fun standardNavigate(activity: ComponentActivity, data: DestinationData) { if (data.enableBackStack) { backStackEntryManager.addEntry(activity, BackStackEntry(data)) } ComposeNavigatorHelper.navigate(activity, data) } private fun clearTopNavigate(activity: ComponentActivity, data: DestinationData) { val topEntryList = backStackEntryManager.getTopEntryList(activity, data) return if (topEntryList.isEmpty()) { standardNavigate(activity, data) } else { // do not remove target entry. val topEntry = topEntryList.removeFirst() backStackEntryManager.removeEntryList(activity, topEntryList) // Clear removed entry resources. topEntryList.forEach { each -> // Clear ViewModel ComposeViewModel.getInstance(activity.viewModelStore) .clear(each.destinationData.uniqueTag) // Clear view content ComposeNavigatorHelper.clearComposeView(activity, each.destinationData) } // update old destination's bundle data val newData = topEntry.destinationData.copy(bundle = data.bundle) ComposeNavigatorHelper.navigate(activity, newData) } } private fun singleTopNavigate(activity: ComponentActivity, data: DestinationData) { val topEntry = backStackEntryManager.getTopEntry(activity) return if (topEntry != null && topEntry.destinationData.className == data.className) { // update old destination's bundle data val newData = topEntry.destinationData.copy(bundle = data.bundle) ComposeNavigatorHelper.navigate(activity, newData) } else { standardNavigate(activity, data) } } } ================================================ FILE: butterfly-compose/src/main/java/zlc/season/butterfly/compose/navigator/ComposeGroupNavigator.kt ================================================ package zlc.season.butterfly.compose.navigator import androidx.activity.ComponentActivity import zlc.season.butterfly.core.GroupEntryManager import zlc.season.butterfly.entities.DestinationData import zlc.season.butterfly.entities.GroupEntry class ComposeGroupNavigator(private val groupEntryManager: GroupEntryManager) { fun navigate(activity: ComponentActivity, data: DestinationData) { val groupEntryList = groupEntryManager.getGroupList(activity, data.groupId) val find = groupEntryList.find { it.destinationData.className == data.className } if (find == null) { groupEntryManager.addEntry(activity, GroupEntry(data)) } ComposeNavigatorHelper.navigate(activity, data) } } ================================================ FILE: butterfly-compose/src/main/java/zlc/season/butterfly/compose/navigator/ComposeNavigatorHelper.kt ================================================ package zlc.season.butterfly.compose.navigator import android.view.View import android.view.ViewGroup import androidx.activity.ComponentActivity import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import zlc.season.butterfly.compose.ComposeDestination import zlc.season.butterfly.compose.ComposeViewModel import zlc.season.butterfly.compose.DestinationViewModelStoreOwner import zlc.season.butterfly.compose.Utils import zlc.season.butterfly.compose.Utils.findContainerView import zlc.season.butterfly.entities.DestinationData object ComposeNavigatorHelper { fun clearComposeView(activity: ComponentActivity, data: DestinationData) { val containerView = activity.findContainerView(data) val composeView = containerView.findViewWithTag(Utils.COMPOSE_VIEW_TAG) if (composeView != null) { val composeViewModel = ComposeViewModel.getInstance(activity.viewModelStore) val destinationDataFlow = composeViewModel.getDestinationDataFlow(composeView.id) destinationDataFlow.value = null } } fun navigate(activity: ComponentActivity, data: DestinationData) { val containerView = activity.findContainerView(data) var composeView = containerView.findViewWithTag(Utils.COMPOSE_VIEW_TAG) if (composeView == null) { composeView = createComposeView(containerView) val composeViewModel = ComposeViewModel.getInstance(activity.viewModelStore) val destinationDataFlow = composeViewModel.getDestinationDataFlow(composeView.id) destinationDataFlow.value = data composeView.setContent { val destinationData by destinationDataFlow.collectAsState() destinationData?.let { val viewModelStoreOwner = DestinationViewModelStoreOwner(composeViewModel, it) val composeDestination = createComposeDestination(it) CompositionLocalProvider(LocalViewModelStoreOwner provides viewModelStoreOwner) { when { composeDestination.paramsViewModelComposable != null -> { val viewModel = viewModelStoreOwner.getViewModel(composeDestination) composeDestination.paramsViewModelComposable?.invoke( data.bundle, viewModel ) } composeDestination.viewModelComposable != null -> { val viewModel = viewModelStoreOwner.getViewModel(composeDestination) composeDestination.viewModelComposable?.invoke(viewModel) } composeDestination.paramsComposable != null -> { composeDestination.paramsComposable?.invoke(data.bundle) } else -> { composeDestination.composable?.invoke() } } } } } } else { val composeViewModel = ComposeViewModel.getInstance(activity.viewModelStore) val destinationDataFlow = composeViewModel.getDestinationDataFlow(composeView.id) destinationDataFlow.value = data } } private fun createComposeView(containerView: ViewGroup): ComposeView { val composeView = ComposeView(containerView.context).apply { id = View.generateViewId() tag = Utils.COMPOSE_VIEW_TAG } containerView.addView( composeView, ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) ) return composeView } private fun createComposeDestination(data: DestinationData): ComposeDestination { return Class.forName(data.className) .getDeclaredConstructor() .newInstance() as ComposeDestination } } ================================================ FILE: butterfly-compose/src/test/java/zlc/season/butterfly/compose/ExampleUnitTest.kt ================================================ package zlc.season.butterfly.compose 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) } } ================================================ FILE: compiler/.gitignore ================================================ /build ================================================ FILE: compiler/build.gradle.kts ================================================ @file:Suppress("DSL_SCOPE_VIOLATION", "UnstableApiUsage") plugins { kotlin("jvm") } group = "com.github.ssseasonnn" dependencies { implementation(project(":annotation")) implementation(libs.kotlin.poet) implementation("com.google.devtools.ksp:symbol-processing-api:1.9.20-1.0.14") } ================================================ FILE: compiler/src/main/java/zlc/season/butterfly/compiler/Entities.kt ================================================ package zlc.season.butterfly.compiler data class EvadeImplInfo(val className: String, val singleton: Boolean) data class ComposeDestinationInfo( val packageName: String, val methodName: String, val hasBundle: Boolean, val viewModelName: String, ) ================================================ FILE: compiler/src/main/java/zlc/season/butterfly/compiler/ProcessorProvider.kt ================================================ package zlc.season.butterfly.compiler import com.google.devtools.ksp.processing.Dependencies 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.google.devtools.ksp.symbol.KSFile import zlc.season.butterfly.annotation.Destination import zlc.season.butterfly.annotation.Evade import zlc.season.butterfly.annotation.EvadeImpl import zlc.season.butterfly.compiler.generator.ComposableGenerator import zlc.season.butterfly.compiler.generator.ModuleClassGenerator import zlc.season.butterfly.compiler.utils.BUTTERFLY_LOG_ENABLE import zlc.season.butterfly.compiler.utils.DEFAULT_GENERATE_COMPOSABLE_PACKAGE_NAME import zlc.season.butterfly.compiler.utils.DEFAULT_GENERATE_MODULE_PACKAGE import zlc.season.butterfly.compiler.utils.TEMP_FILE_NAME import zlc.season.butterfly.compiler.utils.composeDestinationClassName import zlc.season.butterfly.compiler.utils.getGenerateModuleClassName import zlc.season.butterfly.compiler.visitor.DestinationAnnotationVisitor import zlc.season.butterfly.compiler.visitor.EvadeAnnotationVisitor import zlc.season.butterfly.compiler.visitor.EvadeImplAnnotationVisitor import java.io.File class ProcessorProvider : SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { environment.options.forEach { environment.logc("options: $it") } return ButterflySymbolProcessor(environment) } } private class ButterflySymbolProcessor(private val environment: SymbolProcessorEnvironment) : SymbolProcessor { private val destinationMap = mutableMapOf() private val evadeMap = mutableMapOf() private val evadeImplMap = mutableMapOf() private val composableList = mutableListOf() private val sourceFileList = mutableListOf() private var packageName = DEFAULT_GENERATE_MODULE_PACKAGE override fun process(resolver: Resolver): List { processDestinationSymbols(resolver, sourceFileList) processEvadeSymbols(resolver, sourceFileList) processEvadeImplSymbols(resolver, sourceFileList) return emptyList() } private fun processDestinationSymbols( resolver: Resolver, sourcesFile: MutableList ) { environment.logt("Start process Destination symbols...") val destinationSymbols = resolver.getSymbolsWithAnnotation(Destination::class.qualifiedName!!) environment.logc("find destination list: ${destinationSymbols.toList()}") val visitor = DestinationAnnotationVisitor(environment, resolver, destinationMap, composableList, sourcesFile) destinationSymbols.toList().forEach { it.accept(visitor, Unit) } environment.logc("process Destination symbols end.") } private fun processEvadeSymbols( resolver: Resolver, sourcesFile: MutableList ) { environment.logt("Start process Evade symbols...") val evadeSymbols = resolver.getSymbolsWithAnnotation(Evade::class.qualifiedName!!) environment.logc("find evade list: ${evadeSymbols.toList()}") val visitor = EvadeAnnotationVisitor(environment, evadeMap, sourcesFile) evadeSymbols.toList().forEach { it.accept(visitor, Unit) } environment.logc("process Evade symbols end.") } private fun processEvadeImplSymbols( resolver: Resolver, sourcesFile: MutableList ) { environment.logt("Start process EvadeImpl symbols...") val evadeImplSymbols = resolver.getSymbolsWithAnnotation(EvadeImpl::class.qualifiedName!!) environment.logc("find evade impl list: ${evadeImplSymbols.toList()}") val visitor = EvadeImplAnnotationVisitor(environment, evadeImplMap, sourcesFile) evadeImplSymbols.toList().forEach { it.accept(visitor, Unit) } environment.logc("process EvadeImpl symbols end.") } override fun finish() { // create an empty temp file to get current module name val tempOutputFile = environment.codeGenerator.createNewFile( Dependencies(true), packageName = packageName, fileName = TEMP_FILE_NAME, ) tempOutputFile.close() val tempFile = environment.codeGenerator.generatedFile.find { it.name.startsWith(TEMP_FILE_NAME) } tempFile?.let { // generate composable class file first. if (composableList.isNotEmpty()) { generateComposeDestinationClass() } // generate module class file. generateModuleClass(it) } } private fun generateComposeDestinationClass() { environment.logt("Generate compose destination classes...") val composableGenerator = ComposableGenerator() composableList.forEach { composableInfo -> val composableClassName = composeDestinationClassName(composableInfo.methodName) environment.logc("generate compose destination class file: $composableClassName") val composableClassFile = environment.codeGenerator.createNewFile( Dependencies(true), packageName = DEFAULT_GENERATE_COMPOSABLE_PACKAGE_NAME, fileName = composeDestinationClassName(composableInfo.methodName) ) val composableClassContent = composableGenerator.createFileSpec(composableInfo).toString() composableClassFile.write(composableClassContent.toByteArray()) composableClassFile.close() } } private fun generateModuleClass(tempFile: File) { val moduleClassName = getGenerateModuleClassName(tempFile.absolutePath) environment.logt("Generate module class file: $moduleClassName") val moduleClassFile = environment.codeGenerator.createNewFile( Dependencies(true, *sourceFileList.toTypedArray()), packageName = packageName, fileName = moduleClassName ) val moduleClassGenerator = ModuleClassGenerator(packageName, moduleClassName, destinationMap, evadeMap, evadeImplMap) val moduleClassContent = moduleClassGenerator.generate().toString() moduleClassFile.write(moduleClassContent.toByteArray()) moduleClassFile.close() } } internal fun SymbolProcessorEnvironment.logt(log: String) { if (isEnableLog()) { logger.warn("==== $log") } } internal fun SymbolProcessorEnvironment.logc(log: String) { if (isEnableLog()) { logger.warn("---- $log") } } internal fun SymbolProcessorEnvironment.loge(log: String) { logger.error(log) } private fun SymbolProcessorEnvironment.isEnableLog(): Boolean { val value = options[BUTTERFLY_LOG_ENABLE] return value == "true" } ================================================ FILE: compiler/src/main/java/zlc/season/butterfly/compiler/generator/ComposableGenerator.kt ================================================ package zlc.season.butterfly.compiler.generator import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.LambdaTypeName.Companion.get import com.squareup.kotlinpoet.ParameterSpec import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.asTypeName import zlc.season.butterfly.compiler.ComposeDestinationInfo import zlc.season.butterfly.compiler.utils.DEFAULT_GENERATE_COMPOSABLE_PACKAGE_NAME import zlc.season.butterfly.compiler.utils.composeDestinationClassName import zlc.season.butterfly.compiler.generator.ComposableHelper.composableLambdaType import zlc.season.butterfly.compiler.generator.ComposableHelper.paramsComposableLambdaType import zlc.season.butterfly.compiler.generator.ComposableHelper.paramsViewModelComposableLambdaType import zlc.season.butterfly.compiler.generator.ComposableHelper.viewModelComposableLambdaType import zlc.season.butterfly.compiler.utils.COMPOSE_DESTINATION_CLASS /** Generated file: package zlc.season.butterfly.compose import android.os.Bundle import androidx.compose.runtime.Composable import kotlin.Any import kotlin.String import kotlin.Unit import zlc.season.butterfly.compose.ComposeDestination import zlc.season.compose.dashboard.DashboardScreen public class DashboardScreenComposeDestination : ComposeDestination() { public override val composable: @Composable (() -> Unit)? = @Composable { DashboardScreen() } public override val paramsComposable: @Composable ((Bundle) -> Unit)? = @Composable { bundle -> DashboardScreen(bundle) } public override val viewModelComposable: @Composable ((Any) -> Unit)? = @Composable { viewModel -> DashboardScreen(viewModel as zlc.season.compose.dashboard.DashboardViewModel) } public override val paramsViewModelComposable: @Composable ((Bundle, Any) -> Unit)? = @Composable { bundle, viewModel -> DashboardScreen( bundle, viewModel as zlc.season.compose.dashboard.DashboardViewModel ) } public override val viewModelClass: String = "zlc.season.compose.dashboard.DashboardViewModel" } */ internal object ComposableHelper { private val composeAnnotationCls = ClassName("androidx.compose.runtime", "Composable") private val composeAnnotation = AnnotationSpec.builder(composeAnnotationCls).build() private val bundleCls = ClassName("android.os", "Bundle") private val bundleParams = ParameterSpec.unnamed(bundleCls) private val anyParams = ParameterSpec.unnamed(Any::class) private val unitType = Unit::class.asTypeName() val composableLambdaType = get(returnType = unitType).copy(annotations = arrayListOf(composeAnnotation), nullable = true) val viewModelComposableLambdaType = get(parameters = listOf(anyParams), returnType = unitType) .copy(annotations = arrayListOf(composeAnnotation), nullable = true) val paramsComposableLambdaType = get(parameters = listOf(bundleParams), returnType = unitType) .copy(annotations = arrayListOf(composeAnnotation), nullable = true) val paramsViewModelComposableLambdaType = get(parameters = listOf(bundleParams, anyParams), returnType = unitType) .copy(annotations = arrayListOf(composeAnnotation), nullable = true) val superCls = ClassName(DEFAULT_GENERATE_COMPOSABLE_PACKAGE_NAME, COMPOSE_DESTINATION_CLASS) } internal class ComposableGenerator { fun createFileSpec(composeDestinationInfo: ComposeDestinationInfo): FileSpec { val classBuilder = TypeSpec.classBuilder(composeDestinationClassName(composeDestinationInfo.methodName)) .superclass(ComposableHelper.superCls) .apply { if (composeDestinationInfo.hasBundle) { if (composeDestinationInfo.viewModelName.isNotEmpty()) { addProperty( PropertySpec.builder("paramsViewModelComposable", paramsViewModelComposableLambdaType) .addModifiers(KModifier.OVERRIDE) .initializer( """@Composable { bundle, viewModel -> ${composeDestinationInfo.methodName}(bundle, viewModel as ${composeDestinationInfo.viewModelName}) }""".trimIndent() ) .build() ) addProperty( PropertySpec.builder("viewModelClass", String::class) .addModifiers(KModifier.OVERRIDE) .initializer( """ "${composeDestinationInfo.viewModelName}" """.trimIndent() ) .build() ) } else { addProperty( PropertySpec.builder("paramsComposable", paramsComposableLambdaType) .addModifiers(KModifier.OVERRIDE) .initializer( """@Composable { bundle -> ${composeDestinationInfo.methodName}(bundle) }""".trimIndent() ) .build() ) } } else { if (composeDestinationInfo.viewModelName.isNotEmpty()) { addProperty( PropertySpec.builder("viewModelComposable", viewModelComposableLambdaType) .addModifiers(KModifier.OVERRIDE) .initializer( """@Composable { viewModel -> ${composeDestinationInfo.methodName}(viewModel as ${composeDestinationInfo.viewModelName}) }""".trimIndent() ) .build() ) addProperty( PropertySpec.builder("viewModelClass", String::class) .addModifiers(KModifier.OVERRIDE) .initializer( """ "${composeDestinationInfo.viewModelName}" """.trimIndent() ) .build() ) } else { addProperty( PropertySpec.builder("composable", composableLambdaType) .addModifiers(KModifier.OVERRIDE) .initializer( """@Composable { ${composeDestinationInfo.methodName}() }""".trimIndent() ) .build() ) } } } return FileSpec.builder(DEFAULT_GENERATE_COMPOSABLE_PACKAGE_NAME, composeDestinationClassName(composeDestinationInfo.methodName)) .addType(classBuilder.build()) .addImport(ClassName(composeDestinationInfo.packageName, composeDestinationInfo.methodName), "") .addImport(ComposableHelper.superCls, "") .build() } } ================================================ FILE: compiler/src/main/java/zlc/season/butterfly/compiler/generator/ModuleClassGenerator.kt ================================================ package zlc.season.butterfly.compiler.generator import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.typeNameOf import zlc.season.butterfly.annotation.EvadeData import zlc.season.butterfly.compiler.EvadeImplInfo import zlc.season.butterfly.module.Module /** * public class ButterflyModuleApp() : Module { * public val destinationMap: HashMap = hashMapOf() * public val evadeMap: HashMap = hashMapOf() * public val evadeImplMap: HashMap = hashMapOf() * * init { * destinationMap["/path/foo"] = "zlc.season.butterflydemo.MainActivity" * destinationMap["/path/bar"] = "zlc.season.butterflydemo.TestActivity" * } * * * init { * evadeMap["/path/foo"] = "zlc.season.butterflydemo.Foo" * evadeMap["path/bar"] = "zlc.season.butterflydemo.Bar" * } * * * init { * evadeImplMap["/path/foo"] = EvadeData("zlc.season.butterflydemo.Foo",false) * evadeImplMap["/path/bar"] = EvadeData("zlc.season.butterflydemo.Bar",false) * } * * public override fun getDestination(): HashMap = destinationMap * public override fun getEvade(): HashMap = evadeMap * public override fun getEvadeImpl(): HashMap = evadeImplMap * } */ internal class ModuleClassGenerator( private val packageName: String, private val className: String, private val destinationMap: Map, private val evadeMap: Map, private val evadeImplMap: Map, ) { private val moduleClass = Module::class.asClassName() private val mapClass = typeNameOf>>() private val mapDataClass = HashMap::class.asClassName().parameterizedBy(String::class.asClassName(), EvadeData::class.asClassName()) fun generate(): FileSpec { val companion = TypeSpec.companionObjectBuilder() .addFunction( FunSpec.builder("doNothing") .returns(moduleClass) .addStatement("return ${className}()") .build() ) .build() val classBuilder = TypeSpec.classBuilder(className) .addSuperinterface(moduleClass) .primaryConstructor(FunSpec.constructorBuilder().build()) .addProperty( PropertySpec.builder("destinationMap", mapClass) .initializer("hashMapOf>()") .build() ) .addProperty( PropertySpec.builder("evadeMap", mapClass) .initializer("hashMapOf>()") .build() ) .addProperty( PropertySpec.builder("evadeImplMap", mapDataClass) .initializer("hashMapOf()") .build() ) .addInitializerBlock( generateDestinationMapBlock() ) .addInitializerBlock( generateEvadeMapBlock() ) .addInitializerBlock( generateEvadeImplMapBlock() ) .addFunction( FunSpec.builder("getDestination") .addModifiers(KModifier.OVERRIDE) .addStatement("return destinationMap") .returns(mapClass) .build() ) .addFunction( FunSpec.builder("getEvade") .addModifiers(KModifier.OVERRIDE) .addStatement("return evadeMap") .returns(mapClass) .build() ) .addFunction( FunSpec.builder("getEvadeImpl") .addModifiers(KModifier.OVERRIDE) .addStatement("return evadeImplMap") .returns(mapDataClass) .build() ) .addType(companion) return FileSpec.builder(packageName, className) .addType(classBuilder.build()) .build() } private fun generateDestinationMapBlock(): CodeBlock { val builder = CodeBlock.Builder() destinationMap.forEach { (k, v) -> builder.addStatement("""destinationMap["$k"] = ${v}::class.java """) } return builder.build() } private fun generateEvadeMapBlock(): CodeBlock { val builder = CodeBlock.Builder() evadeMap.forEach { (k, v) -> builder.addStatement("""evadeMap["$k"] = ${v}::class.java """) } return builder.build() } private fun generateEvadeImplMapBlock(): CodeBlock { val builder = CodeBlock.Builder() evadeImplMap.forEach { (k, v) -> builder.addStatement("""evadeImplMap["$k"] = EvadeData(cls=${(v.className)}::class.java, singleton=${v.singleton}) """) } return builder.build() } } ================================================ FILE: compiler/src/main/java/zlc/season/butterfly/compiler/utils/KspUtils.kt ================================================ package zlc.season.butterfly.compiler.utils import com.google.devtools.ksp.symbol.KSAnnotation import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSFunctionDeclaration import com.google.devtools.ksp.symbol.KSType const val BUTTERFLY_LOG_ENABLE = "butterfly.log.enable" /** * Get class full name by KSType. * eg: com.example.Test */ internal fun KSType.getClassFullName(): String { val ksClassDeclaration = declaration as KSClassDeclaration val packageName = ksClassDeclaration.packageName.asString() val className = ksClassDeclaration.simpleName.asString() return "$packageName.$className" } /** * Get class full name by KSClassDeclaration. * eg: com.example.Test */ internal fun KSClassDeclaration.getClassFullName(): String { return "${packageName.asString()}.${simpleName.asString()}" } /** * Get annotation's value by key */ @Suppress("UNCHECKED_CAST") internal fun KSAnnotation.getValue(key: String, defaultValue: T): T { return arguments.find { it.name?.asString() == key }?.value as? T ?: defaultValue } internal fun KSFunctionDeclaration.getAnnotationByName(name: String): KSAnnotation? { return annotations.toList().find { it.shortName.asString() == name } } internal fun KSClassDeclaration.getAnnotationByName(name: String): KSAnnotation? { return annotations.toList().find { it.shortName.asString() == name } } ================================================ FILE: compiler/src/main/java/zlc/season/butterfly/compiler/utils/Util.kt ================================================ package zlc.season.butterfly.compiler.utils import java.util.Locale import java.io.File.separatorChar as s /** * default package name for generate module class. */ const val DEFAULT_GENERATE_MODULE_PACKAGE = "zlc.season.butterfly.module" const val DEFAULT_GENERATE_COMPOSABLE_PACKAGE_NAME = "zlc.season.butterfly.compose" const val TEMP_FILE_NAME = "Temp" const val BUNDLE_CLASS_NAME = "android.os.Bundle" const val VIEW_MODEL_CLASS_NAME = "androidx.lifecycle.ViewModel" const val DESTINATION_NAME = "Destination" const val EVADE_NAME = "Evade" const val EVADE_IMPL_NAME = "EvadeImpl" const val COMPOSE_DESTINATION_CLASS = "ComposeDestination" const val DESTINATION_ROUTE_KEY = "route" const val EVADE_IDENTITY_KEY = "identity" const val EVADE_SINGLETON_KEY = "singleton" const val EVADE_IMPL_SUFFIX = "Impl" /** * get generate module class name. eg: ButterflyHomeModule */ internal fun getGenerateModuleClassName(generateDir: String): String { return try { val kspGenDir = "${s}build${s}generated${s}ksp" val pathIndex = generateDir.lastIndexOf(kspGenDir) val subStr = generateDir.substring(0, pathIndex) val lastIndex = subStr.lastIndexOf(s) val result = subStr.substring(lastIndex + 1) "Butterfly${result.camelCase()}Module" } catch (e: Exception) { "ButterflyDefaultModule" } } /** * The name of each Composable function Class. */ internal fun composeDestinationFullClassName(methodName: String): String { return "$DEFAULT_GENERATE_COMPOSABLE_PACKAGE_NAME.${composeDestinationClassName(methodName)}" } /** * The name of generated ComposeDestination class. * eg: * @Destination("path") * @Composable * fun Test(){} * * will generate class: * class TestComposeDestination: ComposeDestination {} */ internal fun composeDestinationClassName(methodName: String): String { return "${methodName}${COMPOSE_DESTINATION_CLASS}" } internal fun String.camelCase(): String { val words: List = split("[\\W_]+".toRegex()) val builder = StringBuilder() words.forEach { val word = if (it.isEmpty()) it else it[0].uppercase() + it.substring(1).lowercase() builder.append(word) } return builder.toString() } internal fun String.cap(): String { return replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } } ================================================ FILE: compiler/src/main/java/zlc/season/butterfly/compiler/visitor/DestinationAnnotationVisitor.kt ================================================ package zlc.season.butterfly.compiler.visitor import com.google.devtools.ksp.getClassDeclarationByName 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.KSFile import com.google.devtools.ksp.symbol.KSFunctionDeclaration import com.google.devtools.ksp.symbol.KSVisitorVoid import zlc.season.butterfly.compiler.ComposeDestinationInfo import zlc.season.butterfly.compiler.logc import zlc.season.butterfly.compiler.loge import zlc.season.butterfly.compiler.utils.BUNDLE_CLASS_NAME import zlc.season.butterfly.compiler.utils.DESTINATION_NAME import zlc.season.butterfly.compiler.utils.DESTINATION_ROUTE_KEY import zlc.season.butterfly.compiler.utils.VIEW_MODEL_CLASS_NAME import zlc.season.butterfly.compiler.utils.composeDestinationFullClassName import zlc.season.butterfly.compiler.utils.getAnnotationByName import zlc.season.butterfly.compiler.utils.getClassFullName import zlc.season.butterfly.compiler.utils.getValue class DestinationAnnotationVisitor( private val environment: SymbolProcessorEnvironment, private val resolver: Resolver, private val destinationMap: MutableMap, private val composeList: MutableList, private val sourcesFile: MutableList ) : KSVisitorVoid() { private val bundleClassType = resolver.getClassDeclarationByName(BUNDLE_CLASS_NAME)!!.asStarProjectedType() private val viewModelClassType = resolver.getClassDeclarationByName(VIEW_MODEL_CLASS_NAME)!!.asStarProjectedType() override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) { environment.logc("process destination: $classDeclaration") val annotation = classDeclaration.getAnnotationByName(DESTINATION_NAME) if (annotation != null) { val routeValue = annotation.getValue(DESTINATION_ROUTE_KEY, "") val className = classDeclaration.getClassFullName() if (routeValue.isNotEmpty()) { environment.logc("destination processed: [route='$routeValue', target='$className']") destinationMap[routeValue] = className // add file to dependency sourcesFile.add(classDeclaration.containingFile!!) } else { environment.loge("[$classDeclaration] route not found!") } } } override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: Unit) { environment.logc("process compose destination: $function") val annotation = function.getAnnotationByName(DESTINATION_NAME) if (annotation != null) { val packageName = function.packageName.asString() val methodName = function.simpleName.asString() val routeValue = annotation.getValue(DESTINATION_ROUTE_KEY, "") if (routeValue.isNotEmpty()) { if (function.parameters.isNotEmpty()) { when (function.parameters.size) { 1 -> { val parameterKsType = function.parameters[0].type.resolve() if (bundleClassType.isAssignableFrom(parameterKsType)) { composeList.add(ComposeDestinationInfo(packageName, methodName, true, "")) } else if (viewModelClassType.isAssignableFrom(parameterKsType)) { val viewModelClassName = parameterKsType.getClassFullName() composeList.add(ComposeDestinationInfo(packageName, methodName, false, viewModelClassName)) } else { environment.loge("[$function] invalid parameter! Compose only support Bundle or ViewModel type!") } } 2 -> { val firstParameterKsType = function.parameters[0].type.resolve() val secondParameterKsType = function.parameters[1].type.resolve() val isBundleFirst = bundleClassType.isAssignableFrom(firstParameterKsType) val isViewModelSecond = viewModelClassType.isAssignableFrom(secondParameterKsType) if (isBundleFirst && isViewModelSecond) { val viewModelClassName = secondParameterKsType.getClassFullName() composeList.add(ComposeDestinationInfo(packageName, methodName, true, viewModelClassName)) } else { if (!isBundleFirst) { environment.loge("[$function] first parameter type must be Bundle!") } else { environment.loge("[$function] second parameter type must be ViewModel!") } } } else -> { environment.loge("[$function] invalid parameter size! Compose only support max 2 parameters!") } } } else { composeList.add(ComposeDestinationInfo(packageName, methodName, false, "")) } val targetClassName = composeDestinationFullClassName(methodName) environment.logc("compose destination processed: [route='$routeValue', target='$targetClassName']") destinationMap[routeValue] = targetClassName // add file to dependency sourcesFile.add(function.containingFile!!) } else { environment.loge("[$function] route not found!") } } } } ================================================ FILE: compiler/src/main/java/zlc/season/butterfly/compiler/visitor/EvadeAnnotationVisitor.kt ================================================ package zlc.season.butterfly.compiler.visitor import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.symbol.ClassKind import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSFile import com.google.devtools.ksp.symbol.KSVisitorVoid import zlc.season.butterfly.compiler.utils.EVADE_IDENTITY_KEY import zlc.season.butterfly.compiler.utils.getClassFullName import zlc.season.butterfly.compiler.utils.getValue import zlc.season.butterfly.compiler.logc import zlc.season.butterfly.compiler.loge import zlc.season.butterfly.compiler.utils.EVADE_NAME import zlc.season.butterfly.compiler.utils.getAnnotationByName class EvadeAnnotationVisitor( private val environment: SymbolProcessorEnvironment, private val evadeMap: MutableMap, private val sourcesFile: MutableList ) : KSVisitorVoid() { override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) { if (classDeclaration.classKind == ClassKind.INTERFACE) { environment.logc("process evade: $classDeclaration") val annotation = classDeclaration.getAnnotationByName(EVADE_NAME) if (annotation != null) { val identityValue = annotation.getValue(EVADE_IDENTITY_KEY, "") val realKey = identityValue.ifEmpty { classDeclaration.simpleName.asString() } val targetClassName = classDeclaration.getClassFullName() environment.logc("evade processed: [identity='$realKey', target='$targetClassName']") evadeMap[realKey] = targetClassName // add file to dependency sourcesFile.add(classDeclaration.containingFile!!) } } else { environment.loge("[$classDeclaration] invalid evade. @Evade must be annotated at an interface!") } } } ================================================ FILE: compiler/src/main/java/zlc/season/butterfly/compiler/visitor/EvadeImplAnnotationVisitor.kt ================================================ package zlc.season.butterfly.compiler.visitor import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.symbol.ClassKind import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSFile import com.google.devtools.ksp.symbol.KSVisitorVoid import zlc.season.butterfly.compiler.utils.EVADE_IDENTITY_KEY import zlc.season.butterfly.compiler.utils.EVADE_IMPL_SUFFIX import zlc.season.butterfly.compiler.utils.EVADE_SINGLETON_KEY import zlc.season.butterfly.compiler.EvadeImplInfo import zlc.season.butterfly.compiler.utils.getClassFullName import zlc.season.butterfly.compiler.utils.getValue import zlc.season.butterfly.compiler.logc import zlc.season.butterfly.compiler.loge import zlc.season.butterfly.compiler.utils.EVADE_IMPL_NAME import zlc.season.butterfly.compiler.utils.EVADE_NAME import zlc.season.butterfly.compiler.utils.getAnnotationByName class EvadeImplAnnotationVisitor( private val environment: SymbolProcessorEnvironment, private val evadeImplMap: MutableMap, private val sourcesFile: MutableList ) : KSVisitorVoid() { override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) { if (classDeclaration.classKind == ClassKind.CLASS) { environment.logc("process evade impl: $classDeclaration") val annotation = classDeclaration.getAnnotationByName(EVADE_IMPL_NAME) if (annotation != null) { val isSingleton = annotation.getValue(EVADE_SINGLETON_KEY, true) val identityValue = annotation.getValue(EVADE_IDENTITY_KEY, "") val classSimpleName = classDeclaration.simpleName.asString() if (identityValue.isEmpty() && !classSimpleName.endsWith(EVADE_IMPL_SUFFIX)) { environment.loge("[$classDeclaration] invalid evade impl. If your @EvadeImpl class does not provide identity value, then the class name must end with Impl!") } else { val realKey = identityValue.ifEmpty { val index = classSimpleName.lastIndexOf(EVADE_IMPL_SUFFIX) classSimpleName.substring(0, index) } val targetClassName = classDeclaration.getClassFullName() environment.logc("evade impl processed: [identity='$realKey', target='$targetClassName']") evadeImplMap[realKey] = EvadeImplInfo(targetClassName, isSingleton) // add file to dependency sourcesFile.add(classDeclaration.containingFile!!) } } } else { environment.loge("[$classDeclaration] invalid evade impl. @EvadeImpl must be annotated at an class!") } } } ================================================ FILE: compiler/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider ================================================ zlc.season.butterfly.compiler.ProcessorProvider ================================================ FILE: gradle/libs.versions.toml ================================================ [versions] # android sdk-min-version = "23" sdk-target-version = "34" sdk-compile-version = "34" # library gradle = "8.1.4" kover = "0.7.3" detekt = "1.23.0" ksp = "1.9.20-1.0.14" poet = "1.11.0" auto-service = "1.0" # kotlin kotlin = "1.9.20" kotlinx-coroutines = "1.7.2" kotlinx-serialization-json = "1.5.1" # android appcompat = "1.6.1" fragment-ktx = "1.6.2" activity-ktx = "1.8.1" core-ktx = "1.12.0" arch-lifecycle = "2.6.2" arch-lifecycle-extension = "2.2.0" hilt = "2.46.1" # ui material = "1.10.0" constraint-layout = "2.1.4" # compose # compose compiler compose-compiler = "1.5.5" activity-compose = "1.8.1" compose = "1.5.4" compose-material3 = "1.1.2" accompanist = "0.31.3-beta" # http retrofit = "2.9.0" retrofit-kotlinx-serialization-json = "1.0.0" okhttp-logging = "4.11.0" coil = "2.4.0" exoplayer = "2.19.1" # leak leakcanary = "2.9.1" # season butterfly = "1.0.1" clarity = "1.0.6" yasha = "1.1.4" bracer = "1.0.7" # test arch-test = "2.2.0" junit = "4.13.2" mockk = "1.13.5" truth = "1.1.5" robolectric = "4.10.3" turbine = "1.0.0" slf4j = "2.0.7" # android test androidx-junit = "1.1.5" androidx-test-core = "1.5.0" androidx-test-runner = "1.5.2" androidx-test-rules = "1.5.0" espresso-core = "3.5.1" [libraries] # tools kotlin-poet = { module = "com.squareup:kotlinpoet", version.ref = "poet" } auto-service = { module = "com.google.auto.service:auto-service", version.ref = "auto-service" } # plugin android-gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } android-gradle-api = { module = "com.android.tools.build:gradle-api", version.ref = "gradle" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } # kotlin kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } kotlin-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } # android appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" } activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activity-ktx" } fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragment-ktx" } constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraint-layout" } material = { module = "com.google.android.material:material", version.ref = "material" } accompanist-systemuicontroller = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanist" } accompanist-permission = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } # lifecycle lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "arch-lifecycle" } lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "arch-lifecycle" } lifecycle-extensions = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "arch-lifecycle-extension" } # compose compose-activity = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } compose-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "arch-lifecycle" } compose-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "arch-lifecycle" } compose-animation = { module = "androidx.compose.animation:animation", version.ref = "compose" } compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } compose-material = { module = "androidx.compose.material:material", version.ref = "compose" } compose-material-icons-core = { module = "androidx.compose.material:material-icons-core", version.ref = "compose" } compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "compose" } compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" } compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "compose" } compose-runtime-saveable = { module = "androidx.compose.runtime:runtime-saveable", version.ref = "compose" } compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } compose-ui-viewbinding = { module = "androidx.compose.ui:ui-viewbinding", version.ref = "compose" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } compose-ui-test = { module = "androidx.compose.ui:ui-test", version.ref = "compose" } compose-ui-test-junit = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" } compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" } compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" } # hilt hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } # retrofit retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-kotlin-serialization = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit-kotlinx-serialization-json" } okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp-logging" } # detekt detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } # Coil coil-kt = { module = "io.coil-kt:coil", version.ref = "coil" } coil-kt-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } coil-kt-svg = { module = "io.coil-kt:coil-svg", version.ref = "coil" } # video exoplayer = { module = "com.google.android.exoplayer:exoplayer", version.ref = "exoplayer" } # leak leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" } # season yasha = { module = "com.github.ssseasonnn:Yasha", version.ref = "yasha" } bracer = { module = "com.github.ssseasonnn:Bracer", version.ref = "bracer" } clarity = { module = "com.github.ssseasonnn:ClarityPotion", version.ref = "clarity" } # test junit4 = { module = "junit:junit", version.ref = "junit" } kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } mockk = { module = "io.mockk:mockk", version.ref = "mockk" } mockk-agent = { module = "io.mockk:mockk-agent-jvm", version.ref = "mockk" } truth = { module = "com.google.truth:truth", version.ref = "truth" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } slf4j = { module = "org.slf4j:slf4j-nop", version.ref = "slf4j" } # android test core-testing = { module = "androidx.arch.core:core-testing", version.ref = "arch-test" } hilt-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" } androidx-junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "androidx-junit" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" } androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidx-test-rules" } espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso-core" } [bundles] season = [ "yasha", "bracer", "clarity" ] compose = [ "compose-activity", "compose-lifecycle", "compose-runtime", "compose-viewmodel", "compose-ui", "compose-material3", "compose-ui-tooling", "compose-ui-tooling-preview" ] kotlin = [ "kotlin-stdlib", "kotlin-coroutines", "kotlin-serialization-json" ] android = [ "appcompat", "core-ktx", "activity-ktx", "fragment-ktx", "constraintlayout", "material", "lifecycle-runtime-ktx", "lifecycle-viewmodel-ktx", "accompanist-systemuicontroller" ] retrofit = [ "retrofit-core", "retrofit-kotlin-serialization", "okhttp-logging" ] coil = [ "coil-kt", "coil-kt-compose", "coil-kt-svg" ] unit-test = [ "junit4", "kotlin-coroutines-test", "mockk", "mockk-agent", "truth", "turbine", "slf4j" ] android-test = [ "core-testing", "compose-ui-test", "compose-ui-test-junit", "compose-ui-test-manifest", "androidx-junit", "androidx-junit-ktx", "androidx-test-core", "androidx-test-runner", "androidx-test-rules", "espresso-core", "mockk", "mockk-agent", "truth", "slf4j" ] [plugins] application = { id = "com.android.application", version.ref = "gradle" } library = { id = "com.android.library", version.ref = "gradle" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } kotlin-kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } butterfly = { id = "io.github.ssseasonnn.butterfly", version.ref = "butterfly" } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Mon Feb 07 17:16:51 CST 2022 distributionBase=GRADLE_USER_HOME distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app"s APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh # # Copyright 2015 the original author or 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 UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$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 "$*" } die () { echo echo "$*" echo exit 1 } # 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 ;; 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" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin or MSYS, switch paths to Windows format before running java if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=`expr $i + 1` done case $i in 0) set -- ;; 1) set -- "$args0" ;; 2) set -- "$args0" "$args1" ;; 3) set -- "$args0" "$args1" "$args2" ;; 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @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 ================================================ FILE: jitpack.yml ================================================ jdk: - openjdk17 ================================================ FILE: plugin/.gitignore ================================================ /build ================================================ FILE: plugin/build.gradle.kts ================================================ plugins { id("kotlin") id("groovy") id("java-gradle-plugin") id("maven-publish") id("com.gradle.plugin-publish") version ("0.18.0") } dependencies { implementation(gradleApi()) implementation(localGroovy()) implementation(libs.android.gradle) implementation(libs.android.gradle.api) } group = "io.github.ssseasonnn" version = "1.0.1" gradlePlugin { plugins { create("butterflyPlugin") { id = "io.github.ssseasonnn.butterfly" displayName = "Butterfly plugin" description = "Butterfly plugin" implementationClass = "zlc.season.butterfly.plugin.ButterflyPlugin" } } } pluginBundle { website = "https://github.com/ssseasonnn/Butterfly" vcsUrl = "https://github.com/ssseasonnn/Butterfly.git" tags = listOf("Butterfly", "Plugin") } tasks.withType().all { duplicatesStrategy = DuplicatesStrategy.EXCLUDE } publishing { publications { create("gradle") { groupId = "io.github.ssseasonnn" artifactId = "io.github.ssseasonnn.butterfly" version = "1.0.1" from(components.getByName("java")) } } repositories { mavenLocal() } } ================================================ FILE: plugin/src/main/java/zlc/season/butterfly/plugin/ButterflyPlugin.kt ================================================ package zlc.season.butterfly.plugin import com.android.build.api.instrumentation.FramesComputationMode import com.android.build.api.instrumentation.InstrumentationScope import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.gradle.internal.tasks.factory.dependsOn import com.android.build.gradle.internal.utils.setDisallowChanges import org.gradle.api.DefaultTask import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.tasks.TaskAction import java.util.Locale class ButterflyPlugin : Plugin { override fun apply(project: Project) { project.pluginManager.withPlugin("com.android.application") { val androidComponentsExtension = project.extensions.getByType(AndroidComponentsExtension::class.java) androidComponentsExtension.onVariants { variant -> val task = project.tasks.register("clean${variant.name.cap()}ButterflyModule", CleanModuleMapTask::class.java) {} val cleanTask = project.tasks.named("clean") cleanTask.dependsOn(task) variant.instrumentation.transformClassesWith(ModuleClassVisitorFactory::class.java, InstrumentationScope.ALL) { it.invalidate.setDisallowChanges(System.currentTimeMillis()) } variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS) } } } private fun String.cap(): String { return replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } } } abstract class CleanModuleMapTask : DefaultTask() { @TaskAction fun action() { ModuleHolder.clearModule() } } ================================================ FILE: plugin/src/main/java/zlc/season/butterfly/plugin/ModuleClassVisitorFactory.kt ================================================ package zlc.season.butterfly.plugin import com.android.build.api.instrumentation.AsmClassVisitorFactory import com.android.build.api.instrumentation.ClassContext import com.android.build.api.instrumentation.ClassData import com.android.build.api.instrumentation.InstrumentationParameters import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.Optional import org.objectweb.asm.ClassVisitor import org.objectweb.asm.MethodVisitor import org.objectweb.asm.Opcodes.* import java.util.concurrent.ConcurrentHashMap /** * Save all module name */ object ModuleHolder { private val modulesMap = ConcurrentHashMap() fun printCurrentModules() { "Current module info ${modulesMap.values}".log() } fun addModule(moduleName: String) { modulesMap[moduleName] = moduleName "Found module $moduleName".log() } fun forEach(block: (String) -> Unit) { modulesMap.values.forEach { "Auto register module $it".log() block(it) } } fun clearModule() { "Clear module info...".log() modulesMap.clear() } } abstract class ModuleClassVisitorFactory : AsmClassVisitorFactory { interface ModuleInstrumentation : InstrumentationParameters { @get:Input @get:Optional val invalidate: Property } override fun createClassVisitor(classContext: ClassContext, nextClassVisitor: ClassVisitor): ClassVisitor { return ModuleClassVisitor(nextClassVisitor) } override fun isInstrumentable(classData: ClassData): Boolean { if (classData.interfaces.contains("zlc.season.butterfly.module.Module")) { ModuleHolder.addModule(classData.className) } return if (classData.superClasses.contains("android.app.Application")) { "Found application: ${classData.className}".log() true } else { false } } } class ModuleClassVisitor(nextClassVisitor: ClassVisitor) : ClassVisitor(ASM7, nextClassVisitor) { override fun visitMethod( access: Int, name: String, descriptor: String?, signature: String?, exceptions: Array? ): MethodVisitor? { var mv = super.visitMethod(access, name, descriptor, signature, exceptions) if (name == "onCreate") { mv = ModuleMethodVisitor(mv) } return mv } } class ModuleMethodVisitor(methodVisitor: MethodVisitor) : MethodVisitor(ASM7, methodVisitor) { override fun visitCode() { super.visitCode() ModuleHolder.forEach { mv.visitFieldInsn(GETSTATIC, "zlc/season/butterfly/ButterflyCore", "INSTANCE", "Lzlc/season/butterfly/ButterflyCore;") mv.visitLdcInsn(it) mv.visitMethodInsn(INVOKEVIRTUAL, "zlc/season/butterfly/ButterflyCore", "addModuleName", "(Ljava/lang/String;)V", false) } } } ================================================ FILE: plugin/src/main/java/zlc/season/butterfly/plugin/Utils.kt ================================================ package zlc.season.butterfly.plugin fun String.log() { println("[Butterfly Plugin] $this") } ================================================ FILE: plugin/src/main/resources/META-INF/gradle-plugins/io.github.ssseasonnn.butterfly.properties ================================================ implementation-class=zlc.season.butterfly.plugin.ButterflyPlugin ================================================ FILE: samples/app/.gitignore ================================================ /build ================================================ FILE: samples/app/build.gradle.kts ================================================ @file:Suppress("DSL_SCOPE_VIOLATION", "UnstableApiUsage") import zlc.season.buildlogic.base.androidApplication import zlc.season.buildlogic.base.enableCompose plugins { alias(libs.plugins.application) alias(libs.plugins.kotlin) alias(libs.plugins.ksp) alias(libs.plugins.butterfly) } androidApplication { namespace = "zlc.season.butterflydemo" viewBinding { enable = true } enableCompose() } ksp { arg("butterfly.log.enable", "true") } dependencies { implementation(project(":samples:modules:normal:home")) implementation(project(":samples:modules:normal:dashboard")) implementation(project(":samples:modules:normal:notifications")) implementation(project(":samples:modules:compose:compose_home")) implementation(project(":samples:modules:compose:compose_dashboard")) implementation(project(":samples:modules:compose:compose_notifications")) implementation(project(":samples:modules:base")) implementation(project(":samples:modules:feature1")) implementation(project(":samples:modules:feature2")) ksp(project(":compiler")) implementation(project(":butterfly")) implementation(project(":butterfly-compose")) implementation(libs.bundles.android) implementation(libs.bundles.compose) implementation(libs.bundles.kotlin) implementation(libs.bundles.season) debugImplementation(libs.leakcanary) } ================================================ FILE: samples/app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle.kts. # # 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 -keep public class zlc.season.butterfly.module.** -keep public class zlc.season.butterfly.annotation.** -keep public class zlc.season.butterfly.ButterflyCore {*;} -keep public class * extends zlc.season.butterfly.action.Action -keep @zlc.season.butterfly.annotation.Destination class * {*;} -keep @zlc.season.butterfly.annotation.Evade class * {*;} -keep @zlc.season.butterfly.annotation.EvadeImpl class * {*;} ================================================ FILE: samples/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: samples/app/src/main/java/zlc/season/butterflydemo/ComposeBottomNavigationActivity.kt ================================================ package zlc.season.butterflydemo import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import zlc.season.base.Destinations import zlc.season.butterfly.Butterfly import zlc.season.butterfly.annotation.Destination import zlc.season.butterflydemo.databinding.ActivityComposeBottomNavigationBinding @Destination(Destinations.COMPOSE_BOTTOM_NAVIGATION) class ComposeBottomNavigationActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ActivityComposeBottomNavigationBinding.inflate(layoutInflater) setContentView(binding.root) binding.navView.setOnItemSelectedListener { when (it.itemId) { R.id.navigation_home -> { Butterfly.of(this) .container(R.id.container) .group("") .navigate(Destinations.COMPOSE_HOME) } R.id.navigation_dashboard -> { Butterfly.of(this) .container(R.id.container) .group("") .navigate(Destinations.COMPOSE_DASHBOARD) } R.id.navigation_notifications -> { Butterfly.of(this) .container(R.id.container) .group("") .navigate(Destinations.COMPOSE_NOTIFICATION) } } true } binding.navView.selectedItemId = R.id.navigation_home } } ================================================ FILE: samples/app/src/main/java/zlc/season/butterflydemo/ComposeDemoActivity.kt ================================================ package zlc.season.butterflydemo import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import zlc.season.base.Destinations import zlc.season.butterfly.Butterfly import zlc.season.butterfly.annotation.Destination import zlc.season.butterflydemo.databinding.ActivityComposeDemoBinding @Destination(Destinations.COMPOSE_DEMO) class ComposeDemoActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ActivityComposeDemoBinding.inflate(layoutInflater) setContentView(binding.root) binding.btnStartA.setOnClickListener { Butterfly.of(this).asRoot().navigate(Destinations.COMPOSE_A) } binding.btnStartB.setOnClickListener { Butterfly.of(this).navigate(Destinations.COMPOSE_B) } binding.btnStartC.setOnClickListener { Butterfly.of(this).navigate(Destinations.COMPOSE_C) } } override fun onBackPressed() { // super.onBackPressed() Butterfly.of(this).popBack("result" to "Result from Activity") } } ================================================ FILE: samples/app/src/main/java/zlc/season/butterflydemo/DemoApplication.kt ================================================ package zlc.season.butterflydemo import android.app.Application //import zlc.season.compose.TestModule class DemoApplication : Application() { override fun onCreate() { super.onCreate() // ButterflyCore.addModule(TestModule()) } } ================================================ FILE: samples/app/src/main/java/zlc/season/butterflydemo/DestinationTestActivity.kt ================================================ package zlc.season.butterflydemo import android.annotation.SuppressLint import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import zlc.season.base.Destinations import zlc.season.bracer.params import zlc.season.butterfly.Butterfly import zlc.season.butterfly.annotation.Destination import zlc.season.butterflydemo.databinding.ActivityDestinationTestBinding @Destination(Destinations.DESTINATION_TEST) class DestinationTestActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ActivityDestinationTestBinding.inflate(layoutInflater) setContentView(binding.root) binding.startActivity.setOnClickListener { Butterfly.of(this) .params( "intValue" to 1, "booleanValue" to true, "stringValue" to "test value" ) .navigate(Destinations.TEST + "?a=1&b=2") } binding.startActivityForResult.setOnClickListener { Butterfly.of(this) .params( "intValue" to 1, "booleanValue" to true, "stringValue" to "test value" ) .navigate(Destinations.TEST_RESULT + "?a=1&b=2") { if (it.isSuccess) { val bundle = it.getOrDefault(Bundle.EMPTY) val result by bundle.params() binding.tvResult.text = result } } } binding.startAction.setOnClickListener { Butterfly.of(this).navigate(Destinations.ACTION + "?a=1&b=2") } binding.startFragment.setOnClickListener { Butterfly.of(this).navigate(Destinations.FRAGMENT) { if (it.isSuccess) { val bundle = it.getOrDefault(Bundle.EMPTY) val abc by bundle.params() binding.tvResult.text = abc } } } binding.startDialogFragment.setOnClickListener { Butterfly.of(this).navigate(Destinations.DIALOG_FRAGMbENT) } binding.startBottomSheetDialogFragment.setOnClickListener { Butterfly.of(this).navigate(Destinations.BOTTOM_SHEET_DIALOG_FRAGMENT) } } @SuppressLint("MissingSuperCall") override fun onBackPressed() { // super.onBackPressed() Butterfly.of(this).popBack() } } ================================================ FILE: samples/app/src/main/java/zlc/season/butterflydemo/EvadeTestActivity.kt ================================================ package zlc.season.butterflydemo import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import zlc.season.base.Destinations import zlc.season.butterfly.Butterfly import zlc.season.butterfly.annotation.Destination import zlc.season.butterflydemo.databinding.ActivityEvadeTestBinding @Destination(Destinations.EVADE_TEST) class EvadeTestActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ActivityEvadeTestBinding.inflate(layoutInflater) setContentView(binding.root) val home = Butterfly.evade() binding.showFragment.setOnClickListener { home.showHome(supportFragmentManager, R.id.container) } binding.testCompose.setOnClickListener { setContent { home.testCompose().composable?.invoke() } } } override fun onBackPressed() { val home = Butterfly.evade() if (home.isHomeShowing(supportFragmentManager)) { home.hideHome(supportFragmentManager) return } super.onBackPressed() } } ================================================ FILE: samples/app/src/main/java/zlc/season/butterflydemo/FragmentBottomNavigationActivity.kt ================================================ package zlc.season.butterflydemo import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import zlc.season.base.Destinations import zlc.season.butterfly.Butterfly import zlc.season.butterfly.annotation.Destination import zlc.season.butterflydemo.databinding.ActivityFragmentBottomNavigationBinding @Destination(Destinations.FRAGMENT_BOTTOM_NAVIGATION) class FragmentBottomNavigationActivity : AppCompatActivity() { private lateinit var binding: ActivityFragmentBottomNavigationBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityFragmentBottomNavigationBinding.inflate(layoutInflater) setContentView(binding.root) val groupId = "test_group" binding.navView.setOnItemSelectedListener { when (it.itemId) { R.id.navigation_home -> { Butterfly.of(this) .container(R.id.container) .group(groupId) .navigate(Destinations.HOME) } R.id.navigation_dashboard -> { Butterfly.of(this) .container(R.id.container) .group(groupId) .navigate(Destinations.DASHBOARD) } R.id.navigation_notifications -> { Butterfly.of(this) .container(R.id.container) .group(groupId) .navigate(Destinations.NOTIFICATION) } } true } binding.navView.selectedItemId = R.id.navigation_home } } ================================================ FILE: samples/app/src/main/java/zlc/season/butterflydemo/FragmentDemoActivity.kt ================================================ package zlc.season.butterflydemo import android.annotation.SuppressLint import android.os.Bundle import zlc.season.base.BaseActivity import zlc.season.base.Destinations import zlc.season.bracer.params import zlc.season.butterfly.Butterfly import zlc.season.butterfly.annotation.Destination import zlc.season.butterflydemo.databinding.ActivityFragmentDemoBinding @Destination(Destinations.FRAGMENT_DEMO) class FragmentDemoActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = ActivityFragmentDemoBinding.inflate(layoutInflater) setContentView(binding.root) binding.btnStartA.setOnClickListener { Butterfly.of(this) .asRoot() .navigate(Destinations.FRAGMENT_A) { if (it.isSuccess) { val bundle = it.getOrDefault(Bundle.EMPTY) val result by bundle.params() binding.tvResult.text = result } } } binding.btnStartB.setOnClickListener { Butterfly.of(this) .navigate(Destinations.FRAGMENT_B) { if (it.isSuccess) { val bundle = it.getOrDefault(Bundle.EMPTY) val result by bundle.params() binding.tvResult.text = result } } } binding.btnStartC.setOnClickListener { Butterfly.of(this) .navigate(Destinations.FRAGMENT_C) { if (it.isSuccess) { val bundle = it.getOrDefault(Bundle.EMPTY) val result by bundle.params() binding.tvResult.text = result } } } } @SuppressLint("MissingSuperCall") override fun onBackPressed() { Butterfly.of(this).popBack("result" to "Result from Activity") } } ================================================ FILE: samples/app/src/main/java/zlc/season/butterflydemo/Home.kt ================================================ package zlc.season.butterflydemo import androidx.fragment.app.FragmentManager import zlc.season.butterfly.annotation.Evade import zlc.season.butterfly.compose.ComposeDestination @Evade interface Home { fun isHomeShowing(fragmentManager: FragmentManager): Boolean fun showHome(fragmentManager: FragmentManager, container: Int) fun hideHome(fragmentManager: FragmentManager) fun testCompose(): ComposeDestination } ================================================ FILE: samples/app/src/main/java/zlc/season/butterflydemo/MainActivity.kt ================================================ package zlc.season.butterflydemo import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import zlc.season.base.Destinations import zlc.season.butterfly.Butterfly import zlc.season.butterflydemo.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { val binding by lazy { ActivityMainBinding.inflate(layoutInflater) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(binding.root) binding.startDestinationTest.setOnClickListener { Butterfly.of(this).navigate(Destinations.DESTINATION_TEST) } binding.btnFragmentTest.setOnClickListener { Butterfly.of(this).navigate(Destinations.FRAGMENT_DEMO) } binding.btnBottomNavigationTest.setOnClickListener { Butterfly.of(this).navigate(Destinations.FRAGMENT_BOTTOM_NAVIGATION) } binding.btnComposeTest.setOnClickListener { Butterfly.of(this).navigate(Destinations.COMPOSE_DEMO) } binding.btnComposeBottomNavigationTest.setOnClickListener { Butterfly.of(this).navigate(Destinations.COMPOSE_BOTTOM_NAVIGATION) } binding.startEvadeTest.setOnClickListener { Butterfly.of(this).navigate(Destinations.EVADE_TEST) } } } ================================================ FILE: samples/app/src/main/res/drawable/ic_dashboard_black_24dp.xml ================================================ ================================================ FILE: samples/app/src/main/res/drawable/ic_home_black_24dp.xml ================================================ ================================================ FILE: samples/app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: samples/app/src/main/res/drawable/ic_notifications_black_24dp.xml ================================================ ================================================ FILE: samples/app/src/main/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: samples/app/src/main/res/layout/activity_compose_bottom_navigation.xml ================================================ ================================================ FILE: samples/app/src/main/res/layout/activity_compose_demo.xml ================================================