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

[](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
================================================

[](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
================================================
================================================
FILE: samples/app/src/main/res/layout/activity_destination_test.xml
================================================
================================================
FILE: samples/app/src/main/res/layout/activity_evade_test.xml
================================================
================================================
FILE: samples/app/src/main/res/layout/activity_fragment_bottom_navigation.xml
================================================
================================================
FILE: samples/app/src/main/res/layout/activity_fragment_demo.xml
================================================
================================================
FILE: samples/app/src/main/res/layout/activity_main.xml
================================================
================================================
FILE: samples/app/src/main/res/menu/bottom_nav_menu.xml
================================================
================================================
FILE: samples/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
================================================
FILE: samples/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
================================================
FILE: samples/app/src/main/res/values/colors.xml
================================================
#FF000000
#FFFFFFFF
#FCFCFC
================================================
FILE: samples/app/src/main/res/values/dimens.xml
================================================
16dp
16dp
================================================
FILE: samples/app/src/main/res/values/strings.xml
================================================
ButterflyDemo
Home
Dashboard
Notifications
================================================
FILE: samples/app/src/main/res/values/themes.xml
================================================
================================================
FILE: samples/app/src/main/res/xml/network_security_config.xml
================================================
================================================
FILE: samples/modules/base/.gitignore
================================================
/build
================================================
FILE: samples/modules/base/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.serialization)
alias(libs.plugins.butterfly)
alias(libs.plugins.ksp)
}
androidLibrary {
namespace = "zlc.season.base"
}
dependencies {
ksp(project(":compiler"))
implementation(project(":butterfly"))
implementation(libs.bundles.android)
implementation(libs.bundles.kotlin)
}
================================================
FILE: samples/modules/base/consumer-rules.pro
================================================
================================================
FILE: samples/modules/base/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: samples/modules/base/src/main/AndroidManifest.xml
================================================
================================================
FILE: samples/modules/base/src/main/java/zlc/season/base/BaseActivity.kt
================================================
package zlc.season.base
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
open class BaseActivity : AppCompatActivity() {
open val name: String = "$this"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
println("$name onCreate: $savedInstanceState")
}
override fun onRestart() {
super.onRestart()
println("$name onRestart")
}
override fun onDestroy() {
super.onDestroy()
println("$name onDestroy")
}
}
================================================
FILE: samples/modules/base/src/main/java/zlc/season/base/BaseFragment.kt
================================================
package zlc.season.base
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
open class BaseFragment : Fragment() {
open val name: String
get() = "$this tag: ${this.tag}"
override fun onAttach(context: Context) {
super.onAttach(context)
println("$name onAttach")
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
println("$name onCreate: $savedInstanceState")
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
println("$name onCreateView: $savedInstanceState")
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onStart() {
super.onStart()
println("$name onStart")
}
override fun onResume() {
super.onResume()
println("$name onResume")
}
override fun onPause() {
super.onPause()
println("$name onPause")
}
override fun onStop() {
super.onStop()
println("$name onStop")
}
override fun onDestroyView() {
super.onDestroyView()
println("$name onDestroyView")
}
override fun onDestroy() {
super.onDestroy()
println("$name onDestroy")
}
override fun onDetach() {
super.onDetach()
println("$name onDetach")
}
}
================================================
FILE: samples/modules/base/src/main/java/zlc/season/base/Destinations.kt
================================================
package zlc.season.base
object Destinations {
private const val HOST = "butterfly_demo"
const val DESTINATION_TEST = "destination/test"
const val EVADE_TEST = "evade/test"
const val FRAGMENT_DEMO = "fragment/demo"
const val FRAGMENT_BOTTOM_NAVIGATION = "fragment/bottom_navigation"
const val COMPOSE_DEMO = "compose/demo"
const val COMPOSE_BOTTOM_NAVIGATION = "compose/bottom_navigation"
const val TEST = "${HOST}://test"
const val TEST_RESULT = "${HOST}://test_result"
const val FRAGMENT = "${HOST}://test_fragment"
const val DIALOG_FRAGMbENT = "${HOST}://test_dialog_fragment"
const val BOTTOM_SHEET_DIALOG_FRAGMENT = "${HOST}://test_bottom_sheet_dialog_fragment"
const val ACTION = "${HOST}://action"
const val FRAGMENT_A = "${HOST}://a_fragment"
const val FRAGMENT_B = "${HOST}://b_fragment"
const val FRAGMENT_C = "${HOST}://c_fragment"
const val COMPOSE_A = "${HOST}://a_screen"
const val COMPOSE_B = "${HOST}://b_screen"
const val COMPOSE_C = "${HOST}://c_screen"
const val DASHBOARD = "${HOST}://dashboard"
const val NOTIFICATION = "${HOST}://notification"
const val HOME = "${HOST}://home"
const val COMPOSE_DASHBOARD = "${HOST}://compose/dashboard"
const val COMPOSE_NOTIFICATION = "${HOST}://compose/notification"
const val COMPOSE_HOME = "${HOST}://compose/home"
}
================================================
FILE: samples/modules/compose/compose_dashboard/.gitignore
================================================
/build
================================================
FILE: samples/modules/compose/compose_dashboard/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.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.butterfly)
}
androidLibrary {
namespace = "zlc.season.compose.dashboard"
enableCompose()
}
dependencies {
ksp(project(":compiler"))
implementation(project(":butterfly"))
implementation(project(":butterfly-compose"))
implementation(project(":samples:modules:base"))
implementation(libs.bundles.android)
implementation(libs.bundles.kotlin)
implementation(libs.bundles.season)
implementation(libs.bundles.compose)
}
================================================
FILE: samples/modules/compose/compose_dashboard/consumer-rules.pro
================================================
================================================
FILE: samples/modules/compose/compose_dashboard/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: samples/modules/compose/compose_dashboard/src/main/AndroidManifest.xml
================================================
================================================
FILE: samples/modules/compose/compose_dashboard/src/main/java/zlc/season/compose/dashboard/DashboardScreen.kt
================================================
package zlc.season.compose.dashboard
import android.os.Bundle
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import zlc.season.base.Destinations
import zlc.season.butterfly.annotation.Destination
@Destination(Destinations.COMPOSE_DASHBOARD)
@Composable
fun DashboardScreen(test: Bundle = bundleOf(), viewModel: DashboardViewModel = DashboardViewModel()) {
val textFromViewModel = viewModel.text.collectAsState(initial = "")
Surface(modifier = Modifier.fillMaxSize()) {
Box {
Text(
modifier = Modifier.align(Alignment.Center),
text = "This is dashboard screen!"
)
Text(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 200.dp),
text = textFromViewModel.value
)
}
}
}
================================================
FILE: samples/modules/compose/compose_dashboard/src/main/java/zlc/season/compose/dashboard/DashboardViewModel.kt
================================================
package zlc.season.compose.dashboard
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
class DashboardViewModel : ViewModel() {
val text = MutableStateFlow("")
init {
viewModelScope.launch {
delay(1500)
text.value = "This is text from dashboard viewModel"
}
}
}
================================================
FILE: samples/modules/compose/compose_home/.gitignore
================================================
/build
================================================
FILE: samples/modules/compose/compose_home/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.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.butterfly)
}
androidLibrary {
namespace = "zlc.season.compose.home"
enableCompose()
}
dependencies {
ksp(project(":compiler"))
implementation(project(":butterfly"))
implementation(project(":butterfly-compose"))
implementation(project(":samples:modules:base"))
implementation(libs.bundles.android)
implementation(libs.bundles.kotlin)
implementation(libs.bundles.season)
implementation(libs.bundles.compose)
}
================================================
FILE: samples/modules/compose/compose_home/consumer-rules.pro
================================================
================================================
FILE: samples/modules/compose/compose_home/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: samples/modules/compose/compose_home/src/main/AndroidManifest.xml
================================================
================================================
FILE: samples/modules/compose/compose_home/src/main/java/zlc/season/compose/home/HomeScreen.kt
================================================
package zlc.season.compose.home
import android.os.Bundle
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import zlc.season.base.Destinations
import zlc.season.butterfly.annotation.Destination
@Destination(Destinations.COMPOSE_HOME)
@Composable
fun HomeScreen(test: Bundle) {
Surface(modifier = Modifier.fillMaxSize()) {
Box {
Text(
modifier = Modifier.align(Alignment.Center),
text = "This is home screen!"
)
}
}
}
================================================
FILE: samples/modules/compose/compose_notifications/.gitignore
================================================
/build
================================================
FILE: samples/modules/compose/compose_notifications/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.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.butterfly)
}
androidLibrary {
namespace = "zlc.season.compose.notifications"
enableCompose()
}
dependencies {
ksp(project(":compiler"))
implementation(project(":butterfly"))
implementation(project(":butterfly-compose"))
implementation(project(":samples:modules:base"))
implementation(libs.bundles.android)
implementation(libs.bundles.kotlin)
implementation(libs.bundles.season)
implementation(libs.bundles.compose)
}
================================================
FILE: samples/modules/compose/compose_notifications/consumer-rules.pro
================================================
================================================
FILE: samples/modules/compose/compose_notifications/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: samples/modules/compose/compose_notifications/src/main/AndroidManifest.xml
================================================
================================================
FILE: samples/modules/compose/compose_notifications/src/main/java/zlc/season/compose/notifications/NotificationsScreen.kt
================================================
package zlc.season.compose.notifications
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import zlc.season.base.Destinations
import zlc.season.butterfly.annotation.Destination
@Destination(Destinations.COMPOSE_NOTIFICATION)
@Composable
fun NotificationsScreen(viewModel: NotificationsViewModel) {
val textFromViewModel = viewModel.text.collectAsState(initial = "")
Surface(modifier = Modifier.fillMaxSize()) {
Box {
Text(
modifier = Modifier.align(Alignment.Center),
text = "This is notification screen!"
)
Text(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 200.dp),
text = textFromViewModel.value
)
}
}
}
================================================
FILE: samples/modules/compose/compose_notifications/src/main/java/zlc/season/compose/notifications/NotificationsViewModel.kt
================================================
package zlc.season.compose.notifications
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
class NotificationsViewModel : ViewModel() {
val text = MutableStateFlow("")
init {
viewModelScope.launch {
delay(1500)
text.value = "This is text from notification viewModel"
}
}
}
================================================
FILE: samples/modules/feature1/.gitignore
================================================
/build
================================================
FILE: samples/modules/feature1/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.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.butterfly)
}
androidLibrary {
namespace = "zlc.season.feature1"
viewBinding { enable = true }
}
dependencies {
ksp(project(":compiler"))
implementation(project(":butterfly"))
implementation(project(":samples:modules:base"))
implementation(libs.bundles.android)
implementation(libs.bundles.kotlin)
implementation(libs.bundles.season)
}
================================================
FILE: samples/modules/feature1/consumer-rules.pro
================================================
================================================
FILE: samples/modules/feature1/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: samples/modules/feature1/src/main/AndroidManifest.xml
================================================
================================================
FILE: samples/modules/feature1/src/main/java/zlc/season/feature1/AFragment.kt
================================================
package zlc.season.feature1
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import zlc.season.base.BaseFragment
import zlc.season.base.Destinations
import zlc.season.bracer.params
import zlc.season.butterfly.Butterfly
import zlc.season.butterfly.annotation.Destination
import zlc.season.feature1.databinding.FragmentCommonBinding
@Destination(Destinations.FRAGMENT_A)
class AFragment : BaseFragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return FragmentCommonBinding.inflate(inflater, container, false).also {
it.setup()
}.root
}
private fun FragmentCommonBinding.setup() {
root.setBackgroundResource(R.color.blue)
tvContent.text = "Fragment A ${hashCode()}"
btnBack.setOnClickListener {
Butterfly.of(requireContext()).popBack("result" to "Result from FragmentA")
}
btnDialog.setOnClickListener {
Butterfly.of(requireContext())
.navigate(Destinations.BOTTOM_SHEET_DIALOG_FRAGMENT) {
if (it.isSuccess) {
val bundle = it.getOrDefault(Bundle.EMPTY)
val result by bundle.params()
tvResult.text = result
}
}
}
btnNextA.setOnClickListener {
Butterfly.of(requireContext())
.run {
if (cbClearTop.isChecked) {
clearTop()
} else if (cbSingleTop.isChecked) {
singleTop()
} else {
this
}
}
.navigate(Destinations.FRAGMENT_A) {
if (it.isSuccess) {
val bundle = it.getOrDefault(Bundle.EMPTY)
val result by bundle.params()
tvResult.text = result
}
}
}
btnNextB.setOnClickListener {
Butterfly.of(requireContext())
.run {
if (cbClearTop.isChecked) {
clearTop()
} else if (cbSingleTop.isChecked) {
singleTop()
} else {
this
}
}
.navigate(Destinations.FRAGMENT_B) {
if (it.isSuccess) {
val bundle = it.getOrDefault(Bundle.EMPTY)
val result by bundle.params()
tvResult.text = result
}
}
}
btnNextC.setOnClickListener {
Butterfly.of(requireContext())
.run {
if (cbClearTop.isChecked) {
clearTop()
} else if (cbSingleTop.isChecked) {
singleTop()
} else {
this
}
}
.navigate(Destinations.FRAGMENT_C) {
if (it.isSuccess) {
val bundle = it.getOrDefault(Bundle.EMPTY)
val result by bundle.params()
tvResult.text = result
}
}
}
}
}
================================================
FILE: samples/modules/feature1/src/main/java/zlc/season/feature1/BFragment.kt
================================================
package zlc.season.feature1
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import zlc.season.base.BaseFragment
import zlc.season.base.Destinations
import zlc.season.bracer.params
import zlc.season.butterfly.Butterfly
import zlc.season.butterfly.annotation.Destination
import zlc.season.feature1.databinding.FragmentCommonBinding
@Destination(Destinations.FRAGMENT_B)
class BFragment : BaseFragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return FragmentCommonBinding.inflate(inflater, container, false).also {
it.setup()
}.root
}
private fun FragmentCommonBinding.setup() {
root.setBackgroundResource(R.color.green)
tvContent.text = "Fragment B ${hashCode()}"
btnBack.setOnClickListener {
Butterfly.of(requireContext()).popBack("result" to "Result from FragmentB")
}
btnDialog.setOnClickListener {
Butterfly.of(requireContext())
.navigate(Destinations.BOTTOM_SHEET_DIALOG_FRAGMENT) {
if (it.isSuccess) {
val bundle = it.getOrDefault(Bundle.EMPTY)
val result by bundle.params()
tvResult.text = result
}
}
}
btnNextA.setOnClickListener {
Butterfly.of(requireContext())
.run {
if (cbClearTop.isChecked) {
clearTop()
} else if (cbSingleTop.isChecked) {
singleTop()
} else {
this
}
}
.navigate(Destinations.FRAGMENT_A) {
if (it.isSuccess) {
val bundle = it.getOrDefault(Bundle.EMPTY)
val result by bundle.params()
tvResult.text = result
}
}
}
btnNextB.setOnClickListener {
Butterfly.of(requireContext())
.run {
if (cbClearTop.isChecked) {
clearTop()
} else if (cbSingleTop.isChecked) {
singleTop()
} else {
this
}
}
.navigate(Destinations.FRAGMENT_B) {
if (it.isSuccess) {
val bundle = it.getOrDefault(Bundle.EMPTY)
val result by bundle.params()
tvResult.text = result
}
}
}
btnNextC.setOnClickListener {
Butterfly.of(requireContext())
.run {
if (cbClearTop.isChecked) {
clearTop()
} else if (cbSingleTop.isChecked) {
singleTop()
} else {
this
}
}
.navigate(Destinations.FRAGMENT_C) {
if (it.isSuccess) {
val bundle = it.getOrDefault(Bundle.EMPTY)
val result by bundle.params()
tvResult.text = result
}
}
}
}
}
================================================
FILE: samples/modules/feature1/src/main/java/zlc/season/feature1/CFragment.kt
================================================
package zlc.season.feature1
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import zlc.season.base.BaseFragment
import zlc.season.base.Destinations
import zlc.season.bracer.params
import zlc.season.butterfly.Butterfly
import zlc.season.butterfly.annotation.Destination
import zlc.season.feature1.databinding.FragmentCommonBinding
@Destination(Destinations.FRAGMENT_C)
class CFragment : BaseFragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return FragmentCommonBinding.inflate(inflater, container, false).also {
it.setup()
}.root
}
private fun FragmentCommonBinding.setup() {
root.setBackgroundResource(R.color.yellow)
tvContent.text = "Fragment C ${hashCode()}"
btnBack.setOnClickListener {
Butterfly.of(requireContext()).popBack("result" to "Result from FragmentC")
}
btnDialog.setOnClickListener {
Butterfly.of(requireContext())
.navigate(Destinations.BOTTOM_SHEET_DIALOG_FRAGMENT) {
if (it.isSuccess) {
val bundle = it.getOrDefault(Bundle.EMPTY)
val result by bundle.params()
tvResult.text = result
}
}
}
btnNextA.setOnClickListener {
Butterfly.of(requireContext())
.run {
if (cbClearTop.isChecked) {
clearTop()
} else if (cbSingleTop.isChecked) {
singleTop()
} else {
this
}
}
.navigate(Destinations.FRAGMENT_A) {
if (it.isSuccess) {
val bundle = it.getOrDefault(Bundle.EMPTY)
val result by bundle.params()
tvResult.text = result
}
}
}
btnNextB.setOnClickListener {
Butterfly.of(requireContext())
.run {
if (cbClearTop.isChecked) {
clearTop()
} else if (cbSingleTop.isChecked) {
singleTop()
} else {
this
}
}
.navigate(Destinations.FRAGMENT_B) {
if (it.isSuccess) {
val bundle = it.getOrDefault(Bundle.EMPTY)
val result by bundle.params()
tvResult.text = result
}
}
}
btnNextC.setOnClickListener {
Butterfly.of(requireContext())
.run {
if (cbClearTop.isChecked) {
clearTop()
} else if (cbSingleTop.isChecked) {
singleTop()
} else {
this
}
}
.navigate(Destinations.FRAGMENT_C) {
if (it.isSuccess) {
val bundle = it.getOrDefault(Bundle.EMPTY)
val result by bundle.params()
tvResult.text = result
}
}
}
}
}
================================================
FILE: samples/modules/feature1/src/main/java/zlc/season/feature1/TestAction.kt
================================================
package zlc.season.feature1
import android.content.Context
import android.os.Bundle
import android.widget.Toast
import zlc.season.base.Destinations
import zlc.season.butterfly.action.Action
import zlc.season.butterfly.annotation.Destination
@Destination(Destinations.ACTION)
class TestAction : Action {
override fun doAction(context: Context, route: String, data: Bundle) {
Toast.makeText(context, "This is an Action", Toast.LENGTH_SHORT).show()
}
}
================================================
FILE: samples/modules/feature1/src/main/java/zlc/season/feature1/TestActivity.kt
================================================
package zlc.season.feature1
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.feature1.databinding.ActivityTestBinding
@Destination(Destinations.TEST)
class TestActivity : AppCompatActivity() {
val intValue by params()
val booleanValue by params()
val stringValue by params()
val a by params()
val b by params()
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityTestBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.tvParam.text = """
intValue = $intValue
booleanValue = $booleanValue
stringValue = $stringValue
a = $a
b = $b
""".trimIndent()
binding.btnFinish.setOnClickListener {
Butterfly.of(this).popBack("result" to "asb")
}
}
}
================================================
FILE: samples/modules/feature1/src/main/java/zlc/season/feature1/TestBottomSheetDialogFragment.kt
================================================
package zlc.season.feature1
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import zlc.season.base.Destinations
import zlc.season.base.Destinations.BOTTOM_SHEET_DIALOG_FRAGMENT
import zlc.season.butterfly.Butterfly
import zlc.season.butterfly.annotation.Destination
import zlc.season.feature1.databinding.DialogTestBinding
@Destination(BOTTOM_SHEET_DIALOG_FRAGMENT)
class TestBottomSheetDialogFragment : BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return DialogTestBinding.inflate(inflater, container, false).also {
it.btnNext.setOnClickListener {
Butterfly.of(requireContext()).navigate(Destinations.FRAGMENT)
Butterfly.of(requireContext()).popBack()
}
it.btnBack.setOnClickListener {
Butterfly.of(requireContext()).popBack("result" to "Result from BottomSheetDialog!")
}
}.root
}
}
================================================
FILE: samples/modules/feature1/src/main/java/zlc/season/feature1/TestDialogFragment.kt
================================================
package zlc.season.feature1
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import zlc.season.base.Destinations
import zlc.season.base.Destinations.DIALOG_FRAGMbENT
import zlc.season.butterfly.Butterfly
import zlc.season.butterfly.annotation.Destination
import zlc.season.feature1.databinding.DialogTestBinding
@Destination(DIALOG_FRAGMbENT)
class TestDialogFragment : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, android.R.style.ThemeOverlay_Material_Dialog_Alert)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return DialogTestBinding.inflate(inflater, container, false).also {
it.btnNext.setOnClickListener {
Butterfly.of(requireContext()).navigate(Destinations.FRAGMENT)
dismiss()
}
it.btnBack.setOnClickListener {
Butterfly.of(requireContext()).popBack("result" to "Result from dialog!")
}
}.root
}
}
================================================
FILE: samples/modules/feature1/src/main/java/zlc/season/feature1/TestFragment.kt
================================================
package zlc.season.feature1
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import zlc.season.base.Destinations.FRAGMENT
import zlc.season.bracer.params
import zlc.season.butterfly.Butterfly
import zlc.season.butterfly.annotation.Destination
import zlc.season.feature1.databinding.FragmentTestBinding
@Destination(FRAGMENT)
class TestFragment : Fragment() {
val number by params()
var binding: FragmentTestBinding? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return FragmentTestBinding.inflate(inflater, container, false).also { binding = it }.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.apply {
tvContent.text = "This is Fragment $number"
btnSetResult.setOnClickListener {
Butterfly.of(requireContext()).popBack("abc" to "123")
}
}
}
}
================================================
FILE: samples/modules/feature1/src/main/java/zlc/season/feature1/TestResultActivity.kt
================================================
package zlc.season.feature1
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import zlc.season.base.Destinations
import zlc.season.bracer.params
import zlc.season.butterfly.annotation.Destination
import zlc.season.feature1.databinding.ActivityTestResultBinding
@Destination(Destinations.TEST_RESULT)
class TestResultActivity : AppCompatActivity() {
val intValue by params()
val booleanValue by params()
val stringValue by params()
val a by params()
val b by params()
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityTestResultBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.tvParam.text = """
intValue = $intValue
booleanValue = $booleanValue
stringValue = $stringValue
a = $a
b = $b
""".trimIndent()
binding.btnFinish.setOnClickListener {
setResult(Activity.RESULT_OK, Intent().apply {
putExtras(
bundleOf(
"result" to "This is result"
)
)
})
finish()
// or use
// Butterfly.retreat("result" to "aaa")
}
}
}
================================================
FILE: samples/modules/feature1/src/main/res/layout/activity_test.xml
================================================
================================================
FILE: samples/modules/feature1/src/main/res/layout/activity_test_result.xml
================================================
================================================
FILE: samples/modules/feature1/src/main/res/layout/dialog_test.xml
================================================
================================================
FILE: samples/modules/feature1/src/main/res/layout/fragment_common.xml
================================================
================================================
FILE: samples/modules/feature1/src/main/res/layout/fragment_test.xml
================================================
================================================
FILE: samples/modules/feature1/src/main/res/values/colors.xml
================================================
#2196F3
#00BCD4
#FF9800
#FFBB86FC
#FF6200EE
#FF3700B3
================================================
FILE: samples/modules/feature1/src/main/res/values/strings.xml
================================================
================================================
FILE: samples/modules/feature2/.gitignore
================================================
/build
================================================
FILE: samples/modules/feature2/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.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.butterfly)
}
androidLibrary {
namespace = "zlc.season.feature2"
enableCompose()
}
dependencies {
ksp(project(":compiler"))
implementation(project(":butterfly"))
implementation(project(":butterfly-compose"))
implementation(project(":samples:modules:base"))
implementation(libs.bundles.android)
implementation(libs.bundles.kotlin)
implementation(libs.bundles.compose)
implementation(libs.bundles.season)
}
================================================
FILE: samples/modules/feature2/consumer-rules.pro
================================================
================================================
FILE: samples/modules/feature2/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: samples/modules/feature2/src/main/AndroidManifest.xml
================================================
================================================
FILE: samples/modules/feature2/src/main/java/zlc/season/bar/Colors.kt
================================================
package zlc.season.bar
import androidx.compose.ui.graphics.Color
object Colors {
val YELLOW = Color(0xFFFF5722)
val PURPLE = Color(0xFF673AB7)
val GREEN = Color(0xFF009688)
}
================================================
FILE: samples/modules/feature2/src/main/java/zlc/season/bar/ComposeScreenA.kt
================================================
package zlc.season.bar
import android.os.Bundle
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.os.bundleOf
import androidx.lifecycle.viewmodel.compose.viewModel
import zlc.season.base.Destinations
import zlc.season.bracer.params
import zlc.season.butterfly.Butterfly
import zlc.season.butterfly.annotation.Destination
@Destination(Destinations.COMPOSE_A)
@Composable
fun ComposeScreenA(bundle: Bundle = bundleOf()) {
val id by bundle.params()
val viewModel = viewModel()
val textFromViewModel = viewModel.text.collectAsState(initial = "")
val ctx = LocalContext.current
Surface(
modifier = Modifier
.fillMaxSize(),
color = Colors.YELLOW
) {
Box {
Column(
modifier = Modifier
.wrapContentSize()
.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "This is ComposeScreen A ${id}")
Text(text = textFromViewModel.value)
Button(onClick = {
Butterfly.of(ctx).popBack()
}) {
Text(text = "Back")
}
Button(onClick = {
Butterfly.of(ctx).navigate(Destinations.BOTTOM_SHEET_DIALOG_FRAGMENT)
}) {
Text(text = "Show Dialog")
}
var singleTop by remember { mutableStateOf(false) }
var clearTop by remember { mutableStateOf(false) }
Button(onClick = {
Butterfly.of(ctx)
.params(bundleOf("id" to id + 1))
.run {
if (clearTop) {
clearTop()
} else if (singleTop) {
singleTop()
} else {
this
}
}
.navigate(Destinations.COMPOSE_A)
}) {
Text(text = "Next To A")
}
Button(onClick = {
Butterfly.of(ctx)
.run {
if (clearTop) {
clearTop()
} else if (singleTop) {
singleTop()
} else {
this
}
}
.navigate(Destinations.COMPOSE_B)
}) {
Text(text = "Next To B")
}
Button(onClick = {
Butterfly.of(ctx)
.run {
if (clearTop) {
clearTop()
} else if (singleTop) {
singleTop()
} else {
this
}
}
.navigate(Destinations.COMPOSE_C)
}) {
Text(text = "Next to C")
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = singleTop, onCheckedChange = { singleTop = it })
Text(text = "SingleTop")
Checkbox(checked = clearTop, onCheckedChange = { clearTop = it })
Text(text = "ClearTop")
}
Text(text = "result")
}
}
}
}
================================================
FILE: samples/modules/feature2/src/main/java/zlc/season/bar/ComposeScreenB.kt
================================================
package zlc.season.bar
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import zlc.season.base.Destinations
import zlc.season.butterfly.Butterfly
import zlc.season.butterfly.annotation.Destination
@Destination(Destinations.COMPOSE_B)
@Composable
fun ComposeScreenB() {
val ctx = LocalContext.current
val viewModel = viewModel()
val textFromViewModel = viewModel.text.collectAsState(initial = "")
Surface(modifier = Modifier.fillMaxSize(), color = Colors.PURPLE) {
Box {
Column(
modifier = Modifier
.wrapContentSize()
.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "This is ComposeScreen B")
Text(text = textFromViewModel.value)
Button(onClick = {
Butterfly.of(ctx).popBack()
}) {
Text(text = "Back")
}
Button(onClick = {
Butterfly.of(ctx).navigate(Destinations.BOTTOM_SHEET_DIALOG_FRAGMENT)
}) {
Text(text = "Show Dialog")
}
var singleTop by remember { mutableStateOf(false) }
var clearTop by remember { mutableStateOf(false) }
Button(onClick = {
Butterfly.of(ctx)
.run {
if (clearTop) {
clearTop()
} else if (singleTop) {
singleTop()
} else {
this
}
}
.navigate(Destinations.COMPOSE_A)
}) {
Text(text = "Next To A")
}
Button(onClick = {
Butterfly.of(ctx)
.run {
if (clearTop) {
clearTop()
} else if (singleTop) {
singleTop()
} else {
this
}
}
.navigate(Destinations.COMPOSE_B)
}) {
Text(text = "Next To B")
}
Button(onClick = {
Butterfly.of(ctx)
.run {
if (clearTop) {
clearTop()
} else if (singleTop) {
singleTop()
} else {
this
}
}
.navigate(Destinations.COMPOSE_C)
}) {
Text(text = "Next to C")
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = singleTop, onCheckedChange = { singleTop = it })
Text(text = "SingleTop")
Checkbox(checked = clearTop, onCheckedChange = { clearTop = it })
Text(text = "ClearTop")
}
Text(text = "result")
}
}
}
}
================================================
FILE: samples/modules/feature2/src/main/java/zlc/season/bar/ComposeScreenC.kt
================================================
package zlc.season.bar
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import zlc.season.base.Destinations
import zlc.season.butterfly.Butterfly
import zlc.season.butterfly.annotation.Destination
@Destination(Destinations.COMPOSE_C)
@Composable
fun ComposeScreenC() {
val viewModel = viewModel()
val textFromViewModel = viewModel.text.collectAsState(initial = "")
val ctx = LocalContext.current
Surface(modifier = Modifier.fillMaxSize(), color = Colors.GREEN) {
Box {
Column(
modifier = Modifier
.wrapContentSize()
.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "This is ComposeScreen C")
Text(text = textFromViewModel.value)
Button(onClick = {
Butterfly.of(ctx).popBack()
}) {
Text(text = "Back")
}
Button(onClick = {
Butterfly.of(ctx).navigate(Destinations.BOTTOM_SHEET_DIALOG_FRAGMENT)
}) {
Text(text = "Show Dialog")
}
var singleTop by remember { mutableStateOf(false) }
var clearTop by remember { mutableStateOf(false) }
Button(onClick = {
Butterfly.of(ctx)
.run {
if (clearTop) {
clearTop()
} else if (singleTop) {
singleTop()
} else {
this
}
}
.navigate(Destinations.COMPOSE_A)
}) {
Text(text = "Next To A")
}
Button(onClick = {
Butterfly.of(ctx)
.run {
if (clearTop) {
clearTop()
} else if (singleTop) {
singleTop()
} else {
this
}
}
.navigate(Destinations.COMPOSE_B)
}) {
Text(text = "Next To B")
}
Button(onClick = {
Butterfly.of(ctx)
.run {
if (clearTop) {
clearTop()
} else if (singleTop) {
singleTop()
} else {
this
}
}
.navigate(Destinations.COMPOSE_C)
}) {
Text(text = "Next to C")
}
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = singleTop, onCheckedChange = { singleTop = it })
Text(text = "SingleTop")
Checkbox(checked = clearTop, onCheckedChange = { clearTop = it })
Text(text = "ClearTop")
}
Text(text = "result")
}
}
}
}
================================================
FILE: samples/modules/feature2/src/main/java/zlc/season/bar/ScreenViewModels.kt
================================================
package zlc.season.bar
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
class AScreenViewModel : ViewModel() {
val text = MutableStateFlow("")
var count = 0
init {
viewModelScope.launch {
while (isActive) {
delay(1000)
text.value = "This is count: ${count++} from A screen viewModel"
}
}
}
override fun onCleared() {
super.onCleared()
println("A screen view model is cleared!")
}
}
class BScreenViewModel : ViewModel() {
val text = MutableStateFlow("")
var count = 0
init {
viewModelScope.launch {
while (isActive) {
delay(1000)
text.value = "This is count: ${count++} from B screen viewModel"
}
}
}
override fun onCleared() {
super.onCleared()
println("B screen view model is cleared!")
}
}
class CScreenViewModel : ViewModel() {
val text = MutableStateFlow("")
var count = 0
init {
viewModelScope.launch {
while (isActive) {
delay(1000)
text.value = "This is count: ${count++} from C screen viewModel"
}
}
}
override fun onCleared() {
super.onCleared()
println("C screen view model is cleared!")
}
}
================================================
FILE: samples/modules/normal/dashboard/.gitignore
================================================
/build
================================================
FILE: samples/modules/normal/dashboard/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.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.butterfly)
}
androidLibrary {
namespace = "zlc.season.dashboard"
viewBinding { enable = true }
}
dependencies {
ksp(project(":compiler"))
implementation(project(":butterfly"))
implementation(project(":samples:modules:base"))
implementation(libs.bundles.android)
implementation(libs.bundles.kotlin)
implementation(libs.bundles.season)
}
================================================
FILE: samples/modules/normal/dashboard/consumer-rules.pro
================================================
================================================
FILE: samples/modules/normal/dashboard/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: samples/modules/normal/dashboard/src/main/AndroidManifest.xml
================================================
================================================
FILE: samples/modules/normal/dashboard/src/main/java/zlc/season/dashboard/DashboardFragment.kt
================================================
package zlc.season.dashboard
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import zlc.season.base.Destinations
import zlc.season.butterfly.annotation.Destination
import zlc.season.dashboard.databinding.FragmentDashboardBinding
@Destination(Destinations.DASHBOARD)
class DashboardFragment : Fragment() {
private var _binding: FragmentDashboardBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val dashboardViewModel by viewModels()
_binding = FragmentDashboardBinding.inflate(inflater, container, false)
val root: View = binding.root
val textView: TextView = binding.textDashboard
dashboardViewModel.text.observe(viewLifecycleOwner) {
textView.text = it
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
================================================
FILE: samples/modules/normal/dashboard/src/main/java/zlc/season/dashboard/DashboardViewModel.kt
================================================
package zlc.season.dashboard
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class DashboardViewModel : ViewModel() {
private val _text = MutableLiveData().apply {
value = "This is dashboard Fragment"
}
val text: LiveData = _text
}
================================================
FILE: samples/modules/normal/dashboard/src/main/res/layout/fragment_dashboard.xml
================================================
================================================
FILE: samples/modules/normal/home/.gitignore
================================================
/build
================================================
FILE: samples/modules/normal/home/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.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.butterfly)
}
androidLibrary {
namespace = "zlc.season.home"
viewBinding { enable = true }
}
dependencies {
ksp(project(":compiler"))
implementation(project(":butterfly"))
implementation(project(":butterfly-compose"))
implementation(project(":samples:modules:base"))
implementation(libs.bundles.android)
implementation(libs.bundles.kotlin)
implementation(libs.bundles.season)
implementation(libs.bundles.coil)
}
================================================
FILE: samples/modules/normal/home/consumer-rules.pro
================================================
================================================
FILE: samples/modules/normal/home/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: samples/modules/normal/home/src/main/AndroidManifest.xml
================================================
================================================
FILE: samples/modules/normal/home/src/main/java/zlc/season/home/HomeFragment.kt
================================================
package zlc.season.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import zlc.season.base.Destinations
import zlc.season.butterfly.annotation.Destination
import zlc.season.home.databinding.FragmentHomeBinding
@Destination(Destinations.HOME)
class HomeFragment : Fragment() {
var binding: FragmentHomeBinding? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentHomeBinding.inflate(inflater, container, false)
val root: View = binding!!.root
val textView: TextView = binding!!.textHome
textView.text = "This is HomeFragment"
return root
}
}
================================================
FILE: samples/modules/normal/home/src/main/java/zlc/season/home/HomeImpl.kt
================================================
package zlc.season.home
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.fragment.app.FragmentManager
import zlc.season.butterfly.annotation.EvadeImpl
import zlc.season.butterfly.compose.ComposeDestination
@EvadeImpl
class HomeImpl {
val TAG = "home_tag"
var homeFragment: HomeFragment? = null
fun showHome(fragmentManager: FragmentManager, container: Int) {
if (homeFragment == null) {
homeFragment = HomeFragment()
}
homeFragment?.let {
fragmentManager.beginTransaction()
.replace(container, it, TAG)
.commit()
}
}
fun isHomeShowing(fragmentManager: FragmentManager): Boolean {
val find = fragmentManager.findFragmentByTag(TAG)
return find != null
}
fun hideHome(fragmentManager: FragmentManager) {
homeFragment?.let {
fragmentManager.beginTransaction()
.remove(it)
.commit()
}
}
fun testCompose(): ComposeDestination {
return ComposeDestination(composable = @Composable {
BasicText("test compose")
})
}
}
================================================
FILE: samples/modules/normal/home/src/main/res/layout/fragment_home.xml
================================================
================================================
FILE: samples/modules/normal/notifications/.gitignore
================================================
/build
================================================
FILE: samples/modules/normal/notifications/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.serialization)
alias(libs.plugins.ksp)
alias(libs.plugins.butterfly)
}
androidLibrary {
namespace = "zlc.season.notifications"
viewBinding { enable = true }
}
dependencies {
ksp(project(":compiler"))
implementation(project(":butterfly"))
implementation(project(":samples:modules:base"))
implementation(libs.bundles.android)
implementation(libs.bundles.kotlin)
implementation(libs.bundles.season)
}
================================================
FILE: samples/modules/normal/notifications/consumer-rules.pro
================================================
================================================
FILE: samples/modules/normal/notifications/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: samples/modules/normal/notifications/src/main/AndroidManifest.xml
================================================
================================================
FILE: samples/modules/normal/notifications/src/main/java/zlc/season/notifications/NotificationsFragment.kt
================================================
package zlc.season.notifications
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import zlc.season.base.Destinations
import zlc.season.butterfly.annotation.Destination
import zlc.season.notifications.databinding.FragmentNotificationsBinding
@Destination(Destinations.NOTIFICATION)
class NotificationsFragment : Fragment() {
private var _binding: FragmentNotificationsBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val notificationsViewModel by viewModels()
_binding = FragmentNotificationsBinding.inflate(inflater, container, false)
val root: View = binding.root
val textView: TextView = binding.textNotifications
notificationsViewModel.text.observe(viewLifecycleOwner) {
textView.text = it
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
================================================
FILE: samples/modules/normal/notifications/src/main/java/zlc/season/notifications/NotificationsViewModel.kt
================================================
package zlc.season.notifications
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class NotificationsViewModel : ViewModel() {
private val _text = MutableLiveData().apply {
value = "This is notifications Fragment"
}
val text: LiveData = _text
}
================================================
FILE: samples/modules/normal/notifications/src/main/res/layout/fragment_notifications.xml
================================================
================================================
FILE: settings.gradle.kts
================================================
@file:Suppress("DSL_SCOPE_VIOLATION", "UnstableApiUsage")
pluginManagement {
includeBuild("buildLogic")
repositories {
mavenLocal()
google()
mavenCentral()
gradlePluginPortal()
maven("https://jitpack.io")
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
repositories {
mavenLocal()
google()
mavenCentral()
gradlePluginPortal()
maven("https://jitpack.io")
}
}
rootProject.name = "ButterflyDemo"
include(":annotation")
include(":butterfly")
include(":butterfly-compose")
include(":compiler")
include(":plugin")
include(":samples")
include(":samples:app")
include(":samples:modules")
include(":samples:modules:base")
include(":samples:modules:feature2")
include(":samples:modules:feature1")
include(":samples:modules:compose")
include(":samples:modules:compose:compose_home")
include(":samples:modules:compose:compose_dashboard")
include(":samples:modules:compose:compose_notifications")
include(":samples:modules:normal")
include(":samples:modules:normal:home")
include(":samples:modules:normal:dashboard")
include(":samples:modules:normal:notifications")