├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── apply_signing.sh
├── dependabot.yml
└── workflows
│ ├── build.yml
│ └── static-validations.yml
├── .gitignore
├── .idea
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
└── inspectionProfiles
│ ├── ktlint.xml
│ └── profiles_settings.xml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── build.gradle.kts
├── docs
├── _config.yml
├── component
│ ├── koin.md
│ ├── molecule.md
│ ├── navigation.md
│ └── view_model.md
├── index.md
├── sample.md
└── setup.md
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── media
├── greeting_app.gif
├── note_app.webp
└── note_app_android.webp
├── precompose-molecule
├── build.gradle.kts
└── src
│ ├── androidMain
│ └── kotlin
│ │ └── moe
│ │ └── tlaster
│ │ └── precompose
│ │ └── molecule
│ │ └── ProvidePlatformDispatcher.kt
│ ├── commonMain
│ └── kotlin
│ │ └── moe
│ │ └── tlaster
│ │ └── precompose
│ │ └── molecule
│ │ └── Molecule.kt
│ ├── iosMain
│ └── kotlin
│ │ └── moe
│ │ └── tlaster
│ │ └── precompose
│ │ └── molecule
│ │ └── ProvidePlatformDispatcher.kt
│ ├── jsMain
│ └── kotlin
│ │ └── moe
│ │ └── tlaster
│ │ └── precompose
│ │ └── molecule
│ │ └── ProvidePlatformDispatcher.kt
│ ├── jvmMain
│ └── kotlin
│ │ └── moe
│ │ └── tlaster
│ │ └── precompose
│ │ └── molecule
│ │ └── ProvidePlatformDispatcher.kt
│ ├── macosMain
│ └── kotlin
│ │ └── moe
│ │ └── tlaster
│ │ └── precompose
│ │ └── molecule
│ │ └── ProvidePlatformDispatcher.kt
│ └── wasmJsMain
│ └── kotlin
│ └── moe
│ └── tlaster
│ └── precompose
│ └── molecule
│ └── ProvidePlatformDispatcher.kt
├── precompose
├── build.gradle.kts
└── src
│ ├── androidMain
│ └── kotlin
│ │ └── moe
│ │ └── tlaster
│ │ └── precompose
│ │ ├── PreComposeApp.android.kt
│ │ └── reflect
│ │ └── KClass.android.kt
│ ├── commonMain
│ └── kotlin
│ │ └── moe
│ │ └── tlaster
│ │ └── precompose
│ │ ├── PreComposeApp.kt
│ │ ├── navigation
│ │ ├── BackHandler.kt
│ │ ├── BackStackEntry.kt
│ │ ├── BackStackManager.kt
│ │ ├── NavControllerViewModel.kt
│ │ ├── NavHost.kt
│ │ ├── NavOptions.kt
│ │ ├── Navigator.kt
│ │ ├── PopUpTo.kt
│ │ ├── QueryString.kt
│ │ ├── RouteBuilder.kt
│ │ ├── RouteGraph.kt
│ │ ├── RouteMatch.kt
│ │ ├── RouteMatchResult.kt
│ │ ├── RouteParser.kt
│ │ ├── SwipeProperties.kt
│ │ ├── UiClosable.kt
│ │ ├── ViewModelStoreProvider.kt
│ │ ├── route
│ │ │ ├── ComposeRoute.kt
│ │ │ ├── FloatingRoute.kt
│ │ │ ├── GroupRoute.kt
│ │ │ ├── Route.kt
│ │ │ └── SceneRoute.kt
│ │ └── transition
│ │ │ └── NavTransition.kt
│ │ ├── reflect
│ │ └── KClass.kt
│ │ └── ui
│ │ └── BackPressAdapter.kt
│ ├── iosMain
│ └── kotlin
│ │ └── moe
│ │ └── tlaster
│ │ └── precompose
│ │ └── PreComposeApplication.kt
│ ├── jsMain
│ └── kotlin
│ │ └── moe
│ │ └── tlaster
│ │ └── precompose
│ │ ├── PreComposeWindow.kt
│ │ └── reflect
│ │ └── KClass.js.kt
│ ├── jvmMain
│ └── kotlin
│ │ └── moe
│ │ └── tlaster
│ │ └── precompose
│ │ ├── PreComposeWindow.kt
│ │ └── reflect
│ │ └── KClass.jvm.kt
│ ├── jvmTest
│ └── kotlin
│ │ └── moe
│ │ └── tlaster
│ │ └── precompose
│ │ └── navigation
│ │ ├── BackDispatcherTest.kt
│ │ ├── BackStackEntryTest.kt
│ │ ├── BackStackManagerTest.kt
│ │ ├── NavHostTest.kt
│ │ ├── NavigatorTest.kt
│ │ ├── QueryStringTest.kt
│ │ ├── RouteBuilderTest.kt
│ │ ├── RouteParserTest.kt
│ │ ├── TestRoute.kt
│ │ ├── TestViewModelStoreOwner.kt
│ │ └── TestViewModelStoreProvider.kt
│ ├── macosMain
│ └── kotlin
│ │ └── moe
│ │ └── tlaster
│ │ └── precompose
│ │ └── PreComposeWindow.kt
│ ├── nativeMain
│ └── kotlin
│ │ └── moe
│ │ └── tlaster
│ │ └── precompose
│ │ └── reflect
│ │ └── KClass.native.kt
│ └── wasmJsMain
│ └── kotlin
│ └── moe
│ └── tlaster
│ └── precompose
│ ├── PreComposeApp.kt
│ └── reflect
│ └── KClass.wasm.kt
├── project.yml
├── sample
├── molecule
│ ├── composeApp
│ │ ├── build.gradle.kts
│ │ └── src
│ │ │ ├── androidMain
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── kotlin
│ │ │ │ └── moe
│ │ │ │ │ └── tlaster
│ │ │ │ │ └── precompose
│ │ │ │ │ └── molecule
│ │ │ │ │ └── sample
│ │ │ │ │ └── MainActivity.kt
│ │ │ └── res
│ │ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ │ ├── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ │ └── values
│ │ │ │ └── strings.xml
│ │ │ ├── commonMain
│ │ │ ├── composeResources
│ │ │ │ └── drawable
│ │ │ │ │ └── compose-multiplatform.xml
│ │ │ └── kotlin
│ │ │ │ └── moe
│ │ │ │ └── tlaster
│ │ │ │ └── precompose
│ │ │ │ └── molecule
│ │ │ │ └── sample
│ │ │ │ └── App.kt
│ │ │ ├── iosMain
│ │ │ └── kotlin
│ │ │ │ └── moe
│ │ │ │ └── tlaster
│ │ │ │ └── precompose
│ │ │ │ └── molecule
│ │ │ │ └── sample
│ │ │ │ └── MainViewController.kt
│ │ │ ├── jvmMain
│ │ │ └── kotlin
│ │ │ │ └── moe
│ │ │ │ └── tlaster
│ │ │ │ └── precompose
│ │ │ │ └── molecule
│ │ │ │ └── sample
│ │ │ │ └── Main.kt
│ │ │ └── wasmJsMain
│ │ │ ├── kotlin
│ │ │ └── moe
│ │ │ │ └── tlaster
│ │ │ │ └── precompose
│ │ │ │ └── molecule
│ │ │ │ └── sample
│ │ │ │ └── Main.kt
│ │ │ └── resources
│ │ │ ├── index.html
│ │ │ └── styles.css
│ └── iosApp
│ │ ├── Configuration
│ │ └── Config.xcconfig
│ │ └── iosApp
│ │ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ └── app-icon-1024.png
│ │ └── Contents.json
│ │ ├── ContentView.swift
│ │ ├── Info.plist
│ │ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ │ └── iOSApp.swift
└── todo
│ ├── android
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── moe
│ │ └── tlaster
│ │ └── android
│ │ ├── MainActivity.kt
│ │ └── TodoApplication.kt
│ ├── common
│ ├── build.gradle.kts
│ └── src
│ │ └── commonMain
│ │ └── kotlin
│ │ └── moe
│ │ └── tlaster
│ │ └── common
│ │ ├── App.kt
│ │ ├── di
│ │ └── AppModule.kt
│ │ ├── model
│ │ └── Note.kt
│ │ ├── repository
│ │ └── FakeRepository.kt
│ │ ├── scene
│ │ ├── NoteDetailScene.kt
│ │ ├── NoteEditScene.kt
│ │ └── NoteListScene.kt
│ │ └── viewmodel
│ │ ├── NoteDetailViewModel.kt
│ │ ├── NoteEditViewModel.kt
│ │ └── NoteListViewModel.kt
│ ├── desktop
│ ├── build.gradle.kts
│ └── src
│ │ └── jvmMain
│ │ └── kotlin
│ │ └── Main.kt
│ ├── ios
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── plists
│ │ └── Ios
│ │ │ └── Info.plist
│ └── src
│ │ └── uikitMain
│ │ └── kotlin
│ │ └── Main.uikit.kt
│ ├── js
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── jsMain
│ │ ├── kotlin
│ │ └── Main.kt
│ │ └── resources
│ │ ├── index.html
│ │ └── styles.css
│ └── macos
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ └── macosMain
│ └── kotlin
│ └── Main.kt
└── settings.gradle.kts
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG] "
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Minimal reproducible example**
24 | A reproducible repo would be better.
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/apply_signing.sh:
--------------------------------------------------------------------------------
1 | echo $SIGNING_KEY | base64 -d > ./key.gpg
2 | echo "signing.keyId=$SIGNING_KEY_ID
3 | signing.password=$SIGNING_PASSWORD
4 | signing.secretKeyRingFile=./key.gpg
5 | ossrhUsername=$OSSRH_USERNAME
6 | ossrhPassword=$OSSRH_PASSWORD" >publish.properties
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "gradle"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: CI Assemble, Test, and Publish
2 |
3 | on:
4 | workflow_call:
5 |
6 | jobs:
7 | build:
8 |
9 | runs-on: macos-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v4
13 |
14 | - name: Set up JDK 17
15 | uses: actions/setup-java@v4
16 | with:
17 | distribution: 'zulu'
18 | java-version: 17
19 |
20 | - name: Apply Signing
21 | if: ${{ github.event_name != 'pull_request' }}
22 | env:
23 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
24 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
25 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
26 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
27 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
28 | run: ./.github/apply_signing.sh
29 |
30 | - name: Assemble
31 | run: ./gradlew assemble --stacktrace
32 |
33 | - name: Testing
34 | run: ./gradlew test --stacktrace
35 |
36 | - name: Upload Precompose Test Reports
37 | uses: actions/upload-artifact@v4
38 | with:
39 | name: precompose-reports
40 | path: "precompose/build/reports/tests"
41 |
42 | - name: Upload Precompose ViewModel Test Reports
43 | uses: actions/upload-artifact@v4
44 | with:
45 | name: precompose-viewmodel-reports
46 | path: "precompose-viewmodel/build/reports/tests"
47 |
48 | - name: Publishing
49 | if: startsWith(github.ref, 'refs/tags/')
50 | run: ./gradlew publish
51 |
--------------------------------------------------------------------------------
/.github/workflows/static-validations.yml:
--------------------------------------------------------------------------------
1 | name: CI Static Validations
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - '**.md'
7 | pull_request:
8 | paths-ignore:
9 | - '**.md'
10 |
11 | jobs:
12 | validate:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - uses: actions/checkout@v4
17 |
18 | - name: Set up JDK 17
19 | uses: actions/setup-java@v4
20 | with:
21 | distribution: 'zulu'
22 | java-version: 17
23 |
24 | - name: Spotless Check
25 | run: ./gradlew spotlessCheck --stacktrace
26 |
27 | - name: Lint
28 | run: ./gradlew lint --stacktrace
29 |
30 | - name: Upload Precompose Lint Report
31 | uses: actions/upload-artifact@v4
32 | with:
33 | name: precompose-lint-report
34 | path: precompose/build/reports
35 |
36 | - name: Upload Precompose Molecule Lint Report
37 | uses: actions/upload-artifact@v4
38 | with:
39 | name: precompose-molecule-lint-report
40 | path: precompose-molecule/build/reports
41 |
42 | - name: Upload Precompose ViewModel Lint Report
43 | uses: actions/upload-artifact@v4
44 | with:
45 | name: precompose-viewmodel-lint-report
46 | path: precompose-viewmodel/build/reports
47 |
48 | call-build:
49 | needs: validate
50 | uses: ./.github/workflows/build.yml
51 | secrets: inherit
52 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.aar
4 | *.ap_
5 | *.aab
6 |
7 | # Files for the ART/Dalvik VM
8 | *.dex
9 |
10 | # Java class files
11 | *.class
12 |
13 | # Generated files
14 | bin/
15 | gen/
16 | out/
17 | # Uncomment the following line in case you need and you don't have the release build type files in your app
18 | # release/
19 |
20 | # Gradle files
21 | .gradle/
22 | build/
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Proguard folder generated by Eclipse
28 | proguard/
29 |
30 | # Log Files
31 | *.log
32 |
33 | # Android Studio Navigation editor temp files
34 | .navigation/
35 |
36 | # Android Studio captures folder
37 | captures/
38 |
39 | # IntelliJ
40 | *.iml
41 | .idea/workspace.xml
42 | .idea/tasks.xml
43 | .idea/gradle.xml
44 | .idea/assetWizardSettings.xml
45 | .idea/dictionaries
46 | .idea/libraries
47 | # Android Studio 3 in .gitignore file.
48 | .idea/caches
49 | .idea/modules.xml
50 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
51 | .idea/navEditor.xml
52 |
53 | # Keystore files
54 | # Uncomment the following lines if you do not want to check your keystore files in.
55 | #*.jks
56 | #*.keystore
57 |
58 | # External native build folder generated in Android Studio 2.2 and later
59 | .externalNativeBuild
60 | .cxx/
61 |
62 | # Google Services (e.g. APIs or Firebase)
63 | # google-services.json
64 |
65 | # Freeline
66 | freeline.py
67 | freeline/
68 | freeline_project_description.json
69 |
70 | # fastlane
71 | fastlane/report.xml
72 | fastlane/Preview.html
73 | fastlane/screenshots
74 | fastlane/test_output
75 | fastlane/readme.md
76 |
77 | # Version control
78 | vcs.xml
79 |
80 | # lint
81 | lint/intermediates/
82 | lint/generated/
83 | lint/outputs/
84 | lint/tmp/
85 | # lint/reports/
86 |
87 | # Android Profiling
88 | *.hprof
89 |
90 | /.idea
91 |
92 | publish.properties
93 |
94 | .DS_Store
95 |
96 | # Ignoring the persistant lockfile until kotlin.js vulnerabilities are fixed
97 | yarn.lock
98 |
99 | *.xcodeproj
100 |
101 | *.gpg
102 |
103 | .kotlin
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/ktlint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | tlaster@outlook.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How To Contribute
2 |
3 | First of all, I'd like to express my appreciation to you for contributing to this project.
4 | Below is the guidance for how to report issues, propose new features, and submit contributions via Pull Requests (PRs).
5 |
6 | ## Before you start, file an issue
7 | If you have a question, think you've discovered an issue, would like to propose a new feature, etc., then find/file an issue **BEFORE** starting work to fix/implement it.
8 |
9 | ### Search existing issues first
10 |
11 | Before filing a new issue, search existing open and closed issues first: It is likely someone else has found the problem you're seeing, and someone may be working on or have already contributed a fix!
12 |
13 | If no existing item describes your issue/feature, great - please file a new issue.
14 |
15 | ## Contributing fixes / features
16 |
17 | For those able & willing to help fix issues and/or implement features ...
18 |
19 | ### Development environment
20 |
21 | Make sure you have
22 | - JDK 11
23 | - A Mac if you're developing Compose for iOS/macOS
24 |
25 | ### Code guidelines
26 | PreCompose uses [ktlint](https://github.com/pinterest/ktlint) to check the code style, so make sure run `./gradlew spotlessCheck` and fix the errors before you submit any PR.
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Tlaster
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PreCompose
2 | [](https://maven-badges.herokuapp.com/maven-central/moe.tlaster/precompose)
3 | [](https://github.com/JetBrains/compose-jb)
4 | 
5 |
6 | 
7 | 
8 | 
9 | 
10 | 
11 |
12 | Compose Multiplatform Navigation && ViewModel, inspired by Jetpack Navigation, ViewModel and Lifecycle, PreCompose provides similar (or even the same) components for you but in Kotlin, and it's Kotlin Multiplatform project.
13 |
14 | # Why PreCompose
15 | - Write your business logic and UI code once in one `commonMain`, and your application can be anywhere, powered by Kotlin and Compose!
16 | - If you familiar with Jetpack Lifecycle, ViewModel and Navigation, there will be nothing to learn.
17 | - Super easy to set up.
18 | - No need to write platform-specific code and UI.
19 | - Lifecycle is handled by PreCompose, you don't need to worry about it.
20 | - With Molecule integration, you can easily write your business logic in Kotlin Multiplatform project.
21 |
22 | # Setup
23 | [Setup guide for PreCompose](/docs/setup.md)
24 |
25 | # Components
26 | - [Navigation](/docs/component/navigation.md)
27 | - [ViewModel](/docs/component/view_model.md)
28 | - [Molecule Integration](/docs/component/molecule.md)
29 | - [Koin Integration](/docs/component/koin.md)
30 |
31 | # Sample
32 | - [Note App](/docs/sample.md#note-app)
33 | - [Greetings App with ViewModel in 100 lines!](/docs/sample.md#greetings-app-with-viewmodel-in-100-lines)
34 |
35 | # Credits
36 |
37 | Thanks JetBrains for [supporting open source software](https://www.jetbrains.com/community/opensource/#support)
38 |
39 |
40 |
41 |
42 |
43 | # LICENSE
44 | ```
45 | MIT License
46 |
47 | Copyright (c) 2021 Tlaster
48 |
49 | Permission is hereby granted, free of charge, to any person obtaining a copy
50 | of this software and associated documentation files (the "Software"), to deal
51 | in the Software without restriction, including without limitation the rights
52 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
53 | copies of the Software, and to permit persons to whom the Software is
54 | furnished to do so, subject to the following conditions:
55 |
56 | The above copyright notice and this permission notice shall be included in all
57 | copies or substantial portions of the Software.
58 |
59 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
60 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
61 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
62 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
63 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
64 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
65 | SOFTWARE.
66 | ```
67 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2 |
3 | plugins {
4 | alias(libs.plugins.android.application).apply(false)
5 | alias(libs.plugins.android.library).apply(false)
6 | alias(libs.plugins.kotlin.multiplatform).apply(false)
7 | alias(libs.plugins.spotless)
8 | alias(libs.plugins.compose.compiler) apply false
9 | }
10 |
11 | allprojects {
12 | tasks.withType {
13 | compilerOptions {
14 | jvmTarget.set(JvmTarget.fromTarget(libs.versions.java.get()))
15 | allWarningsAsErrors.set(true)
16 | freeCompilerArgs.set(
17 | listOf(
18 | "-Xcontext-receivers",
19 | "-Xexpect-actual-classes",
20 | ),
21 | )
22 | }
23 | }
24 | apply(plugin = "com.diffplug.spotless")
25 | spotless {
26 | kotlin {
27 | target("**/*.kt")
28 | targetExclude("${layout.buildDirectory}/**/*.kt", "bin/**/*.kt", "buildSrc/**/*.kt")
29 | ktlint(libs.versions.ktlint.get())
30 | }
31 | kotlinGradle {
32 | target("*.gradle.kts")
33 | ktlint(libs.versions.ktlint.get())
34 | }
35 | java {
36 | target("**/*.java")
37 | targetExclude("${layout.buildDirectory}/**/*.java", "bin/**/*.java")
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-slate
--------------------------------------------------------------------------------
/docs/component/koin.md:
--------------------------------------------------------------------------------
1 | # Koin
2 |
3 | Koin is a popular dependency injection framework for Kotlin, and it's also a Kotlin Multiplatform project. For more information: https://github.com/InsertKoinIO/koin
4 |
5 | # Why Koin with PreCompose
6 | When using PreCompose ViewModel with Koin, you do have to manage ViewModel's lifecycle, a simple 'inject()' from Koin does not handle the ViewModel's lifecycle.
7 |
8 | # Setup
9 | ## Add Dependency
10 | [](https://maven-badges.herokuapp.com/maven-central/moe.tlaster/precompose-koin)
11 |
12 | Add the dependency **in your common module's commonMain sourceSet**
13 | ```
14 | api("moe.tlaster:precompose-koin:$precompose_version")
15 | ```
16 | # Usage
17 |
18 | First you can define your ViewModel in Koin module like this:
19 | ```Kotlin
20 | factory { (initialCount: Int) ->
21 | CounterViewModel(
22 | initialCount = initialCount,
23 | counterRepository = get(),
24 | )
25 | }
26 | ```
27 | And you can use your ViewModel in Compose like this:
28 | ```Kotlin
29 | val viewModel = koinViewModel { parametersOf(initialCount) }
30 | ```
31 |
32 | NOTE: If you're using Kotlin/Native target, please use viewModel with vmClass parameter instead.
33 | ```kotlin
34 | val viewModel = koinViewModel(vmClass = CounterViewModel::class) { parametersOf(initialCount) }
35 | ```
36 |
--------------------------------------------------------------------------------
/docs/component/molecule.md:
--------------------------------------------------------------------------------
1 | # Molecule
2 |
3 | Molecule is a library from cashapp, which can write business logic in Compose, and it's also Kotlin Multiplatform project. For more information: https://github.com/cashapp/molecule
4 |
5 | # Why Molecule with PreCompose
6 | Since Molecule does not include any Lifecycle and Navigation state management, PreCompose can help you to integrate Molecule with Lifecycle and Navigation.
7 |
8 | # Setup
9 | ## Add Dependency
10 | [](https://maven-badges.herokuapp.com/maven-central/moe.tlaster/precompose-molecule)
11 |
12 | Add the dependency **in your common module's commonMain sourceSet**
13 | ```
14 | api("moe.tlaster:precompose-molecule:$precompose_version")
15 | ```
16 | # Usage
17 |
18 | ## Flow Action
19 |
20 | You can write a Presenter like this:
21 | ```kotlin
22 | @Composable
23 | fun CounterPresenter(
24 | action: Flow,
25 | ): CounterState {
26 | var count by remember { mutableStateOf(0) }
27 |
28 | action.collectAction {
29 | when (this) {
30 | CounterAction.Increment -> count++
31 | CounterAction.Decrement -> count--
32 | }
33 | }
34 |
35 | return CounterState("Clicked $count times")
36 | }
37 | ```
38 | in your Compose UI, you can use this `CounterPresenter` with `rememberPresenter`
39 | ```kotlin
40 | val (state, channel) = rememberPresenter { CounterPresenter(it) }
41 | ```
42 | `state` is the instance of `CounterState`, which return by `CounterPresenter`, and `channel` is the instance of `Channel`, you can send event to `CounterPresenter` by `channel.trySend(CounterEvent.Increment)`
43 |
44 | The molecule scope and the Event Channel will be managed by the ViewModel, so it has the same lifecycle as the ViewModel.
45 |
46 | You can nest your Presenter by using `rememberNestedPresenter`
47 |
48 | ## State Action
49 |
50 | If you prefer using State Action, you can write a Presenter like this:
51 | ```kotlin
52 | @Composable
53 | fun CounterPresenter(): CounterState {
54 | var count by remember { mutableStateOf(0) }
55 | return CounterState("Clicked $count times") {
56 | when (it) {
57 | CounterAction.Increment -> count++
58 | CounterAction.Decrement -> count--
59 | }
60 | }
61 | }
62 | ```
63 |
64 | in your Compose UI, you can use this `CounterPresenter` with `producePresenter`
65 | ```kotlin
66 | val state by producePresenter { CounterPresenter() }
67 | ```
--------------------------------------------------------------------------------
/docs/component/navigation.md:
--------------------------------------------------------------------------------
1 | # Navigation
2 |
3 | Similar to Jetpack Navigation, but a little different.
4 |
5 | You still have the `NavHost` composable function to define the navigation graph like what you've done in Jetpack Navigation, and it behaves like what Jetpack Navigation provides. `NavHost` provides back stack management and stack lifecycle and viewModel management.
6 |
7 | ## Quick example
8 | ```kotlin
9 | // Define a navigator, which is a replacement for Jetpack Navigation's NavController
10 | val navigator = rememberNavigator()
11 | NavHost(
12 | // Assign the navigator to the NavHost
13 | navigator = navigator,
14 | // Navigation transition for the scenes in this NavHost, this is optional
15 | navTransition = NavTransition(),
16 | // The start destination
17 | initialRoute = "/home",
18 | ) {
19 | // Define a scene to the navigation graph
20 | scene(
21 | // Scene's route path
22 | route = "/home",
23 | // Navigation transition for this scene, this is optional
24 | navTransition = NavTransition(),
25 | ) {
26 | Text(text = "Hello!")
27 | }
28 | }
29 | ```
30 |
31 | ## Navigator
32 |
33 | Replacement for Jetpack Navigation's NavController
34 |
35 | - `Navigator.navigate(route: String, options: NavOptions? = null)`
36 | Navigate to a route in the current RouteGraph with optional NavOptions
37 |
38 | - `Navigator.goBack()`
39 | Attempts to navigate up in the navigation hierarchy
40 |
41 | - `Navigator.canGoBack: Boolean`
42 | Check if navigator can navigate up
43 |
44 | ### NavOptions
45 |
46 | Similar to Jetpack Navigation's NavOptions, you can have NavOptions like this:
47 | ```kotlin
48 | navigator.navigate(
49 | "/home",
50 | NavOptions(
51 | // Launch the scene as single top
52 | launchSingleTop = true,
53 | ),
54 | )
55 | ```
56 | or you can have `popUpTo` functionality
57 | ```kotlin
58 | navigator.navigate(
59 | "/detail",
60 | NavOptions(
61 | popUpTo = PopUpTo(
62 | // The destination of popUpTo
63 | route = "/home",
64 | // Whether the popUpTo destination should be popped from the back stack.
65 | inclusive = true,
66 | )
67 | ),
68 | )
69 | ```
70 |
71 | ## Scene route pattern
72 |
73 | ### Static
74 | ```kotlin
75 | scene(route = "/home") {
76 |
77 | }
78 | ```
79 |
80 | ### Variable
81 | ```kotlin
82 | scene(route = "/detail/{id}") { backStackEntry ->
83 | val id: Int? = backStackEntry.path("id")
84 | }
85 | ```
86 | this is most common usage of navigation route
87 |
88 | Optional path variable
89 | ```kotlin
90 | scene(route = "/detail/{id}?") { backStackEntry ->
91 | val id: Int? = backStackEntry.path("id")
92 | }
93 | ```
94 | The trailing ? makes the path variable optional. The route matches:
95 | - `/detail`
96 | - `/detail/123`
97 | - `/detail/asd`
98 |
99 | ### Regex
100 | ```kotlin
101 | scene(route = "/detail/{id:[0-9]+}") { backStackEntry ->
102 | val id: Int? = backStackEntry.path("id")
103 | }
104 | ```
105 | You can define a path variable: `id`. Regex expression is everything after the first `:`, like: `[0-9]+`
106 |
107 | Optional syntax is also supported for regex path variable: `/user/{id:[0-9]+}?`:
108 | - matches `/user`
109 | - matches `/user/123`
110 |
111 | ### Group
112 | Note: group's `route` and `initialRoute` does not support path variable and regex, only static route is supported.
113 | ```kotlin
114 | group(route = "/group", initialRoute = "/nestedScreen1") {
115 | scene(route = "/nestedScreen1") {
116 |
117 | }
118 | scene(route = "/nestedScreen2") {
119 |
120 | }
121 | }
122 | ```
123 |
124 | ## QueryString
125 |
126 | **DO NOT** define your query string in scene route, this will have no effect on both navigation route and query string.
127 |
128 | You can pass your query string as to `Navigator.navigate(route: String)`, like: `Navigator.navigate("/detail/123?my=query")`
129 |
130 | And you can retrieve query string from `BackStackEntry.query(name: String)`
131 |
132 | ```kotlin
133 | scene(route = "/detail/{id}") { backStackEntry ->
134 | val my: String? = backStackEntry.query("my")
135 | }
136 | ```
137 |
138 | If your query string is a list, you can retrieve by `BackStackEntry.queryList(name: String)`
139 |
140 | ## Navigation transition
141 | You can define a `NavTransition` for both `NavHost` and `scene`, PreCompose will use the `scene`'s `NavTransition` if the `scene` define a `NavTransition`, otherwise will fall back to `NavHost`'s `NavTransition`.
142 |
143 | There are 4 transition type for `NavTransition`
144 | - `createTransition`
145 | Transition the scene that about to appear for the first time, similar to activity onCreate
146 |
147 | - `destroyTransition`
148 | Transition the scene that about to disappear forever, similar to activity onDestroy
149 |
150 | - `pauseTransition`
151 | Transition the scene that will be pushed into back stack, similar to activity onPause
152 |
153 | - `resumeTransition`
154 | Transition the scene that about to show from the back stack, similar to activity onResume
155 |
--------------------------------------------------------------------------------
/docs/component/view_model.md:
--------------------------------------------------------------------------------
1 | # ViewModel
2 |
3 | Basically the same as Jetpack ViewModel
4 | # Setup
5 | ## Add Dependency
6 | [](https://maven-badges.herokuapp.com/maven-central/moe.tlaster/precompose-viewmodel)
7 |
8 | Add the dependency **in your common module's commonMain sourceSet**
9 | ```
10 | api("moe.tlaster:precompose-viewmodel:$precompose_version")
11 | ```
12 |
13 | # Usage
14 |
15 | You can define you ViewModel like this:
16 | ```kotlin
17 | class HomeViewModel : ViewModel() {
18 |
19 | }
20 | ```
21 | With `viewModelScope` you can run suspend function like what you've done in Jetpack ViewModel.
22 |
23 | To use ViewModel in compose, you can use the `viewModel()`
24 | ```kotlin
25 | val viewModel = viewModel(keys = listOf(someKey)) {
26 | SomeViewModel(someKey)
27 | }
28 | ```
29 | When the data that passing to the `keys` parameter changed, viewModel will be recreated, otherwise you will have the same viewModel instance. It's useful when your viewModel depends on some parameter that will receive from outside.
30 |
31 | NOTE: If you're using Kotlin/Native target, please use viewModel with modelClass parameter instead.
32 | ```kotlin
33 | val viewModel = viewModel(modelClass = SomeViewModel::class, keys = listOf(someKey)) {
34 | SomeViewModel(someKey)
35 | }
36 | ```
37 |
38 | If you need to save and restore state in the ViewModel, you can use SavedStateHolder.
39 | ```kotlin
40 | val viewModel = viewModel(modelClass = SomeViewModel::class, keys = listOf(someKey)) { savedStateHolder ->
41 | SomeViewModel(someKey, savedStateHolder)
42 | }
43 | ```
44 |
45 | Then the ViewModel might look like this:
46 | ```kotlin
47 | class SomeViewModel(private val someKey: Int?, savedStateHolder: SavedStateHolder) : ViewModel() {
48 | val someSavedValue = MutableStateFlow(savedStateHolder.consumeRestored("someValue") as String? ?: "")
49 |
50 | init {
51 | savedStateHolder.registerProvider("someValue") {
52 | someSavedValue.value
53 | }
54 | }
55 |
56 | fun setSomeValue(value: String) {
57 | someSavedValue.value = value
58 | }
59 | }
60 | ```
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # PreCompose
2 | [](https://maven-badges.herokuapp.com/maven-central/moe.tlaster/precompose)
3 | [](https://github.com/JetBrains/compose-jb)
4 | 
5 |
6 | 
7 | 
8 | 
9 | 
10 | 
11 |
12 | Compose Multiplatform Navigation && ViewModel, inspired by Jetpack Lifecycle, ViewModel, LiveData and Navigation, PreCompose provides similar (or even the same) components for you but in Kotlin, and it's Kotlin Multiplatform project.
13 |
14 | # Why PreCompose
15 | - Write your business logic and UI code once in one `commonMain`, and your application can be anywhere, powered by Kotlin and Compose!
16 | - If you familiar with Jetpack Lifecycle, ViewModel and Navigation, there will be nothing to learn.
17 | - Super easy to set up.
18 | - No need to write platform-specific code and UI.
19 | - Lifecycle is handled by PreCompose, you don't need to worry about it.
20 | - With Molecule integration, you can easily write your business logic in Kotlin Multiplatform project.
21 |
22 | # Setup
23 | [Setup guide for PreCompose](/setup.md)
24 |
25 | # Components
26 | - [Navigation](/component/navigation.md)
27 | - [ViewModel](/component/view_model.md)
28 | - [Molecule Integration](/component/molecule.md)
29 | - [Koin Integration](/component/koin.md)
30 |
31 | # Sample
32 | - [Note App](/sample.md#note-app)
33 | - [Greetings App with ViewModel in 100 lines!](/sample.md#greetings-app-with-viewmodel-in-100-lines)
34 |
35 | # Why the name PreCompose?
36 | IDK, just let my cat hitting the keyboard, and it came up with it.
--------------------------------------------------------------------------------
/docs/sample.md:
--------------------------------------------------------------------------------
1 | # Sample
2 |
3 | ## Note App
4 | [Source Code](https://github.com/Tlaster/PreCompose/tree/master/sample/todo)
5 |
6 | run `./gradlew :sample:todo:desktop:run` to run the desktop app or `./gradlew :sample:todo:android:installDebug` to install the Android app.
7 |
8 | This sample demonstrates the following features:
9 | - Navigation
10 | - Custom Navigation transition
11 | - ViewModel
12 |
13 |
14 |
15 |
16 | ## Greetings App with ViewModel in 100 lines!
17 |
18 | ```kotlin
19 | @Composable
20 | fun App() {
21 | PreComposeApp {
22 | val navigator = rememberNavigator()
23 | MaterialTheme {
24 | NavHost(
25 | navigator = navigator,
26 | initialRoute = "/home"
27 | ) {
28 | scene(route = "/home") {
29 | val homeViewModel = viewModel {
30 | HomeViewModel()
31 | }
32 | val name by homeViewModel.name.observeAsState()
33 | Column(
34 | modifier = Modifier.fillMaxSize(),
35 | horizontalAlignment = Alignment.CenterHorizontally,
36 | verticalArrangement = Arrangement.Center
37 | ) {
38 | Text(
39 | text = "Greet Me!",
40 | style = MaterialTheme.typography.h6
41 | )
42 | Spacer(modifier = Modifier.height(30.dp))
43 | TextField(
44 | value = name,
45 | maxLines = 1,
46 | label = { Text(text = "Enter your name") },
47 | onValueChange = {
48 | homeViewModel.setName(it)
49 | }
50 | )
51 | Spacer(modifier = Modifier.height(30.dp))
52 | Button(
53 | onClick = {
54 | navigator.navigate(route = "/greeting/$name")
55 | }
56 | ) {
57 | Text(text = "GO!")
58 | }
59 | }
60 | }
61 | scene(route = "/greeting/{name}") { backStackEntry ->
62 | backStackEntry.path("name")?.let { name ->
63 | Column(
64 | modifier = Modifier.fillMaxSize(),
65 | horizontalAlignment = Alignment.CenterHorizontally,
66 | verticalArrangement = Arrangement.Center
67 | ) {
68 | Text(
69 | text = name,
70 | style = MaterialTheme.typography.h6
71 | )
72 | Spacer(modifier = Modifier.height(30.dp))
73 | Button(onClick = { navigator.goBack() }) {
74 | Text(text = "GO BACK!")
75 | }
76 | }
77 | }
78 | }
79 | }
80 | }
81 | }
82 | }
83 |
84 | class HomeViewModel : ViewModel() {
85 | val name = LiveData("")
86 | fun setName(value: String) {
87 | name.value = value
88 | }
89 | }
90 | ```
91 |
92 |
93 | ## Greetings App with ViewModel using StateFlow in 100 lines!
94 |
95 | ```kotlin
96 | @Composable
97 | fun App() {
98 | PreComposeApp {
99 | val navigator = rememberNavigator()
100 | MaterialTheme {
101 | NavHost(
102 | navigator = navigator,
103 | initialRoute = "/home"
104 | ) {
105 | scene(route = "/home") {
106 | val homeViewModel = viewModel {
107 | HomeViewModel()
108 | }
109 | val name by homeViewModel.name.collectAsStateWithLifecycle()
110 | Column(
111 | modifier = Modifier.fillMaxSize(),
112 | horizontalAlignment = Alignment.CenterHorizontally,
113 | verticalArrangement = Arrangement.Center
114 | ) {
115 | Text(
116 | text = "Greet Me!",
117 | style = MaterialTheme.typography.h6
118 | )
119 | Spacer(modifier = Modifier.height(30.dp))
120 | TextField(
121 | value = name,
122 | maxLines = 1,
123 | label = { Text(text = "Enter your name") },
124 | onValueChange = homeViewModel::setName
125 | )
126 | Spacer(modifier = Modifier.height(30.dp))
127 | Button(
128 | onClick = {
129 | navigator.navigate(route = "/greeting/$name")
130 | }
131 | ) {
132 | Text(text = "GO!")
133 | }
134 | }
135 | }
136 | scene(route = "/greeting/{name}") { backStackEntry ->
137 | backStackEntry.path("name")?.let { name ->
138 | Column(
139 | modifier = Modifier.fillMaxSize(),
140 | horizontalAlignment = Alignment.CenterHorizontally,
141 | verticalArrangement = Arrangement.Center
142 | ) {
143 | Text(
144 | text = name,
145 | style = MaterialTheme.typography.h6
146 | )
147 | Spacer(modifier = Modifier.height(30.dp))
148 | Button(onClick = navigator::goBack) {
149 | Text(text = "GO BACK!")
150 | }
151 | }
152 | }
153 | }
154 | }
155 | }
156 | }
157 | }
158 |
159 | class HomeViewModel : ViewModel() {
160 | val name = MutableStateFlow("")
161 | fun setName(value: String) {
162 | name.update { value }
163 | }
164 | }
165 | ```
166 |
167 |
--------------------------------------------------------------------------------
/docs/setup.md:
--------------------------------------------------------------------------------
1 | # Setup
2 |
3 | ## Add Dependency
4 | [](https://maven-badges.herokuapp.com/maven-central/moe.tlaster/precompose)
5 |
6 | Add the dependency **in your common module's commonMain sourceSet**
7 | ```
8 | // Please do remember to add compose.foundation and compose.animation
9 | api(compose.foundation)
10 | api(compose.animation)
11 | //...
12 | api("moe.tlaster:precompose:$precompose_version")
13 |
14 | // api("moe.tlaster:precompose-molecule:$precompose_version") // For Molecule intergration
15 |
16 | // api("moe.tlaster:precompose-viewmodel:$precompose_version") // For ViewModel intergration
17 |
18 | // api("moe.tlaster:precompose-koin:$precompose_version") // For Koin intergration
19 | ```
20 |
21 | ## Wrap the `App()`
22 |
23 | Wrap your App with `PreComposApp` like this:
24 | ```Kotlin
25 | @Composable
26 | fun App() {
27 | PreComposeApp {
28 | // your app's content goes here
29 | }
30 | }
31 | ```
32 |
33 | ## Done!
34 | That's it! Enjoying the PreCompose! Now you can write all your business logic and ui code in `commonMain`
35 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | ## For more details on how to configure your build environment visit
2 | # http://www.gradle.org/docs/current/userguide/build_environment.html
3 | #
4 | # Specifies the JVM arguments used for the daemon process.
5 | # The setting is particularly useful for tweaking memory settings.
6 | # Default value: -Xmx1024m -XX:MaxPermSize=256m
7 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
8 | #
9 | # When configured, Gradle will run in incubating parallel mode.
10 | # This option should only be used with decoupled projects. More details, visit
11 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
12 | # org.gradle.parallel=true
13 | #Sat Mar 20 23:20:57 CST 2021
14 | kotlin.code.style=official
15 | android.useAndroidX=true
16 | android.enableJetifier=true
17 | org.gradle.jvmargs=-Xmx4g
18 | org.jetbrains.compose.experimental.jscanvas.enabled=true
19 | org.jetbrains.compose.experimental.macos.enabled=true
20 | org.jetbrains.compose.experimental.uikit.enabled=true
21 | org.jetbrains.compose.experimental.wasm.enabled=true
22 | kotlin.mpp.androidSourceSetLayoutVersion=2
23 | android.defaults.buildfeatures.buildconfig=true
24 | android.nonTransitiveRClass=false
25 | android.nonFinalResIds=false
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions] # also check project root build.gradle.kts for versions
2 | libVersion = "1.7.0"
3 | compileSdk = "35"
4 | minSdk = "21"
5 | java = "11"
6 | androidx-animation = "1.7.5"
7 | androidx-foundation = "1.7.5"
8 | androidx-appcompat = "1.7.0"
9 | androidx-coreKtx = "1.15.0"
10 | androidxActivityVer = "1.9.3"
11 | androidGradlePlugin = "8.6.1"
12 | junit = "4.13.2"
13 | junitJupiterEngine = "5.11.0"
14 | junitJupiterApi = "5.11.0"
15 | kotlin = "2.0.20"
16 | lifecycleRuntimeKtx = "2.8.7"
17 | material = "1.7.5"
18 | kotlinxCoroutinesCore = "1.9.0"
19 | moleculeRuntime = "2.0.0"
20 | savedstateKtx = "1.2.1"
21 | spotless = "6.25.0"
22 | jetbrainsComposePlugin = "1.7.1"
23 | skiko = "0.8.18"
24 | koin = "4.0.0"
25 | uiTestJunit4Android = "1.7.5"
26 | uiTestManifest = "1.7.5"
27 | uuid = "0.8.4"
28 | webpackCliVersion = "5.1.4"
29 | nodeVersion = "20.14.0"
30 | ktlint = "0.50.0"
31 |
32 | [libraries]
33 | androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "androidxActivityVer" }
34 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivityVer" }
35 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
36 | androidx-coreKtx = { module = "androidx.core:core-ktx", version.ref = "androidx-coreKtx" }
37 | androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
38 | androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
39 | androidx-material = { module = "androidx.compose.material:material", version.ref = "material" }
40 | androidx-savedstate-ktx = { module = "androidx.savedstate:savedstate-ktx", version.ref = "savedstateKtx" }
41 | androidx-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "uiTestManifest" }
42 | androidx-ui-test-junit4-android = { module = "androidx.compose.ui:ui-test-junit4-android", version.ref = "uiTestJunit4Android" }
43 | animation = { module = "androidx.compose.animation:animation", version.ref = "androidx-animation" }
44 | foundation = { module = "androidx.compose.foundation:foundation", version.ref = "androidx-foundation" }
45 | junit = { module = "junit:junit", version.ref = "junit" }
46 | junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junitJupiterEngine" }
47 | junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junitJupiterApi" }
48 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
49 | kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinxCoroutinesCore" }
50 | kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesCore" }
51 | molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "moleculeRuntime" }
52 | skiko = { module = "org.jetbrains.skiko:skiko", version.ref = "skiko" }
53 | skiko-js = { module = "org.jetbrains.skiko:skiko-js-wasm-runtime", version.ref = "skiko" }
54 | koin = { module = "io.insert-koin:koin-core", version.ref = "koin" }
55 | koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
56 | koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" }
57 | uuid = { module = "com.benasher44:uuid", version.ref = "uuid" }
58 | jetbrains-lifecycle = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version = "2.8.4" }
59 | jetbrains-viewmodel = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version = "2.8.4" }
60 |
61 | [plugins]
62 | android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
63 | android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
64 | jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "jetbrainsComposePlugin" }
65 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
66 | spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
67 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tlaster/PreCompose/0f79c88a4f768cf851cdaa1da36759ee2d489de1/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo. 1>&2
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
48 | echo. 1>&2
49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
50 | echo location of your Java installation. 1>&2
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo. 1>&2
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
62 | echo. 1>&2
63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
64 | echo location of your Java installation. 1>&2
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/media/greeting_app.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tlaster/PreCompose/0f79c88a4f768cf851cdaa1da36759ee2d489de1/media/greeting_app.gif
--------------------------------------------------------------------------------
/media/note_app.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tlaster/PreCompose/0f79c88a4f768cf851cdaa1da36759ee2d489de1/media/note_app.webp
--------------------------------------------------------------------------------
/media/note_app_android.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tlaster/PreCompose/0f79c88a4f768cf851cdaa1da36759ee2d489de1/media/note_app_android.webp
--------------------------------------------------------------------------------
/precompose-molecule/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
2 | import java.util.Properties
3 |
4 | plugins {
5 | kotlin("multiplatform")
6 | alias(libs.plugins.jetbrains.compose)
7 | id("com.android.library")
8 | id("maven-publish")
9 | id("signing")
10 | alias(libs.plugins.compose.compiler)
11 | }
12 |
13 | group = "moe.tlaster"
14 | version = libs.versions.libVersion.get()
15 |
16 | kotlin {
17 | applyDefaultHierarchyTemplate()
18 | androidTarget {
19 | publishLibraryVariants("release", "debug")
20 | }
21 | jvm {
22 | testRuns["test"].executionTask.configure {
23 | useJUnitPlatform()
24 | }
25 | }
26 | macosArm64()
27 | macosX64()
28 | // ios()
29 | iosX64()
30 | iosArm64()
31 | iosSimulatorArm64()
32 | js(IR) {
33 | browser()
34 | }
35 | @OptIn(ExperimentalWasmDsl::class)
36 | wasmJs {
37 | browser()
38 | }
39 | sourceSets {
40 | val commonMain by getting {
41 | dependencies {
42 | implementation(compose.foundation)
43 | compileOnly(libs.molecule.runtime)
44 | implementation(libs.jetbrains.viewmodel)
45 | }
46 | }
47 | val commonTest by getting {
48 | dependencies {
49 | implementation(kotlin("test-common"))
50 | implementation(kotlin("test-annotations-common"))
51 | // implementation(compose("org.jetbrains.compose.ui:ui-test-junit4"))
52 | }
53 | }
54 | val androidMain by getting {
55 | dependencies {
56 | api(libs.androidx.activity.ktx)
57 | implementation(libs.foundation)
58 | }
59 | }
60 | val androidUnitTest by getting {
61 | dependencies {
62 | implementation(kotlin("test-junit"))
63 | implementation(libs.junit)
64 | }
65 | }
66 | val jvmMain by getting {
67 | dependencies {
68 | implementation(compose.foundation)
69 | api(libs.kotlinx.coroutines.swing)
70 | }
71 | }
72 | val jvmTest by getting {
73 | dependencies {
74 | implementation(kotlin("test-junit5"))
75 | implementation(libs.junit.jupiter.api)
76 | runtimeOnly(libs.junit.jupiter.engine)
77 | }
78 | }
79 | val jsMain by getting {
80 | dependencies {
81 | implementation(compose.foundation)
82 | }
83 | }
84 | val jsTest by getting {
85 | dependencies {
86 | implementation(kotlin("test-js"))
87 | }
88 | }
89 | val macosMain by getting {
90 | dependsOn(commonMain)
91 | dependencies {
92 | implementation(compose.foundation)
93 | }
94 | }
95 | val iosMain by getting {
96 | dependsOn(commonMain)
97 | dependencies {
98 | implementation(compose.foundation)
99 | }
100 | }
101 | }
102 | }
103 |
104 | android {
105 | compileSdk = libs.versions.compileSdk.get().toInt()
106 | namespace = "moe.tlaster.precompose.molecule"
107 | defaultConfig {
108 | minSdk = libs.versions.minSdk.get().toInt()
109 | }
110 | compileOptions {
111 | sourceCompatibility = JavaVersion.toVersion(libs.versions.java.get())
112 | targetCompatibility = JavaVersion.toVersion(libs.versions.java.get())
113 | }
114 | }
115 |
116 | extra.apply {
117 | val publishPropFile = rootProject.file("publish.properties")
118 | if (publishPropFile.exists()) {
119 | Properties().apply {
120 | load(publishPropFile.inputStream())
121 | }.forEach { name, value ->
122 | if (name == "signing.secretKeyRingFile") {
123 | set(name.toString(), rootProject.file(value.toString()).absolutePath)
124 | } else {
125 | set(name.toString(), value)
126 | }
127 | }
128 | } else {
129 | set("signing.keyId", System.getenv("SIGNING_KEY_ID"))
130 | set("signing.password", System.getenv("SIGNING_PASSWORD"))
131 | set("signing.secretKeyRingFile", System.getenv("SIGNING_SECRET_KEY_RING_FILE"))
132 | set("ossrhUsername", System.getenv("OSSRH_USERNAME"))
133 | set("ossrhPassword", System.getenv("OSSRH_PASSWORD"))
134 | }
135 | }
136 | val javadocJar by tasks.registering(Jar::class) {
137 | archiveClassifier.set("javadoc")
138 | }
139 | // https://github.com/gradle/gradle/issues/26091
140 | val signingTasks = tasks.withType()
141 | tasks.withType().configureEach {
142 | dependsOn(signingTasks)
143 | }
144 | publishing {
145 | if (rootProject.file("publish.properties").exists()) {
146 | signing {
147 | sign(publishing.publications)
148 | }
149 | repositories {
150 | maven {
151 | val releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/"
152 | val snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/"
153 | url = if (version.toString().endsWith("SNAPSHOT")) {
154 | uri(snapshotsRepoUrl)
155 | } else {
156 | uri(releasesRepoUrl)
157 | }
158 | credentials {
159 | username = project.ext.get("ossrhUsername").toString()
160 | password = project.ext.get("ossrhPassword").toString()
161 | }
162 | }
163 | }
164 | }
165 |
166 | publications.withType {
167 | artifact(javadocJar)
168 | pom {
169 | name.set("PreCompose-Molecule")
170 | description.set("PreCompose molecule intergration")
171 | url.set("https://github.com/Tlaster/PreCompose")
172 |
173 | licenses {
174 | license {
175 | name.set("MIT")
176 | url.set("https://opensource.org/licenses/MIT")
177 | }
178 | }
179 | developers {
180 | developer {
181 | id.set("Tlaster")
182 | name.set("James Tlaster")
183 | email.set("tlaster@outlook.com")
184 | }
185 | }
186 | scm {
187 | url.set("https://github.com/Tlaster/PreCompose")
188 | connection.set("scm:git:git://github.com/Tlaster/PreCompose.git")
189 | developerConnection.set("scm:git:git://github.com/Tlaster/PreCompose.git")
190 | }
191 | }
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/precompose-molecule/src/androidMain/kotlin/moe/tlaster/precompose/molecule/ProvidePlatformDispatcher.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.molecule
2 |
3 | import androidx.compose.ui.platform.AndroidUiDispatcher
4 | import kotlin.coroutines.CoroutineContext
5 |
6 | internal actual fun providePlatformDispatcher(): CoroutineContext = AndroidUiDispatcher.Main
7 |
--------------------------------------------------------------------------------
/precompose-molecule/src/commonMain/kotlin/moe/tlaster/precompose/molecule/Molecule.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.molecule
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.LaunchedEffect
5 | import androidx.compose.runtime.MonotonicFrameClock
6 | import androidx.compose.runtime.State
7 | import androidx.compose.runtime.collectAsState
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.runtime.remember
10 | import androidx.lifecycle.ViewModel
11 | import androidx.lifecycle.viewmodel.compose.viewModel
12 | import androidx.lifecycle.viewmodel.viewModelFactory
13 | import app.cash.molecule.RecompositionMode
14 | import app.cash.molecule.launchMolecule
15 | import kotlinx.coroutines.CoroutineScope
16 | import kotlinx.coroutines.cancel
17 | import kotlinx.coroutines.channels.Channel
18 | import kotlinx.coroutines.flow.Flow
19 | import kotlinx.coroutines.flow.StateFlow
20 | import kotlinx.coroutines.flow.consumeAsFlow
21 | import kotlin.coroutines.CoroutineContext
22 |
23 | internal expect fun providePlatformDispatcher(): CoroutineContext
24 |
25 | private class PresenterHolder(
26 | useImmediateClock: Boolean,
27 | body: @Composable () -> T,
28 | ) : ViewModel() {
29 | private val dispatcher = providePlatformDispatcher()
30 | private val clock = if (useImmediateClock || dispatcher[MonotonicFrameClock] == null) {
31 | RecompositionMode.Immediate
32 | } else {
33 | RecompositionMode.ContextClock
34 | }
35 | private val scope = CoroutineScope(dispatcher)
36 | val state = scope.launchMolecule(mode = clock, body = body)
37 |
38 | override fun onCleared() {
39 | scope.cancel()
40 | }
41 | }
42 |
43 | private class ActionViewHolder : ViewModel() {
44 | val channel = Channel(Channel.UNLIMITED)
45 | val pair = channel to channel.consumeAsFlow()
46 | override fun onCleared() {
47 | channel.close()
48 | }
49 | }
50 |
51 | @Composable
52 | private fun rememberAction(
53 | key: String? = null,
54 | ): Pair, Flow> {
55 | return viewModel>(key = key) {
56 | ActionViewHolder()
57 | }.pair
58 | }
59 |
60 | /**
61 | * Return StateFlow, use it in your Compose UI
62 | * The molecule scope will be managed by the [ViewModel], so it has the same lifecycle as the [ViewModel]
63 | * @param key The key to use to identify the Presenter
64 | * @param body The body of the molecule presenter
65 | * @return StateFlow
66 | */
67 | @Composable
68 | private fun rememberPresenterState(
69 | key: String? = null,
70 | useImmediateClock: Boolean,
71 | body: @Composable () -> T,
72 | ): StateFlow {
73 | val factory = remember(key) {
74 | viewModelFactory {
75 | addInitializer(PresenterHolder::class) {
76 | PresenterHolder(useImmediateClock, body)
77 | }
78 | }
79 | }
80 | return viewModel>(key = key, factory = factory).state
81 | }
82 |
83 | /**
84 | * Return State, use it in your Compose UI
85 | * The molecule scope will be managed by the [ViewModel], so it has the same lifecycle as the [ViewModel]
86 | * @param key The key to use to identify the Presenter
87 | * @param useImmediateClock Use immediate clock or not, for text input, you should set it to true
88 | * @param body The body of the molecule presenter
89 | * @return State
90 | */
91 | @Composable
92 | fun producePresenter(
93 | key: String? = null,
94 | useImmediateClock: Boolean = false,
95 | body: @Composable () -> T,
96 | ): State {
97 | val presenter = rememberPresenterState(key = key, useImmediateClock = useImmediateClock) { body() }
98 | return presenter.collectAsState()
99 | }
100 |
101 | /**
102 | * Return pair of State and Action Channel, use it in your Compose UI
103 | * The molecule scope and the Action Channel will be managed by the [ViewModel], so it has the same lifecycle as the [ViewModel]
104 | *
105 | * @param key The key to use to identify the Presenter
106 | * @param useImmediateClock Use immediate clock or not, for text input, you should set it to true
107 | * @param body The body of the molecule presenter, the flow parameter is the flow of the action channel
108 | * @return Pair of State and Action channel
109 | */
110 | @Composable
111 | fun rememberPresenter(
112 | key: String? = null,
113 | useImmediateClock: Boolean = false,
114 | body: @Composable (flow: Flow) -> T,
115 | ): Pair> {
116 | val (channel, action) = rememberAction(key = key)
117 | val presenter = rememberPresenterState(key = key, useImmediateClock = useImmediateClock) { body(action) }
118 | val state by presenter.collectAsState()
119 | return state to channel
120 | }
121 |
122 | /**
123 | * Return pair of State and Action Channel, use it in your Compose UI
124 | * The molecule scope and the Action Channel will be managed by the [ViewModel], so it has the same lifecycle as the [ViewModel]
125 | *
126 | * @param body The body of the molecule presenter, the flow parameter is the flow of the action channel
127 | * @return Pair of State and Action channel
128 | */
129 | // @Composable
130 | // inline fun rememberPresenter(
131 | // crossinline body: @Composable (flow: Flow) -> T
132 | // ): Pair> {
133 | // return rememberPresenter(keys = listOf(T::class, E::class)) {
134 | // body.invoke(it)
135 | // }
136 | // }
137 |
138 | /**
139 | * Return pair of State and Action channel, use it in your Presenter, not Compose UI
140 | *
141 | * @param body The body of the molecule presenter, the flow parameter is the flow of the action channel
142 | * @return Pair of State and Action channel
143 | */
144 | @Composable
145 | fun rememberNestedPresenter(
146 | body: @Composable (flow: Flow) -> T,
147 | ): Pair> {
148 | val channel = remember { Channel(Channel.UNLIMITED) }
149 | val flow = remember { channel.consumeAsFlow() }
150 | val presenter = body(flow)
151 | return presenter to channel
152 | }
153 |
154 | /**
155 | * Helper function to collect the action channel in your Presenter
156 | *
157 | * @param body Your action handler
158 | */
159 | @Composable
160 | fun Flow.collectAction(
161 | body: suspend T.() -> Unit,
162 | ) {
163 | LaunchedEffect(Unit) {
164 | collect {
165 | body(it)
166 | }
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/precompose-molecule/src/iosMain/kotlin/moe/tlaster/precompose/molecule/ProvidePlatformDispatcher.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.molecule
2 |
3 | import app.cash.molecule.DisplayLinkClock
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlin.coroutines.CoroutineContext
6 |
7 | internal actual fun providePlatformDispatcher(): CoroutineContext = DisplayLinkClock + Dispatchers.Main
8 |
--------------------------------------------------------------------------------
/precompose-molecule/src/jsMain/kotlin/moe/tlaster/precompose/molecule/ProvidePlatformDispatcher.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.molecule
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlin.coroutines.CoroutineContext
5 |
6 | internal actual fun providePlatformDispatcher(): CoroutineContext = Dispatchers.Main
7 |
--------------------------------------------------------------------------------
/precompose-molecule/src/jvmMain/kotlin/moe/tlaster/precompose/molecule/ProvidePlatformDispatcher.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.molecule
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlin.coroutines.CoroutineContext
5 |
6 | internal actual fun providePlatformDispatcher(): CoroutineContext = Dispatchers.Main
7 |
--------------------------------------------------------------------------------
/precompose-molecule/src/macosMain/kotlin/moe/tlaster/precompose/molecule/ProvidePlatformDispatcher.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.molecule
2 |
3 | import app.cash.molecule.DisplayLinkClock
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlin.coroutines.CoroutineContext
6 |
7 | internal actual fun providePlatformDispatcher(): CoroutineContext = Dispatchers.Main + DisplayLinkClock
8 |
--------------------------------------------------------------------------------
/precompose-molecule/src/wasmJsMain/kotlin/moe/tlaster/precompose/molecule/ProvidePlatformDispatcher.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.molecule
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlin.coroutines.CoroutineContext
5 |
6 | internal actual fun providePlatformDispatcher(): CoroutineContext = Dispatchers.Main
7 |
--------------------------------------------------------------------------------
/precompose/src/androidMain/kotlin/moe/tlaster/precompose/PreComposeApp.android.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose
2 |
3 | import androidx.activity.BackEventCompat
4 | import androidx.activity.OnBackPressedCallback
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.CompositionLocalProvider
7 | import androidx.compose.runtime.DisposableEffect
8 | import androidx.compose.runtime.LaunchedEffect
9 | import androidx.compose.runtime.collectAsState
10 | import androidx.compose.runtime.getValue
11 | import androidx.lifecycle.DefaultLifecycleObserver
12 | import androidx.lifecycle.compose.LocalLifecycleOwner
13 | import moe.tlaster.precompose.ui.BackDispatcher
14 | import moe.tlaster.precompose.ui.BackDispatcherOwner
15 | import moe.tlaster.precompose.ui.LocalBackDispatcherOwner
16 |
17 | @Composable
18 | actual fun PreComposeApp(
19 | content: @Composable () -> Unit,
20 | ) {
21 | val viewModel = androidx.lifecycle.viewmodel.compose.viewModel()
22 |
23 | val lifecycle = LocalLifecycleOwner.current.lifecycle
24 | val onBackPressedDispatcher = checkNotNull(androidx.activity.compose.LocalOnBackPressedDispatcherOwner.current) {
25 | "No OnBackPressedDispatcherOwner was provided via LocalOnBackPressedDispatcherOwner"
26 | }.onBackPressedDispatcher
27 |
28 | DisposableEffect(lifecycle) {
29 | val observer = object : DefaultLifecycleObserver {
30 | override fun onCreate(owner: androidx.lifecycle.LifecycleOwner) {
31 | super.onCreate(owner)
32 | onBackPressedDispatcher.addCallback(owner, viewModel.backPressedCallback)
33 | }
34 | }
35 | lifecycle.addObserver(observer)
36 | onDispose {
37 | lifecycle.removeObserver(observer)
38 | }
39 | }
40 |
41 | val state by viewModel.backDispatcher.canHandleBackPress.collectAsState(false)
42 |
43 | LaunchedEffect(state) {
44 | viewModel.backPressedCallback.isEnabled = state
45 | }
46 | CompositionLocalProvider(
47 | LocalBackDispatcherOwner provides viewModel,
48 | ) {
49 | content.invoke()
50 | }
51 | }
52 |
53 | internal class PreComposeViewModel :
54 | androidx.lifecycle.ViewModel(),
55 | BackDispatcherOwner {
56 | override val backDispatcher by lazy {
57 | BackDispatcher()
58 | }
59 |
60 | val backPressedCallback = object : OnBackPressedCallback(true) {
61 | override fun handleOnBackPressed() {
62 | backDispatcher.onBackPress()
63 | }
64 |
65 | override fun handleOnBackStarted(backEvent: BackEventCompat) {
66 | backDispatcher.onBackStarted()
67 | }
68 |
69 | override fun handleOnBackProgressed(backEvent: BackEventCompat) {
70 | backDispatcher.onBackProgressed(backEvent.progress)
71 | }
72 |
73 | override fun handleOnBackCancelled() {
74 | backDispatcher.onBackCancelled()
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/precompose/src/androidMain/kotlin/moe/tlaster/precompose/reflect/KClass.android.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.reflect
2 |
3 | import kotlin.reflect.KClass
4 |
5 | actual val KClass.canonicalName: String?
6 | get() = this.qualifiedName
7 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/PreComposeApp.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | @Composable
6 | expect fun PreComposeApp(
7 | content: @Composable () -> Unit = {},
8 | )
9 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/BackHandler.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.DisposableEffect
5 | import androidx.compose.runtime.SideEffect
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.runtime.rememberCoroutineScope
9 | import androidx.compose.runtime.rememberUpdatedState
10 | import androidx.lifecycle.compose.LocalLifecycleOwner
11 | import kotlinx.coroutines.CancellationException
12 | import kotlinx.coroutines.CoroutineScope
13 | import kotlinx.coroutines.channels.BufferOverflow
14 | import kotlinx.coroutines.channels.Channel
15 | import kotlinx.coroutines.channels.Channel.Factory.BUFFERED
16 | import kotlinx.coroutines.flow.Flow
17 | import kotlinx.coroutines.flow.consumeAsFlow
18 | import kotlinx.coroutines.launch
19 | import moe.tlaster.precompose.ui.BackDispatcher
20 | import moe.tlaster.precompose.ui.BackHandler
21 | import moe.tlaster.precompose.ui.DefaultBackHandler
22 | import moe.tlaster.precompose.ui.LocalBackDispatcherOwner
23 |
24 | @Composable
25 | fun BackHandler(enabled: Boolean = true, onBack: () -> Unit) {
26 | // Safely update the current `onBack` lambda when a new one is provided
27 | val backDispatcher = checkNotNull(LocalBackDispatcherOwner.current) {
28 | "No OnBackPressedDispatcherOwner was provided via LocalOnBackPressedDispatcherOwner"
29 | }.backDispatcher
30 | val currentOnBack by rememberUpdatedState(onBack)
31 | // Remember in Composition a back callback that calls the `onBack` lambda
32 | val backCallback = remember { DefaultBackHandler(enabled) { currentOnBack.invoke() } }
33 | // On every successful composition, update the callback with the `enabled` value
34 | SideEffect {
35 | if (backCallback.isEnabled != enabled) {
36 | backDispatcher.onBackStackChanged()
37 | }
38 | backCallback.isEnabled = enabled
39 | }
40 | val lifecycleOwner = LocalLifecycleOwner.current
41 | DisposableEffect(lifecycleOwner, backDispatcher) {
42 | // Add callback to the backDispatcher
43 | backDispatcher.register(backCallback)
44 | // When the effect leaves the Composition, remove the callback
45 | onDispose {
46 | backDispatcher.unregister(backCallback)
47 | }
48 | }
49 | }
50 |
51 | /**
52 | * An effect for handling predictive system back gestures.
53 | *
54 | * Calling this in your composable adds the given lambda to the [BackDispatcher] of the
55 | * [LocalBackDispatcherOwner]. The lambda passes in a Flow where each
56 | * [Float] reflects the progress of current gesture back. The lambda content should
57 | * follow this structure:
58 | *
59 | * ```
60 | * PredictiveBackHandler { progress: Flow ->
61 | * // code for gesture back started
62 | * try {
63 | * progress.collect { progress ->
64 | * // code for progress
65 | * }
66 | * // code for completion
67 | * } catch (e: CancellationException) {
68 | * // code for cancellation
69 | * }
70 | * }
71 | * ```
72 | *
73 | * If this is called by nested composables, if enabled, the inner most composable will consume
74 | * the call to system back and invoke its lambda. The call will continue to propagate up until it
75 | * finds an enabled BackHandler.
76 | *
77 | * @param enabled if this BackHandler should be enabled, true by default
78 | * @param onBack the action invoked by back gesture
79 | */
80 | @Composable
81 | fun PredictiveBackHandler(
82 | enabled: Boolean = true,
83 | onBack: suspend (progress: Flow) -> Unit,
84 | ) {
85 | // ensure we don't re-register callbacks when onBack changes
86 | val currentOnBack by rememberUpdatedState(onBack)
87 | val onBackScope = rememberCoroutineScope()
88 |
89 | val backCallback = remember {
90 | object : BackHandler {
91 | override var isEnabled: Boolean = enabled
92 | var onBackInstance: OnBackInstance? = null
93 |
94 | override fun handleBackStarted() {
95 | // in case the previous onBackInstance was started by a normal back gesture
96 | // we want to make sure it's still cancelled before we start a predictive
97 | // back gesture
98 | onBackInstance?.cancel()
99 | onBackInstance = OnBackInstance(onBackScope, true, currentOnBack)
100 | }
101 |
102 | override fun handleBackProgressed(progress: Float) {
103 | onBackInstance?.send(progress)
104 | }
105 |
106 | override fun handleBackPress() {
107 | // handleOnBackPressed could be called by regular back to restart
108 | // a new back instance. If this is the case (where current back instance
109 | // was NOT started by handleOnBackStarted) then we need to reset the previous
110 | // regular back.
111 | onBackInstance?.apply {
112 | if (!isPredictiveBack) {
113 | cancel()
114 | onBackInstance = null
115 | }
116 | }
117 | if (onBackInstance == null) {
118 | onBackInstance = OnBackInstance(onBackScope, false, currentOnBack)
119 | }
120 |
121 | // finally, we close the channel to ensure no more events can be sent
122 | // but let the job complete normally
123 | onBackInstance?.close()
124 | }
125 |
126 | override fun handleBackCancelled() {
127 | // cancel will purge the channel of any sent events that are yet to be received
128 | onBackInstance?.cancel()
129 | }
130 | }
131 | }
132 |
133 | val backDispatcher = checkNotNull(LocalBackDispatcherOwner.current) {
134 | "No OnBackPressedDispatcherOwner was provided via LocalOnBackPressedDispatcherOwner"
135 | }.backDispatcher
136 |
137 | SideEffect {
138 | if (backCallback.isEnabled != enabled) {
139 | backDispatcher.onBackStackChanged()
140 | }
141 | backCallback.isEnabled = enabled
142 | }
143 | val lifecycleOwner = LocalLifecycleOwner.current
144 |
145 | DisposableEffect(lifecycleOwner, backDispatcher) {
146 | backDispatcher.register(backCallback)
147 |
148 | onDispose {
149 | backDispatcher.unregister(backCallback)
150 | }
151 | }
152 | }
153 |
154 | private class OnBackInstance(
155 | scope: CoroutineScope,
156 | val isPredictiveBack: Boolean,
157 | onBack: suspend (progress: Flow) -> Unit,
158 | ) {
159 | val channel = Channel(capacity = BUFFERED, onBufferOverflow = BufferOverflow.SUSPEND)
160 | val job = scope.launch {
161 | onBack(channel.consumeAsFlow())
162 | }
163 |
164 | fun send(backEvent: Float) = channel.trySend(backEvent)
165 |
166 | // idempotent if invoked more than once
167 | fun close() = channel.close()
168 |
169 | fun cancel() {
170 | channel.cancel(CancellationException("onBack cancelled"))
171 | job.cancel()
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/BackStackEntry.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import androidx.lifecycle.LifecycleOwner
5 | import androidx.lifecycle.LifecycleRegistry
6 | import androidx.lifecycle.ViewModelStoreOwner
7 | import moe.tlaster.precompose.navigation.route.GroupRoute
8 | import moe.tlaster.precompose.navigation.route.Route
9 | import moe.tlaster.precompose.navigation.route.toSceneRoute
10 | import moe.tlaster.precompose.navigation.transition.NavTransition
11 |
12 | class BackStackEntry internal constructor(
13 | internal val stateId: String,
14 | internal var routeInternal: Route,
15 | val path: String,
16 | val pathMap: Map,
17 | private val provider: ViewModelStoreProvider,
18 | val queryString: QueryString? = null,
19 | ) : LifecycleOwner,
20 | ViewModelStoreOwner {
21 | val route: Route
22 | get() = routeInternal
23 | internal var uiClosable: UiClosable? = null
24 | private var _destroyAfterTransition = false
25 | internal val swipeProperties: SwipeProperties?
26 | get() = route.toSceneRoute()?.swipeProperties
27 |
28 | internal val navTransition: NavTransition?
29 | get() = route.toSceneRoute()?.navTransition
30 |
31 | private val lifecycleRegistry by lazy {
32 | LifecycleRegistry(this)
33 | }
34 |
35 | override val lifecycle: Lifecycle
36 | get() = lifecycleRegistry
37 |
38 | init {
39 | lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
40 | }
41 |
42 | fun active() {
43 | lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
44 | }
45 |
46 | fun inActive() {
47 | lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
48 | if (_destroyAfterTransition) {
49 | destroy()
50 | }
51 | }
52 |
53 | fun destroy() {
54 | if (lifecycleRegistry.currentState.isAtLeast(Lifecycle.State.STARTED)) {
55 | _destroyAfterTransition = true
56 | } else {
57 | destroyDirectly()
58 | }
59 | }
60 |
61 | internal fun destroyDirectly() {
62 | lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
63 | provider.clear(stateId)
64 | uiClosable?.close(stateId)
65 | }
66 |
67 | fun hasRoute(route: String): Boolean {
68 | return this.route.route == route || (this.route as? GroupRoute)?.hasRoute(route) == true
69 | }
70 |
71 | fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
72 | when (event) {
73 | Lifecycle.Event.ON_DESTROY -> {
74 | destroy()
75 | }
76 | else -> {
77 | lifecycleRegistry.handleLifecycleEvent(event)
78 | }
79 | }
80 | }
81 |
82 | override val viewModelStore by lazy {
83 | provider.getViewModelStore(stateId)
84 | }
85 | }
86 |
87 | internal fun BackStackEntry.hasRoute(route: String, path: String, includePath: Boolean): Boolean {
88 | return if (includePath) {
89 | hasRoute(route = route) && this.path == path
90 | } else {
91 | hasRoute(route = route)
92 | }
93 | }
94 |
95 | inline fun BackStackEntry.path(path: String, default: T? = null): T? {
96 | val value = pathMap[path] ?: return default
97 | return convertValue(value)
98 | }
99 |
100 | inline fun BackStackEntry.query(name: String, default: T? = null): T? {
101 | return queryString?.query(name, default)
102 | }
103 |
104 | inline fun BackStackEntry.queryList(name: String): List {
105 | val value = queryString?.map?.get(name) ?: return emptyList()
106 | return value.map { convertValue(it) }
107 | }
108 |
109 | inline fun convertValue(value: String): T? {
110 | return when (T::class) {
111 | Int::class -> value.toIntOrNull()
112 | Long::class -> value.toLongOrNull()
113 | String::class -> value
114 | Boolean::class -> value.toBooleanStrictOrNull()
115 | Float::class -> value.toFloatOrNull()
116 | Double::class -> value.toDoubleOrNull()
117 | else -> throw NotImplementedError()
118 | } as T
119 | }
120 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/NavControllerViewModel.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import androidx.lifecycle.ViewModelStore
6 | import androidx.lifecycle.get
7 | import androidx.lifecycle.viewmodel.CreationExtras
8 | import kotlin.reflect.KClass
9 |
10 | internal class NavControllerViewModel : ViewModel(), ViewModelStoreProvider {
11 | private val viewModelStores = mutableMapOf()
12 |
13 | override fun clear(backStackEntryId: String) {
14 | // Clear and remove the NavGraph's ViewModelStore
15 | val viewModelStore = viewModelStores.remove(backStackEntryId)
16 | viewModelStore?.clear()
17 | }
18 |
19 | override fun onCleared() {
20 | for (store in viewModelStores.values) {
21 | store.clear()
22 | }
23 | viewModelStores.clear()
24 | }
25 |
26 | override fun getViewModelStore(backStackEntryId: String): ViewModelStore {
27 | var viewModelStore = viewModelStores[backStackEntryId]
28 | if (viewModelStore == null) {
29 | viewModelStore = ViewModelStore()
30 | viewModelStores[backStackEntryId] = viewModelStore
31 | }
32 | return viewModelStore
33 | }
34 |
35 | companion object {
36 | private val FACTORY: ViewModelProvider.Factory =
37 | object : ViewModelProvider.Factory {
38 | @Suppress("UNCHECKED_CAST")
39 | override fun create(
40 | modelClass: KClass,
41 | extras: CreationExtras,
42 | ): T {
43 | return NavControllerViewModel() as T
44 | }
45 | }
46 |
47 | fun getInstance(viewModelStore: ViewModelStore): NavControllerViewModel {
48 | val viewModelProvider = ViewModelProvider.create(viewModelStore, FACTORY)
49 | return viewModelProvider.get()
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/NavOptions.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | /**
4 | * [NavOptions] stores special options for navigate actions
5 | */
6 | data class NavOptions(
7 | /**
8 | * Whether this navigation action should launch as single-top (i.e., there will be at most
9 | * one copy of a given destination on the top of the back stack regardless of path parameters).
10 | * To include path parameters see [includePath].
11 | */
12 | val launchSingleTop: Boolean = false,
13 |
14 | /**
15 | * [includePath] overrides the default [launchSingleTop] behavior allowing
16 | * single-top launch of destinations with variable path parameters.
17 | *
18 | * This override has no effect when [launchSingleTop] is false, and it is disabled by default.
19 | */
20 | val includePath: Boolean = false,
21 | /**
22 | * The destination to pop up to before navigating. When set, all non-matching destinations
23 | * should be popped from the back stack.
24 | * @see [PopUpTo]
25 | */
26 | val popUpTo: PopUpTo = PopUpTo.None,
27 | )
28 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/Navigator.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.lifecycle.LifecycleOwner
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.ViewModelStoreOwner
7 | import androidx.lifecycle.viewmodel.compose.viewModel
8 | import kotlinx.coroutines.flow.map
9 |
10 | /**
11 | * Creates or returns an existing [Navigator] that controls the [NavHost].
12 | * @param name: Identify the navigator so you can have as many navigator instances as you need.
13 | * @return Returns an instance of Navigator.
14 | */
15 | @Composable
16 | fun rememberNavigator(key: String? = null): Navigator {
17 | val viewModel = viewModel(key = key) {
18 | NavigatorViewModel()
19 | }
20 | return viewModel.navigator
21 | }
22 |
23 | internal class NavigatorViewModel : ViewModel() {
24 | val navigator by lazy {
25 | Navigator()
26 | }
27 | }
28 |
29 | class Navigator {
30 | // FIXME: 2021/4/1 Temp workaround for deeplink
31 | private var _pendingNavigation: String? = null
32 | private var _initialized = false
33 | internal val stackManager = BackStackManager()
34 |
35 | /**
36 | * Initializes the navigator with a set parameters.
37 | * @param lifecycleOwner: lifecycleOwner object
38 | * @param viewModelStoreOwner: viewModelStoreOwner object
39 | */
40 | internal fun init(
41 | lifecycleOwner: LifecycleOwner,
42 | viewModelStoreOwner: ViewModelStoreOwner,
43 | ) {
44 | if (_initialized) {
45 | return
46 | }
47 | _initialized = true
48 | stackManager.init(
49 | lifecycleOwner = lifecycleOwner,
50 | viewModelStoreOwner = viewModelStoreOwner,
51 | )
52 | }
53 |
54 | /**
55 | * Set the RouteGraph for the navigator.
56 | * @param routeGraph: destination's navigation graph
57 | */
58 | internal fun setRouteGraph(routeGraph: RouteGraph) {
59 | stackManager.setRouteGraph(routeGraph)
60 | _pendingNavigation?.let {
61 | stackManager.push(it)
62 | _pendingNavigation = null
63 | }
64 | }
65 |
66 | /**
67 | * Navigate to a route in the current RouteGraph.
68 | * @param route: route for the destination
69 | * @param options: navigation options for the destination
70 | */
71 | fun navigate(route: String, options: NavOptions? = null) {
72 | if (!_initialized) {
73 | _pendingNavigation = route
74 | return
75 | }
76 |
77 | stackManager.push(route, options)
78 | }
79 |
80 | /**
81 | * Navigate to a route in the current RouteGraph and wait for result.
82 | * @param route: route for the destination
83 | * @param options: navigation options for the destination
84 | * @return: result from the destination
85 | */
86 | suspend fun navigateForResult(route: String, options: NavOptions? = null): Any? {
87 | if (!_initialized) {
88 | _pendingNavigation = route
89 | return null
90 | }
91 | return stackManager.pushForResult(route, options)
92 | }
93 |
94 | /**
95 | * Attempts to navigate up in the navigation hierarchy.
96 | */
97 | fun goBack() {
98 | if (!_initialized) {
99 | return
100 | }
101 | stackManager.pop()
102 | }
103 |
104 | /**
105 | * Go back with a specific result.
106 | * @param result: result to be returned when moved back.
107 | */
108 | fun goBackWith(result: Any? = null) {
109 | if (!_initialized) {
110 | return
111 | }
112 | stackManager.pop(result)
113 | }
114 |
115 | /**
116 | * Go back to a specific destination.
117 | * @param popUpTo: the destination to pop back to.
118 | */
119 | fun goBack(
120 | popUpTo: PopUpTo,
121 | ) {
122 | if (!_initialized) {
123 | return
124 | }
125 | stackManager.popWithOptions(popUpTo)
126 | }
127 |
128 | /**
129 | * Compatibility layer for Jetpack Navigation.
130 | */
131 | fun popBackStack() {
132 | if (!_initialized) {
133 | return
134 | }
135 | goBack()
136 | }
137 |
138 | /**
139 | * Check if navigator can navigate up
140 | * @return Returns true if navigator can navigate up, false otherwise.
141 | */
142 | val canGoBack = stackManager.canGoBack
143 |
144 | /**
145 | * Current route
146 | * @ return Returns the current navigation back stack entry.
147 | */
148 | val currentEntry = stackManager.currentBackStackEntry
149 |
150 | /**
151 | * Previous route
152 | * @ return Returns the previous navigation back stack entry.
153 | */
154 | val previousEntry = stackManager.prevBackStackEntry
155 |
156 | /**
157 | * Number of routes in the back stack
158 | */
159 | val backStackCount = stackManager.backStacks.map { it.size }
160 | }
161 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/PopUpTo.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | sealed interface PopUpTo {
4 |
5 | /**
6 | * Whether the `popUpTo` destination should be popped from the back stack.
7 | */
8 | val inclusive: Boolean get() = false
9 |
10 | /**
11 | * do nothing
12 | */
13 | object None : PopUpTo
14 |
15 | /**
16 | * pop prev back stack
17 | */
18 | object Prev : PopUpTo {
19 | override val inclusive: Boolean get() = true
20 | }
21 |
22 | /**
23 | * The `popUpTo` destination, if it's an empty string will clear all backstack
24 | * @param route the route to pop up to
25 | * @param inclusive whether the `popUpTo` destination should be popped from the back stack.
26 | */
27 | data class Route(
28 | val route: String,
29 | override val inclusive: Boolean,
30 | ) : PopUpTo
31 |
32 | companion object {
33 |
34 | /**
35 | * popUpTo first back stack
36 | * @param inclusive whether the `popUpTo` destination should be popped from the back stack.
37 | */
38 | @Suppress("FunctionName")
39 | fun First(inclusive: Boolean = true): PopUpTo = Route("", inclusive)
40 | }
41 | }
42 |
43 | /**
44 | * The `popUpTo` destination, if it's an empty string will clear all backstack
45 | * @param route the route to pop up to
46 | * @param inclusive whether the `popUpTo` destination should be popped from the back stack.
47 | */
48 | @Suppress("FunctionName")
49 | fun PopUpTo(route: String, inclusive: Boolean = false) = PopUpTo.Route(
50 | route = route,
51 | inclusive = inclusive,
52 | )
53 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/QueryString.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | data class QueryString(
4 | private val rawInput: String,
5 | ) {
6 | val map by lazy {
7 | rawInput
8 | .substringAfter("?")
9 | .splitToSequence("&")
10 | .map { it.split("=") }
11 | .filter { it.size in 1..2 && it[0].isNotEmpty() }
12 | .groupBy({ it[0] }, { it.getOrNull(1) })
13 | .map { it -> it.key to it.value.mapNotNull { it?.takeIf { it.isNotEmpty() } } }
14 | .toMap()
15 | }
16 | }
17 |
18 | inline fun QueryString.query(name: String, default: T? = null): T? {
19 | val value = map[name]?.firstOrNull() ?: return default
20 | return convertValue(value)
21 | }
22 |
23 | inline fun QueryString.queryList(name: String): List {
24 | val value = map[name] ?: return emptyList()
25 | return value.mapNotNull { convertValue(it) }
26 | }
27 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/RouteBuilder.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import androidx.compose.animation.AnimatedContentScope
4 | import androidx.compose.runtime.Composable
5 | import moe.tlaster.precompose.navigation.route.GroupRoute
6 | import moe.tlaster.precompose.navigation.route.Route
7 | import moe.tlaster.precompose.navigation.route.SceneRoute
8 | import moe.tlaster.precompose.navigation.route.floatingRouteWithoutAnimatedContent
9 | import moe.tlaster.precompose.navigation.route.sceneRouteWithoutAnimatedContent
10 | import moe.tlaster.precompose.navigation.transition.NavTransition
11 |
12 | class RouteBuilder(
13 | private val initialRoute: String,
14 | ) {
15 | private val route = arrayListOf()
16 |
17 | /**
18 | * Add the scene [Composable] to the [RouteBuilder]
19 | * @param route route for the destination
20 | * @param navTransition navigation transition for current scene
21 | * @param swipeProperties swipe back navigation properties for current scene
22 | * @param content composable for the destination
23 | */
24 | @Deprecated(
25 | message = "Deprecated in favor of scene that supports AnimatedContent",
26 | level = DeprecationLevel.HIDDEN,
27 | )
28 | fun scene(
29 | route: String,
30 | deepLinks: List = emptyList(),
31 | navTransition: NavTransition? = null,
32 | swipeProperties: SwipeProperties? = null,
33 | content: @Composable (BackStackEntry) -> Unit,
34 | ) {
35 | addRoute(
36 | @Suppress("DEPRECATION")
37 | sceneRouteWithoutAnimatedContent(
38 | route = route,
39 | navTransition = navTransition,
40 | deepLinks = deepLinks,
41 | swipeProperties = swipeProperties,
42 | content = content,
43 | ),
44 | )
45 | }
46 |
47 | /**
48 | * Add the scene [Composable] to the [RouteBuilder]
49 | * @param route route for the destination
50 | * @param navTransition navigation transition for current scene
51 | * @param swipeProperties swipe back navigation properties for current scene
52 | * @param content composable for the destination. The AnimatedContentScope provided is the
53 | * animation that drives the scene transition. That is either entering or exiting the NavHost
54 | */
55 | fun scene(
56 | route: String,
57 | deepLinks: List = emptyList(),
58 | navTransition: NavTransition? = null,
59 | swipeProperties: SwipeProperties? = null,
60 | content: @Composable AnimatedContentScope.(BackStackEntry) -> Unit,
61 | ) {
62 | addRoute(
63 | SceneRoute(
64 | route = route,
65 | navTransition = navTransition,
66 | deepLinks = deepLinks,
67 | swipeProperties = swipeProperties,
68 | content = content,
69 | ),
70 | )
71 | }
72 |
73 | /**
74 | * Add a group of [Composable] to the [RouteBuilder]
75 | * @param route route for the destination
76 | * @param initialRoute initial route for the group
77 | * @param content composable for the destination
78 | */
79 | fun group(
80 | route: String,
81 | initialRoute: String,
82 | content: RouteBuilder.() -> Unit,
83 | ) {
84 | require(!route.contains("{")) { "GroupRoute does not support path matching" }
85 | require(!initialRoute.contains("{")) { "GroupRoute does not support path matching" }
86 | content.invoke(this)
87 | val actualInitialRoute = this.route.firstOrNull { it.route == initialRoute }
88 | ?: throw IllegalArgumentException("Initial route $initialRoute not found")
89 | addRoute(
90 | GroupRoute(
91 | route = route,
92 | initialRoute = actualInitialRoute,
93 | ),
94 | )
95 | }
96 |
97 | /**
98 | * Add the dialog [Composable] to the [RouteBuilder], which will show over the scene
99 | * @param route route for the destination
100 | * @param content composable for the destination
101 | */
102 | fun dialog(
103 | route: String,
104 | content: @Composable (BackStackEntry) -> Unit,
105 | ) {
106 | floating(
107 | route,
108 | content,
109 | )
110 | }
111 |
112 | /**
113 | * Add the floating [Composable] to the [RouteBuilder], which will show over the scene
114 | * @param route route for the destination
115 | * @param content composable for the destination
116 | */
117 | fun floating(
118 | route: String,
119 | content: @Composable (BackStackEntry) -> Unit,
120 | ) {
121 | addRoute(
122 | @Suppress("DEPRECATION")
123 | floatingRouteWithoutAnimatedContent(
124 | route = route,
125 | content = content,
126 | ),
127 | )
128 | }
129 |
130 | fun addRoute(route: Route) {
131 | this.route += route
132 | }
133 |
134 | @Suppress("ControlFlowWithEmptyBody")
135 | internal fun build(): RouteGraph {
136 | if (initialRoute.isEmpty() && route.isEmpty()) {
137 | // FIXME: 2021/4/2 Show warning
138 | }
139 | require(!route.groupBy { it.route }.any { it.value.size > 1 }) {
140 | "Duplicate route can not be applied"
141 | }
142 | return RouteGraph(initialRoute, route.toList())
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/RouteGraph.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import moe.tlaster.precompose.navigation.route.Route
4 |
5 | internal data class RouteGraph(
6 | val initialRoute: String,
7 | val routes: List,
8 | ) {
9 | override fun equals(other: Any?): Boolean {
10 | if (other !is RouteGraph) {
11 | return false
12 | }
13 | return initialRoute == other.initialRoute && routes.size == other.routes.size && routes.map { it.route }
14 | .containsAll(other.routes.map { it.route })
15 | }
16 |
17 | override fun hashCode(): Int {
18 | var result = initialRoute.hashCode()
19 | result = 31 * result + routes.hashCode()
20 | return result
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/RouteMatch.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import moe.tlaster.precompose.navigation.route.ComposeRoute
4 | import kotlin.math.min
5 |
6 | internal class RouteMatch {
7 | var matches = false
8 | var route: ComposeRoute? = null
9 | var vars = arrayListOf()
10 | var keys = arrayListOf()
11 | var pathMap = linkedMapOf()
12 | fun key() {
13 | val size = min(keys.size, vars.size)
14 | for (i in 0 until size) {
15 | pathMap[keys[i]] = vars[i]
16 | }
17 | for (i in 0 until size) {
18 | vars.removeFirst()
19 | }
20 | }
21 |
22 | fun truncate(size: Int) {
23 | var sizeInt = size
24 | while (sizeInt < vars.size) {
25 | vars.removeAt(sizeInt++)
26 | }
27 | }
28 |
29 | fun value(value: String) {
30 | vars.add(value)
31 | }
32 |
33 | fun pop() {
34 | if (vars.isNotEmpty()) {
35 | vars.removeLast()
36 | }
37 | }
38 |
39 | fun found(route: ComposeRoute): RouteMatch {
40 | this.route = route
41 | matches = true
42 | return this
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/RouteMatchResult.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import moe.tlaster.precompose.navigation.route.Route
4 |
5 | internal data class RouteMatchResult(
6 | val route: Route,
7 | val pathMap: Map = emptyMap(),
8 | )
9 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/SwipeProperties.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import androidx.compose.ui.unit.Density
4 | import androidx.compose.ui.unit.dp
5 |
6 | class SwipeProperties(
7 | val positionalThreshold: (totalDistance: Float) -> Float = { distance: Float -> distance * 0.5f },
8 | val velocityThreshold: Density.() -> Float = { 56.dp.toPx() },
9 | )
10 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/UiClosable.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import androidx.compose.runtime.saveable.SaveableStateHolder
4 |
5 | interface UiClosable {
6 | fun close(stateId: String)
7 | }
8 |
9 | internal class ComposeUiClosable(
10 | val composeSaveableStateHolder: SaveableStateHolder,
11 | ) : UiClosable {
12 | override fun close(stateId: String) {
13 | composeSaveableStateHolder.removeState(stateId)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/ViewModelStoreProvider.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import androidx.lifecycle.ViewModelStore
4 |
5 | interface ViewModelStoreProvider {
6 | fun getViewModelStore(backStackEntryId: String): ViewModelStore
7 | fun clear(backStackEntryId: String)
8 | }
9 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/route/ComposeRoute.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation.route
2 |
3 | import androidx.compose.animation.AnimatedContentScope
4 | import androidx.compose.runtime.Composable
5 | import moe.tlaster.precompose.navigation.BackStackEntry
6 |
7 | interface ComposeRoute : Route {
8 | val content: @Composable AnimatedContentScope.(BackStackEntry) -> Unit
9 | }
10 |
11 | interface ComposeSceneRoute : ComposeRoute
12 |
13 | interface ComposeFloatingRoute : ComposeRoute
14 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/route/FloatingRoute.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation.route
2 |
3 | import androidx.compose.animation.AnimatedContentScope
4 | import androidx.compose.runtime.Composable
5 | import moe.tlaster.precompose.navigation.BackStackEntry
6 |
7 | internal class FloatingRoute(
8 | override val content: @Composable AnimatedContentScope.(BackStackEntry) -> Unit,
9 | override val route: String,
10 | ) : ComposeRoute, ComposeFloatingRoute
11 |
12 | @Deprecated(
13 | message = """
14 | Used as a backwards compatible for the old RouteBuilder APIs which do not expect the content to
15 | be an extension function on AnimatedContentScope
16 | """,
17 | level = DeprecationLevel.WARNING,
18 | )
19 | internal fun floatingRouteWithoutAnimatedContent(
20 | route: String,
21 | content: @Composable (BackStackEntry) -> Unit,
22 | ): FloatingRoute {
23 | return FloatingRoute(
24 | route = route,
25 | content = { entry -> content(entry) },
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/route/GroupRoute.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation.route
2 |
3 | internal data class GroupRoute(
4 | override val route: String,
5 | val initialRoute: Route,
6 | ) : Route {
7 | fun hasRoute(route: String): Boolean {
8 | if (this.route == route) {
9 | return true
10 | }
11 | return if (initialRoute is GroupRoute) {
12 | initialRoute.hasRoute(route)
13 | } else {
14 | initialRoute.route == route
15 | }
16 | }
17 | }
18 |
19 | internal fun GroupRoute.isSceneRoute(): Boolean = if (initialRoute is GroupRoute) {
20 | initialRoute.isSceneRoute()
21 | } else {
22 | initialRoute is SceneRoute
23 | }
24 | internal fun Route.isSceneRoute(): Boolean {
25 | return this is SceneRoute || this is GroupRoute && this.isSceneRoute()
26 | }
27 |
28 | internal fun GroupRoute.toSceneRoute(): SceneRoute? = if (initialRoute is GroupRoute) {
29 | initialRoute.toSceneRoute()
30 | } else {
31 | initialRoute as? SceneRoute
32 | }
33 |
34 | internal fun Route.toSceneRoute(): SceneRoute? {
35 | return this as? SceneRoute ?: (this as? GroupRoute)?.toSceneRoute()
36 | }
37 |
38 | internal fun GroupRoute.isFloatingRoute(): Boolean = if (initialRoute is GroupRoute) {
39 | initialRoute.isFloatingRoute()
40 | } else {
41 | initialRoute is FloatingRoute
42 | }
43 | internal fun Route.isFloatingRoute(): Boolean {
44 | return this is FloatingRoute || this is GroupRoute && this.isFloatingRoute()
45 | }
46 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/route/Route.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation.route
2 |
3 | interface Route {
4 | val route: String
5 | }
6 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/route/SceneRoute.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation.route
2 |
3 | import androidx.compose.animation.AnimatedContentScope
4 | import androidx.compose.runtime.Composable
5 | import moe.tlaster.precompose.navigation.BackStackEntry
6 | import moe.tlaster.precompose.navigation.SwipeProperties
7 | import moe.tlaster.precompose.navigation.transition.NavTransition
8 |
9 | internal class SceneRoute(
10 | override val route: String,
11 | val deepLinks: List,
12 | val navTransition: NavTransition?,
13 | val swipeProperties: SwipeProperties?,
14 | override val content: @Composable AnimatedContentScope.(BackStackEntry) -> Unit,
15 | ) : ComposeRoute, ComposeSceneRoute
16 |
17 | @Deprecated(
18 | message = """
19 | Used as a backwards compatible for the old RouteBuilder APIs which do not expect the content to
20 | be an extension function on AnimatedContentScope
21 | """,
22 | level = DeprecationLevel.WARNING,
23 | )
24 | internal fun sceneRouteWithoutAnimatedContent(
25 | route: String,
26 | deepLinks: List,
27 | navTransition: NavTransition?,
28 | swipeProperties: SwipeProperties?,
29 | content: @Composable (BackStackEntry) -> Unit,
30 | ): SceneRoute {
31 | return SceneRoute(
32 | route,
33 | deepLinks,
34 | navTransition,
35 | swipeProperties,
36 | content = { entry -> content(entry) },
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/navigation/transition/NavTransition.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation.transition
2 |
3 | import androidx.compose.animation.EnterTransition
4 | import androidx.compose.animation.ExitTransition
5 | import androidx.compose.animation.ExperimentalAnimationApi
6 | import androidx.compose.animation.fadeIn
7 | import androidx.compose.animation.fadeOut
8 | import androidx.compose.animation.scaleIn
9 | import androidx.compose.animation.scaleOut
10 |
11 | /**
12 | * Create a navigation transition
13 | */
14 | interface NavTransition {
15 | /**
16 | * Transition the scene that about to appear for the first time, similar to activity onCreate
17 | */
18 | val createTransition: EnterTransition
19 |
20 | /**
21 | * Transition the scene that about to disappear forever, similar to activity onDestroy
22 | */
23 | val destroyTransition: ExitTransition
24 |
25 | /**
26 | * Transition the scene that will be pushed into back stack, similar to activity onPause
27 | * Have no effect for floating/dialog route
28 | */
29 | val pauseTransition: ExitTransition
30 |
31 | /**
32 | * Transition the scene that about to show from the back stack, similar to activity onResume
33 | * Have no effect for floating/dialog route
34 | */
35 | val resumeTransition: EnterTransition
36 |
37 | /**
38 | * This describes the zIndex of the new target content as it enters the container. It defaults
39 | * to 0f. Content with higher zIndex will be drawn over lower `zIndex`ed content. Among
40 | * content with the same index, the target content will be placed on top.
41 | */
42 | val enterTargetContentZIndex: Float get() = 0f
43 |
44 | /**
45 | * This describes the zIndex of the target content as it exists the container. It defaults
46 | * to 0f. Content with higher zIndex will be drawn over lower `zIndex`ed content.
47 | */
48 | val exitTargetContentZIndex: Float get() = 0f
49 | }
50 |
51 | @OptIn(ExperimentalAnimationApi::class)
52 | fun NavTransition(
53 | createTransition: EnterTransition = fadeIn() + scaleIn(initialScale = 0.9f),
54 | destroyTransition: ExitTransition = fadeOut() + scaleOut(targetScale = 0.9f),
55 | pauseTransition: ExitTransition = fadeOut() + scaleOut(targetScale = 1.1f),
56 | resumeTransition: EnterTransition = fadeIn() + scaleIn(initialScale = 1.1f),
57 | enterTargetContentZIndex: Float = 0f,
58 | exitTargetContentZIndex: Float = 0f,
59 | ) = object : NavTransition {
60 | override val createTransition: EnterTransition = createTransition
61 | override val destroyTransition: ExitTransition = destroyTransition
62 | override val pauseTransition: ExitTransition = pauseTransition
63 | override val resumeTransition: EnterTransition = resumeTransition
64 | override val enterTargetContentZIndex: Float = enterTargetContentZIndex
65 | override val exitTargetContentZIndex: Float = exitTargetContentZIndex
66 | }
67 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/reflect/KClass.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.reflect
2 |
3 | import kotlin.reflect.KClass
4 |
5 | expect val KClass.canonicalName: String?
6 |
--------------------------------------------------------------------------------
/precompose/src/commonMain/kotlin/moe/tlaster/precompose/ui/BackPressAdapter.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.ui
2 |
3 | import androidx.compose.runtime.compositionLocalOf
4 | import kotlinx.coroutines.flow.Flow
5 | import kotlinx.coroutines.flow.MutableStateFlow
6 | import kotlinx.coroutines.flow.map
7 |
8 | val LocalBackDispatcherOwner = compositionLocalOf { null }
9 |
10 | interface BackDispatcherOwner {
11 | val backDispatcher: BackDispatcher
12 | }
13 |
14 | class BackDispatcher {
15 | // internal for testing
16 | internal val handlers = arrayListOf()
17 | private var inProgressHandler: BackHandler? = null
18 |
19 | fun onBackPress() {
20 | val handler = currentHandler()
21 | inProgressHandler = null
22 | handler?.handleBackPress()
23 | }
24 |
25 | fun onBackProgressed(progress: Float) {
26 | currentHandler()?.handleBackProgressed(progress)
27 | }
28 |
29 | fun onBackCancelled() {
30 | val handler = currentHandler()
31 | inProgressHandler = null
32 | handler?.handleBackCancelled()
33 | }
34 |
35 | fun onBackStarted() {
36 | val handler = handlers.lastOrNull {
37 | it.isEnabled
38 | }
39 | inProgressHandler = handler
40 | handler?.handleBackStarted()
41 | }
42 |
43 | private fun currentHandler(): BackHandler? {
44 | return inProgressHandler ?: handlers.lastOrNull {
45 | it.isEnabled
46 | }
47 | }
48 |
49 | private val canHandleBackPressFlow = MutableStateFlow(0)
50 | val canHandleBackPress: Flow = canHandleBackPressFlow.map {
51 | handlers.any { it.isEnabled }
52 | }
53 |
54 | internal fun onBackStackChanged() {
55 | canHandleBackPressFlow.value++
56 | }
57 |
58 | internal fun register(handler: BackHandler) {
59 | handlers.add(handler)
60 | onBackStackChanged()
61 | }
62 |
63 | internal fun unregister(handler: BackHandler) {
64 | handlers.remove(handler)
65 | onBackStackChanged()
66 | }
67 | }
68 |
69 | interface BackHandler {
70 | val isEnabled: Boolean
71 | fun handleBackPress()
72 | fun handleBackProgressed(progress: Float)
73 | fun handleBackCancelled()
74 | fun handleBackStarted()
75 | }
76 |
77 | internal class DefaultBackHandler(
78 | override var isEnabled: Boolean = true,
79 | private val onBackPress: () -> Unit,
80 | ) : BackHandler {
81 | override fun handleBackPress() {
82 | onBackPress()
83 | }
84 |
85 | override fun handleBackCancelled() {
86 | }
87 |
88 | override fun handleBackStarted() {
89 | }
90 |
91 | override fun handleBackProgressed(progress: Float) {
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/precompose/src/iosMain/kotlin/moe/tlaster/precompose/PreComposeApplication.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.CompositionLocalProvider
5 | import androidx.compose.runtime.DisposableEffect
6 | import androidx.compose.runtime.remember
7 | import androidx.lifecycle.ViewModelStore
8 | import androidx.lifecycle.ViewModelStoreOwner
9 | import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
10 | import moe.tlaster.precompose.ui.BackDispatcher
11 | import moe.tlaster.precompose.ui.BackDispatcherOwner
12 | import moe.tlaster.precompose.ui.LocalBackDispatcherOwner
13 |
14 | @Composable
15 | actual fun PreComposeApp(
16 | content: @Composable () -> Unit,
17 | ) {
18 | val holder = remember {
19 | PreComposeWindowHolder()
20 | }
21 | DisposableEffect(holder) {
22 | onDispose {
23 | holder.viewModelStore.clear()
24 | }
25 | }
26 | CompositionLocalProvider(
27 | LocalViewModelStoreOwner provides holder,
28 | LocalBackDispatcherOwner provides holder,
29 | ) {
30 | content.invoke()
31 | }
32 | }
33 |
34 | class PreComposeWindowHolder : BackDispatcherOwner, ViewModelStoreOwner {
35 | override val viewModelStore by lazy {
36 | ViewModelStore()
37 | }
38 | override val backDispatcher by lazy {
39 | BackDispatcher()
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/precompose/src/jsMain/kotlin/moe/tlaster/precompose/PreComposeWindow.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.CompositionLocalProvider
5 | import androidx.compose.runtime.DisposableEffect
6 | import androidx.compose.runtime.remember
7 | import androidx.compose.ui.ExperimentalComposeUiApi
8 | import androidx.compose.ui.unit.IntSize
9 | import androidx.compose.ui.window.CanvasBasedWindow
10 | import androidx.lifecycle.ViewModelStore
11 | import androidx.lifecycle.ViewModelStoreOwner
12 | import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
13 | import moe.tlaster.precompose.ui.BackDispatcher
14 | import moe.tlaster.precompose.ui.BackDispatcherOwner
15 | import moe.tlaster.precompose.ui.LocalBackDispatcherOwner
16 |
17 | /**
18 | * Creates a new [CanvasBasedWindow] with the given [title] and [content].
19 | */
20 | @OptIn(ExperimentalComposeUiApi::class)
21 | fun preComposeWindow(
22 | title: String = "Untitled",
23 | canvasElementId: String = "ComposeTarget",
24 | requestResize: (suspend () -> IntSize)? = null,
25 | applyDefaultStyles: Boolean = true,
26 | content: @Composable () -> Unit,
27 | ) {
28 | CanvasBasedWindow(
29 | title = title,
30 | canvasElementId = canvasElementId,
31 | requestResize = requestResize,
32 | applyDefaultStyles = applyDefaultStyles,
33 | content = {
34 | PreComposeApp {
35 | content.invoke()
36 | }
37 | },
38 | )
39 | }
40 |
41 | @Composable
42 | actual fun PreComposeApp(
43 | content: @Composable () -> Unit,
44 | ) {
45 | val holder = remember {
46 | PreComposeWindowHolder()
47 | }
48 | DisposableEffect(holder) {
49 | onDispose {
50 | holder.viewModelStore.clear()
51 | }
52 | }
53 | CompositionLocalProvider(
54 | LocalViewModelStoreOwner provides holder,
55 | LocalBackDispatcherOwner provides holder,
56 | ) {
57 | content.invoke()
58 | }
59 | }
60 |
61 | class PreComposeWindowHolder : BackDispatcherOwner, ViewModelStoreOwner {
62 | override val viewModelStore by lazy {
63 | ViewModelStore()
64 | }
65 | override val backDispatcher by lazy {
66 | BackDispatcher()
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/precompose/src/jsMain/kotlin/moe/tlaster/precompose/reflect/KClass.js.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.reflect
2 |
3 | import kotlin.reflect.KClass
4 |
5 | actual val KClass.canonicalName: String?
6 | // qualifiedName is unsupported [This reflection API is not supported yet in JavaScript]
7 | get() = this.simpleName
8 |
--------------------------------------------------------------------------------
/precompose/src/jvmMain/kotlin/moe/tlaster/precompose/PreComposeWindow.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.CompositionLocalProvider
5 | import androidx.compose.runtime.DisposableEffect
6 | import androidx.compose.runtime.remember
7 | import androidx.lifecycle.ViewModelStore
8 | import androidx.lifecycle.ViewModelStoreOwner
9 | import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
10 | import moe.tlaster.precompose.ui.BackDispatcher
11 | import moe.tlaster.precompose.ui.BackDispatcherOwner
12 | import moe.tlaster.precompose.ui.LocalBackDispatcherOwner
13 |
14 | @Composable
15 | actual fun PreComposeApp(
16 | content: @Composable () -> Unit,
17 | ) {
18 | val holder = remember {
19 | PreComposeWindowHolder()
20 | }
21 | DisposableEffect(holder) {
22 | onDispose {
23 | holder.viewModelStore.clear()
24 | }
25 | }
26 | CompositionLocalProvider(
27 | LocalViewModelStoreOwner provides holder,
28 | LocalBackDispatcherOwner provides holder,
29 | ) {
30 | content.invoke()
31 | }
32 | }
33 |
34 | class PreComposeWindowHolder : BackDispatcherOwner, ViewModelStoreOwner {
35 | override val viewModelStore by lazy {
36 | ViewModelStore()
37 | }
38 | override val backDispatcher by lazy {
39 | BackDispatcher()
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/precompose/src/jvmMain/kotlin/moe/tlaster/precompose/reflect/KClass.jvm.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.reflect
2 |
3 | import kotlin.reflect.KClass
4 |
5 | actual val KClass.canonicalName: String?
6 | get() = this.qualifiedName
7 |
--------------------------------------------------------------------------------
/precompose/src/jvmTest/kotlin/moe/tlaster/precompose/navigation/BackDispatcherTest.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import kotlinx.coroutines.flow.first
4 | import kotlinx.coroutines.test.runTest
5 | import moe.tlaster.precompose.ui.BackDispatcher
6 | import moe.tlaster.precompose.ui.BackHandler
7 | import kotlin.test.Test
8 | import kotlin.test.assertEquals
9 |
10 | class BackDispatcherTest {
11 |
12 | @Test
13 | fun onBackPress_should_call_handleBackPress_on_the_last_enabled_handler() {
14 | val dispatcher = BackDispatcher()
15 | val handler1 = object : BackHandler {
16 | override val isEnabled = true
17 | override fun handleBackPress() {}
18 | override fun handleBackProgressed(progress: Float) {}
19 | override fun handleBackCancelled() {}
20 | override fun handleBackStarted() {}
21 | }
22 | val handler2 = object : BackHandler {
23 | override val isEnabled = true
24 | override fun handleBackPress() {}
25 | override fun handleBackProgressed(progress: Float) {}
26 | override fun handleBackCancelled() {}
27 | override fun handleBackStarted() {}
28 | }
29 | dispatcher.register(handler1)
30 | dispatcher.register(handler2)
31 |
32 | dispatcher.onBackPress()
33 |
34 | assertEquals(handler2, dispatcher.handlers.last())
35 | }
36 |
37 | @Test
38 | fun canHandleBackPress_should_return_true_if_any_handler_is_enabled() = runTest {
39 | val dispatcher = BackDispatcher()
40 | val handler1 = object : BackHandler {
41 | override val isEnabled = true
42 | override fun handleBackPress() {}
43 | override fun handleBackProgressed(progress: Float) {}
44 | override fun handleBackCancelled() {}
45 | override fun handleBackStarted() {}
46 | }
47 | val handler2 = object : BackHandler {
48 | override val isEnabled = true
49 | override fun handleBackPress() {}
50 | override fun handleBackProgressed(progress: Float) {}
51 | override fun handleBackCancelled() {}
52 | override fun handleBackStarted() {}
53 | }
54 | dispatcher.register(handler1)
55 | dispatcher.register(handler2)
56 |
57 | assertEquals(true, dispatcher.canHandleBackPress.first())
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/precompose/src/jvmTest/kotlin/moe/tlaster/precompose/navigation/BackStackEntryTest.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import androidx.lifecycle.Lifecycle
4 | import com.benasher44.uuid.uuid4
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.ExperimentalCoroutinesApi
7 | import kotlinx.coroutines.test.UnconfinedTestDispatcher
8 | import kotlinx.coroutines.test.runTest
9 | import kotlinx.coroutines.test.setMain
10 | import kotlinx.coroutines.withContext
11 | import kotlin.test.Test
12 | import kotlin.test.assertEquals
13 | import kotlin.test.assertFalse
14 | import kotlin.test.assertTrue
15 |
16 | @OptIn(ExperimentalCoroutinesApi::class)
17 | fun runMainTest(block: () -> Unit) = runTest {
18 | val testDispatcher = UnconfinedTestDispatcher(testScheduler)
19 | Dispatchers.setMain(testDispatcher)
20 | withContext(Dispatchers.Main.immediate) {
21 | block()
22 | }
23 | }
24 |
25 | class BackStackEntryTest {
26 | @Test
27 | fun testActive() = runMainTest {
28 | val parentStateHolder = TestViewModelStoreProvider()
29 | val entry = BackStackEntry(
30 | uuid4().toString(),
31 | TestRoute("foo/bar", "foo/bar"),
32 | "foo/bar",
33 | emptyMap(),
34 | parentStateHolder,
35 | )
36 | entry.viewModelStore
37 | assertTrue(parentStateHolder.contains(entry.stateId))
38 | assertEquals(Lifecycle.State.CREATED, entry.lifecycle.currentState)
39 | entry.active()
40 | assertEquals(Lifecycle.State.RESUMED, entry.lifecycle.currentState)
41 | }
42 |
43 | @Test
44 | fun testInActive() = runMainTest {
45 | val parentStateHolder = TestViewModelStoreProvider()
46 | val entry = BackStackEntry(
47 | uuid4().toString(),
48 | TestRoute("foo/bar", "foo/bar"),
49 | "foo/bar",
50 | emptyMap(),
51 | parentStateHolder,
52 | )
53 | entry.viewModelStore
54 | entry.active()
55 | assertEquals(Lifecycle.State.RESUMED, entry.lifecycle.currentState)
56 | entry.inActive()
57 | assertEquals(Lifecycle.State.CREATED, entry.lifecycle.currentState)
58 | assertTrue(parentStateHolder.contains(entry.stateId))
59 | }
60 |
61 | @Test
62 | fun testDestroy() = runMainTest {
63 | val parentStateHolder = TestViewModelStoreProvider()
64 | val entry = BackStackEntry(
65 | uuid4().toString(),
66 | TestRoute("foo/bar", "foo/bar"),
67 | "foo/bar",
68 | emptyMap(),
69 | parentStateHolder,
70 | )
71 | entry.active()
72 | assertEquals(Lifecycle.State.RESUMED, entry.lifecycle.currentState)
73 | entry.inActive()
74 | assertEquals(Lifecycle.State.CREATED, entry.lifecycle.currentState)
75 | entry.destroy()
76 | assertEquals(Lifecycle.State.DESTROYED, entry.lifecycle.currentState)
77 | assertFalse(parentStateHolder.contains(entry.stateId))
78 | }
79 |
80 | @Test
81 | fun testDestroyAfterTransition() = runMainTest {
82 | val parentStateHolder = TestViewModelStoreProvider()
83 | val entry = BackStackEntry(
84 | uuid4().toString(),
85 | TestRoute("foo/bar", "foo/bar"),
86 | "foo/bar",
87 | emptyMap(),
88 | parentStateHolder,
89 | )
90 | entry.viewModelStore
91 | entry.active()
92 | entry.destroy()
93 | assertEquals(Lifecycle.State.RESUMED, entry.lifecycle.currentState)
94 | assertTrue(parentStateHolder.contains(entry.stateId))
95 | entry.inActive()
96 | assertEquals(Lifecycle.State.DESTROYED, entry.lifecycle.currentState)
97 | assertFalse(parentStateHolder.contains(entry.stateId))
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/precompose/src/jvmTest/kotlin/moe/tlaster/precompose/navigation/NavHostTest.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | class NavHostTest {
4 |
5 | // @OptIn(ExperimentalTestApi::class)
6 | // @Test
7 | // fun navigationTest() = runComposeUiTest {
8 | // setContent {
9 | // val testLifecycleOwner = remember { TestLifecycleOwner() }
10 | // CompositionLocalProvider(
11 | // LocalLifecycleOwner provides testLifecycleOwner,
12 | // ) {
13 | // PreComposeApp {
14 | // val navigator = rememberNavigator()
15 | // Column {
16 | // Button(onClick = {
17 | // navigator.goBack()
18 | // }, modifier = Modifier.testTag("goback")) {
19 | // Text("Go Back")
20 | // }
21 | // Button(onClick = {
22 | // navigator.navigate("/1")
23 | // }, modifier = Modifier.testTag("to1")) {
24 | // Text("1")
25 | // }
26 | // Button(onClick = {
27 | // navigator.navigate("/2")
28 | // }, modifier = Modifier.testTag("to2")) {
29 | // Text("2")
30 | // }
31 | // Button(onClick = {
32 | // navigator.navigate("/3")
33 | // }, modifier = Modifier.testTag("to3")) {
34 | // Text("3")
35 | // }
36 | // NavHost(
37 | // navigator = navigator,
38 | // initialRoute = "/1",
39 | // ) {
40 | // scene("/1") {
41 | // Text("1", modifier = Modifier.testTag("screen1"))
42 | // Text("1", modifier = Modifier.testTag("text"))
43 | // }
44 | // scene("/2") {
45 | // Text("2", modifier = Modifier.testTag("screen2"))
46 | // Text("2", modifier = Modifier.testTag("text"))
47 | // }
48 | // scene("/3") {
49 | // Text("3", modifier = Modifier.testTag("screen3"))
50 | // Text("3", modifier = Modifier.testTag("text"))
51 | // }
52 | // }
53 | // }
54 | // }
55 | // }
56 | // }
57 | // onNodeWithTag("text").assertTextEquals("1")
58 | // onNodeWithTag("to2").performClick()
59 | // onNodeWithTag("to3").performClick()
60 | // onNodeWithTag("to1").performClick()
61 | // onNodeWithTag("to3").performClick()
62 | // onNodeWithTag("text").assertTextEquals("3")
63 | // }
64 | }
65 |
--------------------------------------------------------------------------------
/precompose/src/jvmTest/kotlin/moe/tlaster/precompose/navigation/NavigatorTest.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import kotlin.test.Test
4 | import kotlin.test.assertEquals
5 |
6 | class NavigatorTest {
7 | @Test
8 | fun testNavigate() = runMainTest {
9 | val navigator = Navigator()
10 | navigator.init(
11 | TestLifecycleOwner(),
12 | TestViewModelStoreOwner(),
13 | )
14 | navigator.setRouteGraph(
15 | RouteGraph(
16 | "foo/bar",
17 | listOf(
18 | TestRoute("foo/bar", "foo/bar"),
19 | TestRoute("foo/bar/{id}", "foo/bar/{id}"),
20 | TestRoute("foo/bar/{id}/baz", "foo/bar/{id}/baz"),
21 | ),
22 | ),
23 | )
24 | navigator.navigate("foo/bar/1")
25 | navigator.navigate("foo/bar/1/baz")
26 | navigator.goBack()
27 | assertEquals(2, navigator.stackManager.backStacks.value.size)
28 | assertEquals("foo/bar/{id}", navigator.stackManager.backStacks.value.last().route.route)
29 | }
30 |
31 | @Test
32 | fun testPendingNavigate() = runMainTest {
33 | val navigator = Navigator()
34 | navigator.navigate("foo/bar/1")
35 | assertEquals(0, navigator.stackManager.backStacks.value.size)
36 | navigator.init(
37 | TestLifecycleOwner(),
38 | TestViewModelStoreOwner(),
39 | )
40 | navigator.setRouteGraph(
41 | RouteGraph(
42 | "foo/bar",
43 | listOf(
44 | TestRoute("foo/bar", "foo/bar"),
45 | TestRoute("foo/bar/{id}", "foo/bar/{id}"),
46 | TestRoute("foo/bar/{id}/baz", "foo/bar/{id}/baz"),
47 | ),
48 | ),
49 | )
50 | assertEquals(2, navigator.stackManager.backStacks.value.size)
51 | assertEquals("foo/bar/{id}", navigator.stackManager.backStacks.value.last().route.route)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/precompose/src/jvmTest/kotlin/moe/tlaster/precompose/navigation/QueryStringTest.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import kotlin.test.Test
4 | import kotlin.test.assertEquals
5 | import kotlin.test.assertTrue
6 |
7 | class QueryStringTest {
8 | @Test
9 | fun simpleQueryString() {
10 | QueryString("&foo=bar").let {
11 | assertTrue(it.map.size == 1)
12 | assertTrue(it.map.containsKey("foo"))
13 | assertTrue(it.map.containsValue(listOf("bar")))
14 | assertEquals(it.query("foo"), "bar")
15 | }
16 |
17 | QueryString("foo=bar&").let {
18 | assertTrue(it.map.size == 1)
19 | assertTrue(it.map.containsKey("foo"))
20 | assertTrue(it.map.containsValue(listOf("bar")))
21 | assertEquals(it.query("foo"), "bar")
22 | }
23 |
24 | QueryString("foo=bar&&").let {
25 | assertTrue(it.map.size == 1)
26 | assertTrue(it.map.containsKey("foo"))
27 | assertTrue(it.map.containsValue(listOf("bar")))
28 | assertEquals(it.query("foo"), "bar")
29 | }
30 |
31 | QueryString("foo=bar").let {
32 | assertTrue(it.map.size == 1)
33 | assertTrue(it.map.containsKey("foo"))
34 | assertTrue(it.map.containsValue(listOf("bar")))
35 | assertEquals(it.query("foo"), "bar")
36 | }
37 |
38 | QueryString("a=1&b=2").let {
39 | assertTrue(it.map.size == 2)
40 | assertTrue(it.map.containsKey("a"))
41 | assertTrue(it.map.containsValue(listOf("1")))
42 | assertTrue(it.map.containsKey("b"))
43 | assertTrue(it.map.containsValue(listOf("2")))
44 | assertEquals(it.query("a"), "1")
45 | assertEquals(it.query("b"), "2")
46 | }
47 |
48 | QueryString("a=1&b=2&").let {
49 | assertTrue(it.map.size == 2)
50 | assertTrue(it.map.containsKey("a"))
51 | assertTrue(it.map.containsValue(listOf("1")))
52 | assertTrue(it.map.containsKey("b"))
53 | assertTrue(it.map.containsValue(listOf("2")))
54 | assertEquals(it.query("a"), "1")
55 | assertEquals(it.query("b"), "2")
56 | }
57 |
58 | QueryString("a=1&&b=2&").let {
59 | assertTrue(it.map.size == 2)
60 | assertTrue(it.map.containsKey("a"))
61 | assertTrue(it.map.containsValue(listOf("1")))
62 | assertTrue(it.map.containsKey("b"))
63 | assertTrue(it.map.containsValue(listOf("2")))
64 | assertEquals(it.query("a"), "1")
65 | assertEquals(it.query("b"), "2")
66 | }
67 |
68 | QueryString("a=1&a=2").let {
69 | assertTrue(it.map.size == 1)
70 | assertTrue(it.map.containsKey("a"))
71 | assertTrue(it.map.containsValue(listOf("1", "2")))
72 | assertEquals(it.queryList("a"), listOf("1", "2"))
73 | }
74 |
75 | assertTrue(QueryString("a=1;a=2").map.isEmpty())
76 |
77 | QueryString("a=").let {
78 | assertTrue(it.map.size == 1)
79 | assertTrue(it.map.containsKey("a"))
80 | assertEquals(it.queryList("a"), emptyList())
81 | }
82 |
83 | QueryString("a=&").let {
84 | assertTrue(it.map.size == 1)
85 | assertTrue(it.map.containsKey("a"))
86 | assertEquals(it.queryList("a"), emptyList())
87 | }
88 |
89 | QueryString("a=&&").let {
90 | assertTrue(it.map.size == 1)
91 | assertTrue(it.map.containsKey("a"))
92 | assertEquals(it.queryList("a"), emptyList())
93 | }
94 |
95 | assertTrue(QueryString("").map.isEmpty())
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/precompose/src/jvmTest/kotlin/moe/tlaster/precompose/navigation/RouteBuilderTest.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import moe.tlaster.precompose.navigation.route.GroupRoute
4 | import kotlin.test.Test
5 | import kotlin.test.assertContains
6 | import kotlin.test.assertEquals
7 | import kotlin.test.assertFailsWith
8 | import kotlin.test.assertTrue
9 |
10 | class RouteBuilderTest {
11 | @Test
12 | fun testEmptyRoute() {
13 | val graph = RouteBuilder("").build()
14 | assertTrue(graph.routes.isEmpty())
15 | }
16 |
17 | @Test
18 | fun testSingleRoute() {
19 | RouteBuilder("/home").apply {
20 | testRoute("/home", "home")
21 | }.build().apply {
22 | assertTrue(routes.size == 1)
23 | routes.first().let {
24 | assertTrue(it is TestRoute)
25 | assertEquals("/home", it.route)
26 | assertEquals("home", it.id)
27 | }
28 | }
29 | }
30 |
31 | @Test
32 | fun testMultipleRouteWithSameRoute() {
33 | assertFailsWith(IllegalArgumentException::class, "Duplicate route can not be applied") {
34 | RouteBuilder("/home").apply {
35 | testRoute("/home", "home")
36 | testRoute("/home", "home")
37 | }.build()
38 | }
39 | }
40 |
41 | @Test
42 | fun testGroupRoute() {
43 | val graph = RouteBuilder("/home").apply {
44 | testRoute("/home", "home")
45 | group("/group", "/detail") {
46 | testRoute("/detail", "detail")
47 | }
48 | }.build()
49 | assertEquals(3, graph.routes.size)
50 | assertContains(graph.routes, TestRoute("/home", "home"))
51 | assertContains(graph.routes, GroupRoute("/group", TestRoute("/detail", "detail")))
52 | assertContains(graph.routes, TestRoute("/detail", "detail"))
53 | }
54 |
55 | @Test
56 | fun testNestedGroupRoute() {
57 | val graph = RouteBuilder("/home").apply {
58 | testRoute("/home", "home")
59 | group("/group", "/detail") {
60 | testRoute("/detail", "detail")
61 | group("/group2", "/detail2") {
62 | testRoute("/detail2", "detail2")
63 | }
64 | }
65 | }.build()
66 | assertEquals(5, graph.routes.size)
67 | assertContains(graph.routes, TestRoute("/home", "home"))
68 | assertContains(graph.routes, GroupRoute("/group", TestRoute("/detail", "detail")))
69 | assertContains(graph.routes, TestRoute("/detail", "detail"))
70 | assertContains(graph.routes, GroupRoute("/group2", TestRoute("/detail2", "detail2")))
71 | assertContains(graph.routes, TestRoute("/detail2", "detail2"))
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/precompose/src/jvmTest/kotlin/moe/tlaster/precompose/navigation/TestRoute.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import moe.tlaster.precompose.navigation.route.Route
4 |
5 | data class TestRoute(
6 | override val route: String,
7 | val id: String,
8 | ) : Route
9 |
10 | fun RouteBuilder.testRoute(
11 | route: String,
12 | id: String,
13 | ) {
14 | addRoute(
15 | TestRoute(route = route, id = id),
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/precompose/src/jvmTest/kotlin/moe/tlaster/precompose/navigation/TestViewModelStoreOwner.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import androidx.lifecycle.LifecycleOwner
4 | import androidx.lifecycle.LifecycleRegistry
5 | import androidx.lifecycle.ViewModelStore
6 | import androidx.lifecycle.ViewModelStoreOwner
7 |
8 | class TestViewModelStoreOwner : ViewModelStoreOwner {
9 | override val viewModelStore: ViewModelStore
10 | get() = ViewModelStore()
11 | }
12 |
13 | class TestLifecycleOwner : LifecycleOwner {
14 | private val registry by lazy {
15 | LifecycleRegistry(this)
16 | }
17 | override val lifecycle by lazy {
18 | registry
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/precompose/src/jvmTest/kotlin/moe/tlaster/precompose/navigation/TestViewModelStoreProvider.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.navigation
2 |
3 | import androidx.lifecycle.ViewModelStore
4 |
5 | class TestViewModelStoreProvider : ViewModelStoreProvider {
6 | private val viewModelStoreMap = mutableMapOf()
7 |
8 | override fun getViewModelStore(backStackEntryId: String): ViewModelStore {
9 | return viewModelStoreMap.getOrPut(backStackEntryId) { ViewModelStore() }
10 | }
11 |
12 | override fun clear(backStackEntryId: String) {
13 | viewModelStoreMap.remove(backStackEntryId)
14 | }
15 |
16 | fun contains(backStackEntryId: String): Boolean {
17 | return viewModelStoreMap.containsKey(backStackEntryId)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/precompose/src/macosMain/kotlin/moe/tlaster/precompose/PreComposeWindow.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.CompositionLocalProvider
5 | import androidx.compose.runtime.DisposableEffect
6 | import androidx.compose.runtime.ExperimentalComposeApi
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.ui.window.Window
9 | import androidx.lifecycle.ViewModelStore
10 | import androidx.lifecycle.ViewModelStoreOwner
11 | import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
12 | import moe.tlaster.precompose.ui.BackDispatcher
13 | import moe.tlaster.precompose.ui.BackDispatcherOwner
14 | import moe.tlaster.precompose.ui.LocalBackDispatcherOwner
15 |
16 | @ExperimentalComposeApi
17 | fun PreComposeWindow(
18 | title: String,
19 | // hideTitleBar: Boolean = false,
20 | // onCloseRequest: () -> Unit = {},
21 | // onMinimizeRequest: () -> Unit = {},
22 | // onDeminiaturizeRequest: () -> Unit = {},
23 | content: @Composable () -> Unit,
24 | ) {
25 | // Ugly workaround until Native macOS support window resize and hide title bar.
26 | Window(
27 | title = title,
28 | content = {
29 | PreComposeApp {
30 | content.invoke()
31 | }
32 | },
33 | )
34 | }
35 |
36 | @Composable
37 | actual fun PreComposeApp(
38 | content: @Composable () -> Unit,
39 | ) {
40 | val holder = remember {
41 | PreComposeWindowHolder()
42 | }
43 | DisposableEffect(holder) {
44 | onDispose {
45 | holder.viewModelStore.clear()
46 | }
47 | }
48 | CompositionLocalProvider(
49 | LocalViewModelStoreOwner provides holder,
50 | LocalBackDispatcherOwner provides holder,
51 | ) {
52 | content.invoke()
53 | }
54 | }
55 |
56 | class PreComposeWindowHolder : BackDispatcherOwner, ViewModelStoreOwner {
57 | override val viewModelStore by lazy {
58 | ViewModelStore()
59 | }
60 | override val backDispatcher by lazy {
61 | BackDispatcher()
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/precompose/src/nativeMain/kotlin/moe/tlaster/precompose/reflect/KClass.native.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.reflect
2 |
3 | import kotlin.reflect.KClass
4 |
5 | actual val KClass.canonicalName: String?
6 | get() = this.qualifiedName
7 |
--------------------------------------------------------------------------------
/precompose/src/wasmJsMain/kotlin/moe/tlaster/precompose/PreComposeApp.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.CompositionLocalProvider
5 | import androidx.compose.runtime.DisposableEffect
6 | import androidx.compose.runtime.remember
7 | import androidx.lifecycle.ViewModelStore
8 | import androidx.lifecycle.ViewModelStoreOwner
9 | import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
10 | import moe.tlaster.precompose.ui.BackDispatcher
11 | import moe.tlaster.precompose.ui.BackDispatcherOwner
12 | import moe.tlaster.precompose.ui.LocalBackDispatcherOwner
13 |
14 | @Composable
15 | actual fun PreComposeApp(
16 | content: @Composable () -> Unit,
17 | ) {
18 | val holder = remember {
19 | PreComposeWindowHolder()
20 | }
21 | DisposableEffect(holder) {
22 | onDispose {
23 | holder.viewModelStore.clear()
24 | }
25 | }
26 | CompositionLocalProvider(
27 | LocalViewModelStoreOwner provides holder,
28 | LocalBackDispatcherOwner provides holder,
29 | ) {
30 | content.invoke()
31 | }
32 | }
33 |
34 | class PreComposeWindowHolder : BackDispatcherOwner, ViewModelStoreOwner {
35 | override val viewModelStore by lazy {
36 | ViewModelStore()
37 | }
38 | override val backDispatcher by lazy {
39 | BackDispatcher()
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/precompose/src/wasmJsMain/kotlin/moe/tlaster/precompose/reflect/KClass.wasm.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.reflect
2 |
3 | import kotlin.reflect.KClass
4 |
5 | actual val KClass.canonicalName: String?
6 | // qualifiedName is unsupported [This reflection API is not supported yet in WASM]
7 | get() = this.simpleName
8 |
--------------------------------------------------------------------------------
/project.yml:
--------------------------------------------------------------------------------
1 | name: PreComposeSample
2 | options:
3 | bundleIdPrefix: moe.tlaster
4 | settings:
5 | DEVELOPMENT_TEAM: N462MKSJ7M
6 | CODE_SIGN_IDENTITY: "iPhone Developer"
7 | CODE_SIGN_STYLE: Automatic
8 | MARKETING_VERSION: "1.0"
9 | CURRENT_PROJECT_VERSION: "4"
10 | SDKROOT: iphoneos
11 | targets:
12 | PreComposeSample:
13 | type: application
14 | platform: iOS
15 | deploymentTarget: "12.0"
16 | prebuildScripts:
17 | - script: cd "$SRCROOT" && ./gradlew -i -p . packComposeUikitApplicationForXCode
18 | name: GradleCompile
19 | info:
20 | path: sample/ios/plists/Ios/Info.plist
21 | properties:
22 | UILaunchStoryboardName: ""
23 | sources:
24 | - "sample/ios/src"
25 | settings:
26 | LIBRARY_SEARCH_PATHS: "$(inherited)"
27 | ENABLE_BITCODE: "YES"
28 | ONLY_ACTIVE_ARCH: "NO"
29 | VALID_ARCHS: "arm64"
--------------------------------------------------------------------------------
/sample/molecule/composeApp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat
2 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
3 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
4 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
5 | import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
6 |
7 | plugins {
8 | kotlin("multiplatform")
9 | alias(libs.plugins.jetbrains.compose)
10 | id("com.android.application")
11 | alias(libs.plugins.compose.compiler)
12 | }
13 |
14 | kotlin {
15 | applyDefaultHierarchyTemplate()
16 | androidTarget {
17 | @OptIn(ExperimentalKotlinGradlePluginApi::class)
18 | compilerOptions {
19 | jvmTarget.set(JvmTarget.JVM_11)
20 | }
21 | }
22 |
23 | listOf(
24 | iosX64(),
25 | iosArm64(),
26 | iosSimulatorArm64(),
27 | ).forEach { iosTarget ->
28 | iosTarget.binaries.framework {
29 | baseName = "ComposeApp"
30 | isStatic = true
31 | }
32 | }
33 |
34 | jvm()
35 |
36 | @OptIn(ExperimentalWasmDsl::class)
37 | wasmJs {
38 | moduleName = "composeApp"
39 | browser {
40 | val rootDirPath = project.rootDir.path
41 | val projectDirPath = project.projectDir.path
42 | commonWebpackConfig {
43 | outputFileName = "composeApp.js"
44 | devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {
45 | static = (static ?: mutableListOf()).apply {
46 | // Serve sources to debug inside browser
47 | add(rootDirPath)
48 | add(projectDirPath)
49 | }
50 | }
51 | }
52 | }
53 | binaries.executable()
54 | }
55 |
56 | sourceSets {
57 | androidMain.dependencies {
58 | implementation(libs.androidx.appcompat)
59 | implementation(libs.androidx.activity.compose)
60 | }
61 | commonMain.dependencies {
62 | implementation(compose.ui)
63 | implementation(compose.runtime)
64 | implementation(compose.foundation)
65 | implementation(compose.material)
66 | implementation(project(":precompose"))
67 | implementation(project(":precompose-molecule"))
68 | implementation(libs.molecule.runtime)
69 | }
70 | jvmMain.dependencies {
71 | implementation(compose.desktop.currentOs)
72 | }
73 | }
74 | }
75 |
76 | android {
77 | namespace = "moe.tlaster.precompose.molecule.sample"
78 | compileSdk = libs.versions.compileSdk.get().toInt()
79 |
80 | defaultConfig {
81 | applicationId = "moe.tlaster.precompose.molecule.sample"
82 | minSdk = libs.versions.minSdk.get().toInt()
83 | targetSdk = libs.versions.compileSdk.get().toInt()
84 | versionCode = 1
85 | versionName = "1.0"
86 | }
87 | packaging {
88 | resources {
89 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
90 | }
91 | }
92 | buildTypes {
93 | getByName("release") {
94 | isMinifyEnabled = false
95 | }
96 | }
97 | compileOptions {
98 | sourceCompatibility = JavaVersion.VERSION_11
99 | targetCompatibility = JavaVersion.VERSION_11
100 | }
101 | }
102 |
103 | dependencies {
104 | debugImplementation(compose.uiTooling)
105 | }
106 |
107 | compose.desktop {
108 | application {
109 | mainClass = "moe.tlaster.precompose.molecule.sample.MainKt"
110 |
111 | nativeDistributions {
112 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
113 | packageName = "moe.tlaster.precompose.molecule.sample"
114 | packageVersion = "1.0.0"
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/androidMain/kotlin/moe/tlaster/precompose/molecule/sample/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.molecule.sample
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 |
7 | class MainActivity : ComponentActivity() {
8 | override fun onCreate(savedInstanceState: Bundle?) {
9 | super.onCreate(savedInstanceState)
10 |
11 | setContent {
12 | App()
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tlaster/PreCompose/0f79c88a4f768cf851cdaa1da36759ee2d489de1/sample/molecule/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tlaster/PreCompose/0f79c88a4f768cf851cdaa1da36759ee2d489de1/sample/molecule/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tlaster/PreCompose/0f79c88a4f768cf851cdaa1da36759ee2d489de1/sample/molecule/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tlaster/PreCompose/0f79c88a4f768cf851cdaa1da36759ee2d489de1/sample/molecule/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tlaster/PreCompose/0f79c88a4f768cf851cdaa1da36759ee2d489de1/sample/molecule/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tlaster/PreCompose/0f79c88a4f768cf851cdaa1da36759ee2d489de1/sample/molecule/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tlaster/PreCompose/0f79c88a4f768cf851cdaa1da36759ee2d489de1/sample/molecule/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tlaster/PreCompose/0f79c88a4f768cf851cdaa1da36759ee2d489de1/sample/molecule/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tlaster/PreCompose/0f79c88a4f768cf851cdaa1da36759ee2d489de1/sample/molecule/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tlaster/PreCompose/0f79c88a4f768cf851cdaa1da36759ee2d489de1/sample/molecule/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/androidMain/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | molecule-sample
3 |
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
24 |
30 |
36 |
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/commonMain/kotlin/moe/tlaster/precompose/molecule/sample/App.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.molecule.sample
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.material.Button
7 | import androidx.compose.material.MaterialTheme
8 | import androidx.compose.material.Scaffold
9 | import androidx.compose.material.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.runtime.mutableStateOf
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.runtime.setValue
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import moe.tlaster.precompose.PreComposeApp
18 | import moe.tlaster.precompose.molecule.producePresenter
19 |
20 | @Composable
21 | fun App() {
22 | PreComposeApp {
23 | val state by producePresenter { Presenter() }
24 | MaterialTheme {
25 | Scaffold {
26 | Column(
27 | modifier = Modifier.fillMaxSize(),
28 | horizontalAlignment = Alignment.CenterHorizontally,
29 | verticalArrangement = Arrangement.Center,
30 | ) {
31 | Text(text = state.count)
32 | Button(
33 | onClick = {
34 | state.action(Action.Increment)
35 | },
36 | ) {
37 | Text(text = "Increment")
38 | }
39 | Button(
40 | onClick = {
41 | state.action(Action.Decrement)
42 | },
43 | ) {
44 | Text(text = "Decrement")
45 | }
46 | }
47 | }
48 | }
49 | }
50 | }
51 |
52 | @Composable
53 | fun Presenter(): State {
54 | var count by remember { mutableStateOf(0) }
55 | return State(
56 | "Clicked $count times",
57 | ) {
58 | when (it) {
59 | Action.Increment -> count++
60 | Action.Decrement -> count--
61 | }
62 | }
63 | }
64 |
65 | sealed interface Action {
66 | object Increment : Action
67 | object Decrement : Action
68 | }
69 |
70 | data class State(
71 | val count: String,
72 | val action: (Action) -> Unit,
73 | )
74 |
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/iosMain/kotlin/moe/tlaster/precompose/molecule/sample/MainViewController.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.molecule.sample
2 |
3 | import androidx.compose.ui.window.ComposeUIViewController
4 |
5 | fun MainViewController() = ComposeUIViewController { App() }
6 |
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/jvmMain/kotlin/moe/tlaster/precompose/molecule/sample/Main.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.molecule.sample
2 |
3 | import androidx.compose.ui.window.Window
4 | import androidx.compose.ui.window.application
5 |
6 | fun main() = application {
7 | Window(
8 | onCloseRequest = ::exitApplication,
9 | title = "molecule-sample",
10 | ) {
11 | App()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/wasmJsMain/kotlin/moe/tlaster/precompose/molecule/sample/Main.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.precompose.molecule.sample
2 |
3 | import androidx.compose.ui.ExperimentalComposeUiApi
4 | import androidx.compose.ui.window.ComposeViewport
5 | import kotlinx.browser.document
6 |
7 | @OptIn(ExperimentalComposeUiApi::class)
8 | fun main() {
9 | ComposeViewport(document.body!!) {
10 | App()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/wasmJsMain/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | molecule-sample
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/sample/molecule/composeApp/src/wasmJsMain/resources/styles.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | width: 100%;
3 | height: 100%;
4 | margin: 0;
5 | padding: 0;
6 | overflow: hidden;
7 | }
--------------------------------------------------------------------------------
/sample/molecule/iosApp/Configuration/Config.xcconfig:
--------------------------------------------------------------------------------
1 | TEAM_ID=
2 | BUNDLE_ID=moe.tlaster.precompose.molecule.sample.molecule-sample
3 | APP_NAME=molecule-sample
--------------------------------------------------------------------------------
/sample/molecule/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
--------------------------------------------------------------------------------
/sample/molecule/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "app-icon-1024.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/sample/molecule/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tlaster/PreCompose/0f79c88a4f768cf851cdaa1da36759ee2d489de1/sample/molecule/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png
--------------------------------------------------------------------------------
/sample/molecule/iosApp/iosApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
--------------------------------------------------------------------------------
/sample/molecule/iosApp/iosApp/ContentView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftUI
3 | import ComposeApp
4 |
5 | struct ComposeView: UIViewControllerRepresentable {
6 | func makeUIViewController(context: Context) -> UIViewController {
7 | MainViewControllerKt.MainViewController()
8 | }
9 |
10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
11 | }
12 |
13 | struct ContentView: View {
14 | var body: some View {
15 | ComposeView()
16 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler
17 | }
18 | }
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/sample/molecule/iosApp/iosApp/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | CADisableMinimumFrameDurationOnPhone
24 |
25 | UIApplicationSceneManifest
26 |
27 | UIApplicationSupportsMultipleScenes
28 |
29 |
30 | UILaunchScreen
31 |
32 | UIRequiredDeviceCapabilities
33 |
34 | armv7
35 |
36 | UISupportedInterfaceOrientations
37 |
38 | UIInterfaceOrientationPortrait
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UISupportedInterfaceOrientations~ipad
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationPortraitUpsideDown
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/sample/molecule/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
--------------------------------------------------------------------------------
/sample/molecule/iosApp/iosApp/iOSApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct iOSApp: App {
5 | var body: some Scene {
6 | WindowGroup {
7 | ContentView()
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/sample/todo/android/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.jetbrains.compose)
3 | id("com.android.application")
4 | kotlin("android")
5 | alias(libs.plugins.compose.compiler)
6 | }
7 |
8 | group = "moe.tlaster"
9 | version = "1.0"
10 |
11 | dependencies {
12 | implementation(project(":sample:todo:common"))
13 | implementation(libs.androidx.activity.compose)
14 | }
15 |
16 | android {
17 | compileSdk = libs.versions.compileSdk.get().toInt()
18 | defaultConfig {
19 | applicationId = "moe.tlaster.android"
20 | minSdk = libs.versions.minSdk.get().toInt()
21 | targetSdk = libs.versions.compileSdk.get().toInt()
22 | versionCode = 1
23 | versionName = "0.1.0"
24 | }
25 | buildTypes {
26 | getByName("release") {
27 | isMinifyEnabled = false
28 | }
29 | }
30 | compileOptions {
31 | sourceCompatibility = JavaVersion.toVersion(libs.versions.java.get())
32 | targetCompatibility = JavaVersion.toVersion(libs.versions.java.get())
33 | }
34 | namespace = "moe.tlaster.android"
35 | }
36 |
--------------------------------------------------------------------------------
/sample/todo/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/sample/todo/android/src/main/java/moe/tlaster/android/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.android
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import moe.tlaster.common.App
7 |
8 | class MainActivity : ComponentActivity() {
9 | override fun onCreate(savedInstanceState: Bundle?) {
10 | super.onCreate(savedInstanceState)
11 | setContent {
12 | App()
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/sample/todo/android/src/main/java/moe/tlaster/android/TodoApplication.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.android
2 |
3 | import android.app.Application
4 | import moe.tlaster.common.di.AppModule
5 | import org.koin.core.context.startKoin
6 | import org.koin.core.context.stopKoin
7 |
8 | class TodoApplication : Application() {
9 |
10 | override fun onCreate() {
11 | super.onCreate()
12 | initKoin()
13 | }
14 |
15 | private fun initKoin() {
16 | stopKoin()
17 | startKoin {
18 | modules(AppModule.appModule)
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sample/todo/common/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | alias(libs.plugins.jetbrains.compose)
4 | id("com.android.library")
5 | alias(libs.plugins.compose.compiler)
6 | }
7 |
8 | kotlin {
9 | applyDefaultHierarchyTemplate()
10 | macosX64()
11 | macosArm64()
12 | iosArm64()
13 | iosX64()
14 | iosSimulatorArm64()
15 | androidTarget()
16 | jvm("desktop")
17 | js(IR) {
18 | browser()
19 | }
20 | sourceSets {
21 | val commonMain by getting {
22 | dependencies {
23 | api(compose.runtime)
24 | api(compose.foundation)
25 | api(compose.material)
26 | api(libs.koin)
27 | api(libs.koin.compose)
28 | api(libs.koin.compose.viewmodel)
29 | api(project(":precompose"))
30 | }
31 | }
32 | val commonTest by getting
33 | val androidMain by getting {
34 | dependencies {
35 | api(libs.androidx.appcompat)
36 | api(libs.androidx.coreKtx)
37 | }
38 | }
39 | val androidUnitTest by getting {
40 | dependencies {
41 | implementation(libs.junit)
42 | }
43 | }
44 | val desktopMain by getting
45 | val desktopTest by getting
46 | // val jsMain by getting
47 | }
48 | }
49 |
50 | android {
51 | compileSdk = libs.versions.compileSdk.get().toInt()
52 | namespace = "moe.tlaster.common"
53 | defaultConfig {
54 | minSdk = libs.versions.minSdk.get().toInt()
55 | }
56 | kotlin.jvmToolchain(libs.versions.java.get().toInt())
57 | }
58 |
59 | compose.experimental {
60 | web.application {}
61 | }
62 |
63 | tasks.withType {
64 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE
65 | }
66 |
67 | afterEvaluate {
68 | rootProject.extensions.configure {
69 | versions.webpackCli.version = libs.versions.webpackCliVersion.get()
70 | version = libs.versions.nodeVersion.get()
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/sample/todo/common/src/commonMain/kotlin/moe/tlaster/common/App.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.common
2 |
3 | import androidx.compose.material.ExperimentalMaterialApi
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.runtime.Composable
6 | import moe.tlaster.common.scene.NoteDetailScene
7 | import moe.tlaster.common.scene.NoteEditScene
8 | import moe.tlaster.common.scene.NoteListScene
9 | import moe.tlaster.precompose.PreComposeApp
10 | import moe.tlaster.precompose.navigation.NavHost
11 | import moe.tlaster.precompose.navigation.path
12 | import moe.tlaster.precompose.navigation.rememberNavigator
13 | import org.koin.compose.KoinContext
14 |
15 | @OptIn(ExperimentalMaterialApi::class)
16 | @Composable
17 | fun App() {
18 | PreComposeApp {
19 | KoinContext {
20 | val navigator = rememberNavigator()
21 | MaterialTheme {
22 | NavHost(
23 | navigator = navigator,
24 | initialRoute = "/home",
25 | ) {
26 | scene("/home") {
27 | NoteListScene(
28 | onItemClicked = {
29 | navigator.navigate("/detail/${it.id}")
30 | },
31 | onAddClicked = {
32 | navigator.navigate("/edit")
33 | },
34 | onEditClicked = {
35 | navigator.navigate("/edit/${it.id}")
36 | },
37 | )
38 | }
39 | scene("/detail/{id:[0-9]+}") { backStackEntry ->
40 | backStackEntry.path("id")?.let {
41 | NoteDetailScene(
42 | id = it,
43 | onEdit = {
44 | navigator.navigate("/edit/$it")
45 | },
46 | onBack = {
47 | navigator.goBack()
48 | },
49 | )
50 | }
51 | }
52 | dialog(
53 | "/edit/{id:[0-9]+}?",
54 | ) { backStackEntry ->
55 | val id = backStackEntry.path("id")
56 | NoteEditScene(
57 | id = id,
58 | onDone = {
59 | navigator.goBack()
60 | },
61 | onBack = {
62 | navigator.goBack()
63 | },
64 | )
65 | }
66 | }
67 | }
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/sample/todo/common/src/commonMain/kotlin/moe/tlaster/common/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.common.di
2 |
3 | import moe.tlaster.common.repository.FakeRepository
4 | import moe.tlaster.common.viewmodel.NoteDetailViewModel
5 | import moe.tlaster.common.viewmodel.NoteEditViewModel
6 | import moe.tlaster.common.viewmodel.NoteListViewModel
7 | import org.koin.core.module.dsl.viewModel
8 | import org.koin.dsl.module
9 |
10 | object AppModule {
11 | val appModule = module {
12 | single { FakeRepository() }
13 | viewModel { (id: Int) ->
14 | NoteDetailViewModel(
15 | id = id,
16 | fakeRepository = get(),
17 | )
18 | }
19 |
20 | viewModel { (id: Int) ->
21 | NoteEditViewModel(
22 | id = id,
23 | fakeRepository = get(),
24 | )
25 | }
26 | viewModel { NoteListViewModel(fakeRepository = get()) }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/sample/todo/common/src/commonMain/kotlin/moe/tlaster/common/model/Note.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.common.model
2 |
3 | data class Note(
4 | val id: Int,
5 | val title: String,
6 | val content: String,
7 | )
8 |
--------------------------------------------------------------------------------
/sample/todo/common/src/commonMain/kotlin/moe/tlaster/common/repository/FakeRepository.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.common.repository
2 |
3 | import kotlinx.coroutines.flow.MutableStateFlow
4 | import moe.tlaster.common.model.Note
5 |
6 | private val fakeInitialNotes = mutableListOf(
7 | Note(0, title = "This is my first note", content = "Wow!"),
8 | Note(1, title = "You can create one", content = "Sounds great!"),
9 | Note(2, title = "Or you can edit one", content = "Awesome!"),
10 | Note(3, title = "You can also delete one", content = "Ok!"),
11 | Note(4, title = "Finally, you can click one to view", content = "Thanks!"),
12 | )
13 |
14 | class FakeRepository {
15 | private val watchers = hashMapOf>()
16 | val items = MutableStateFlow(fakeInitialNotes)
17 |
18 | fun get(id: Int): Note? {
19 | return items.value.firstOrNull { it.id == id }
20 | }
21 |
22 | fun add(title: String, content: String) {
23 | items.value.let {
24 | items.value = (it + Note(id = items.value.size, title = title, content = content)).toMutableList()
25 | }
26 | }
27 |
28 | fun remove(note: Note) {
29 | items.value.let {
30 | items.value = (it - note).toMutableList()
31 | }
32 | }
33 |
34 | fun update(note: Note) {
35 | watchers[note.id]?.let {
36 | it.value = note
37 | }
38 | get(note.id)?.let { n ->
39 | items.value.let { list ->
40 | list[list.indexOf(n)] = note
41 | items.value = list
42 | }
43 | }
44 | }
45 |
46 | fun getLiveData(id: Int): MutableStateFlow {
47 | return get(id)?.let {
48 | watchers.getOrPut(id) {
49 | MutableStateFlow(it)
50 | }
51 | } ?: throw IllegalArgumentException()
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/sample/todo/common/src/commonMain/kotlin/moe/tlaster/common/scene/NoteDetailScene.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.common.scene
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.material.Divider
5 | import androidx.compose.material.ExperimentalMaterialApi
6 | import androidx.compose.material.Icon
7 | import androidx.compose.material.IconButton
8 | import androidx.compose.material.ListItem
9 | import androidx.compose.material.MaterialTheme
10 | import androidx.compose.material.Scaffold
11 | import androidx.compose.material.Text
12 | import androidx.compose.material.TopAppBar
13 | import androidx.compose.material.icons.Icons
14 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
15 | import androidx.compose.material.icons.filled.Edit
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.runtime.collectAsState
18 | import androidx.compose.runtime.getValue
19 | import moe.tlaster.common.viewmodel.NoteDetailViewModel
20 | import org.koin.compose.viewmodel.koinViewModel
21 | import org.koin.core.parameter.parametersOf
22 |
23 | @ExperimentalMaterialApi
24 | @Composable
25 | fun NoteDetailScene(
26 | id: Int,
27 | onBack: () -> Unit,
28 | onEdit: () -> Unit,
29 | ) {
30 | val viewModel = koinViewModel { parametersOf(id) }
31 |
32 | Scaffold(
33 | topBar = {
34 | TopAppBar(
35 | title = {
36 | Text("Detail")
37 | },
38 | navigationIcon = {
39 | IconButton(onClick = { onBack.invoke() }) {
40 | Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
41 | }
42 | },
43 | actions = {
44 | IconButton(onClick = { onEdit.invoke() }) {
45 | Icon(Icons.Default.Edit, contentDescription = null)
46 | }
47 | },
48 | )
49 | },
50 | ) {
51 | Column {
52 | val note by viewModel.note.collectAsState()
53 | ListItem {
54 | Text(text = note.title, style = MaterialTheme.typography.h5)
55 | }
56 | Divider()
57 | ListItem {
58 | Text(text = note.content)
59 | }
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/sample/todo/common/src/commonMain/kotlin/moe/tlaster/common/scene/NoteEditScene.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.common.scene
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.material.ExperimentalMaterialApi
7 | import androidx.compose.material.Icon
8 | import androidx.compose.material.IconButton
9 | import androidx.compose.material.ListItem
10 | import androidx.compose.material.OutlinedTextField
11 | import androidx.compose.material.Scaffold
12 | import androidx.compose.material.Text
13 | import androidx.compose.material.TopAppBar
14 | import androidx.compose.material.icons.Icons
15 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
16 | import androidx.compose.material.icons.filled.Done
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.getValue
19 | import androidx.compose.ui.Modifier
20 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
21 | import moe.tlaster.common.viewmodel.NoteEditViewModel
22 | import org.koin.compose.viewmodel.koinViewModel
23 | import org.koin.core.parameter.parametersOf
24 |
25 | @ExperimentalMaterialApi
26 | @Composable
27 | fun NoteEditScene(
28 | id: Int? = null,
29 | onDone: () -> Unit = {},
30 | onBack: () -> Unit = {},
31 | ) {
32 | val viewModel = koinViewModel { parametersOf(id) }
33 |
34 | Scaffold(
35 | topBar = {
36 | TopAppBar(
37 | actions = {
38 | IconButton(
39 | onClick = {
40 | viewModel.save()
41 | onDone.invoke()
42 | },
43 | ) {
44 | Icon(Icons.Default.Done, contentDescription = null)
45 | }
46 | },
47 | title = {
48 | if (id == null) {
49 | Text("Create")
50 | } else {
51 | Text("Edit")
52 | }
53 | },
54 | navigationIcon = {
55 | IconButton(onClick = { onBack.invoke() }) {
56 | Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
57 | }
58 | },
59 | )
60 | },
61 | ) {
62 | Column {
63 | val title by viewModel.title.collectAsStateWithLifecycle()
64 | val content by viewModel.content.collectAsStateWithLifecycle()
65 | ListItem {
66 | OutlinedTextField(
67 | modifier = Modifier.fillMaxWidth(),
68 | value = title,
69 | onValueChange = {
70 | viewModel.setTitle(it)
71 | },
72 | placeholder = {
73 | Text("Title")
74 | },
75 | )
76 | }
77 | ListItem(
78 | modifier = Modifier
79 | .weight(1f),
80 | ) {
81 | OutlinedTextField(
82 | modifier = Modifier.fillMaxSize(),
83 | value = content,
84 | onValueChange = {
85 | viewModel.setContent(it)
86 | },
87 | placeholder = {
88 | Text("Content")
89 | },
90 | )
91 | }
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/sample/todo/common/src/commonMain/kotlin/moe/tlaster/common/scene/NoteListScene.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.common.scene
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.lazy.LazyColumn
6 | import androidx.compose.foundation.lazy.items
7 | import androidx.compose.material.ExperimentalMaterialApi
8 | import androidx.compose.material.FloatingActionButton
9 | import androidx.compose.material.Icon
10 | import androidx.compose.material.ListItem
11 | import androidx.compose.material.Scaffold
12 | import androidx.compose.material.Text
13 | import androidx.compose.material.TextButton
14 | import androidx.compose.material.TopAppBar
15 | import androidx.compose.material.icons.Icons
16 | import androidx.compose.material.icons.filled.Add
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.collectAsState
19 | import androidx.compose.runtime.getValue
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.graphics.Color
22 | import moe.tlaster.common.model.Note
23 | import moe.tlaster.common.viewmodel.NoteListViewModel
24 | import org.koin.compose.viewmodel.koinViewModel
25 |
26 | @ExperimentalMaterialApi
27 | @Composable
28 | fun NoteListScene(
29 | onItemClicked: (note: Note) -> Unit,
30 | onEditClicked: (note: Note) -> Unit,
31 | onAddClicked: () -> Unit,
32 | ) {
33 | val viewModel = koinViewModel()
34 |
35 | val items by viewModel.items.collectAsState()
36 |
37 | Scaffold(
38 | topBar = {
39 | TopAppBar(
40 | title = {
41 | Text("Note")
42 | },
43 | )
44 | },
45 | floatingActionButton = {
46 | FloatingActionButton(onClick = { onAddClicked.invoke() }) {
47 | Icon(Icons.Default.Add, contentDescription = null)
48 | }
49 | },
50 | ) {
51 | LazyColumn {
52 | items(items, key = { it.hashCode() }) {
53 | ListItem(
54 | modifier = Modifier
55 | .clickable {
56 | onItemClicked.invoke(it)
57 | },
58 | text = {
59 | Text(it.title)
60 | },
61 | trailing = {
62 | Row {
63 | TextButton(
64 | onClick = {
65 | onEditClicked.invoke(it)
66 | },
67 | ) {
68 | Text("Edit")
69 | }
70 | TextButton(
71 | onClick = {
72 | viewModel.delete(it)
73 | },
74 | ) {
75 | Text("Delete", color = Color.Red)
76 | }
77 | }
78 | },
79 | )
80 | }
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/sample/todo/common/src/commonMain/kotlin/moe/tlaster/common/viewmodel/NoteDetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.common.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import moe.tlaster.common.repository.FakeRepository
5 |
6 | class NoteDetailViewModel(
7 | private val id: Int,
8 | private val fakeRepository: FakeRepository,
9 | ) : ViewModel() {
10 | val note by lazy {
11 | fakeRepository.getLiveData(id)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/sample/todo/common/src/commonMain/kotlin/moe/tlaster/common/viewmodel/NoteEditViewModel.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.common.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import kotlinx.coroutines.flow.MutableStateFlow
5 | import moe.tlaster.common.repository.FakeRepository
6 |
7 | class NoteEditViewModel(
8 | private val id: Int?,
9 | private val fakeRepository: FakeRepository,
10 | ) : ViewModel() {
11 |
12 | private val note by lazy {
13 | if (id != null) {
14 | fakeRepository.get(id)
15 | } else {
16 | null
17 | }
18 | }
19 |
20 | val title = MutableStateFlow(note?.title ?: "")
21 | val content = MutableStateFlow(note?.content ?: "")
22 |
23 | fun setTitle(value: String) {
24 | title.value = value
25 | }
26 |
27 | fun setContent(value: String) {
28 | content.value = value
29 | }
30 |
31 | fun save() {
32 | note?.let {
33 | fakeRepository.update(it.copy(title = title.value, content = content.value))
34 | } ?: run {
35 | fakeRepository.add(title = title.value, content = content.value)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/sample/todo/common/src/commonMain/kotlin/moe/tlaster/common/viewmodel/NoteListViewModel.kt:
--------------------------------------------------------------------------------
1 | package moe.tlaster.common.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import moe.tlaster.common.model.Note
5 | import moe.tlaster.common.repository.FakeRepository
6 |
7 | class NoteListViewModel(
8 | private val fakeRepository: FakeRepository,
9 | ) : ViewModel() {
10 | val items by lazy {
11 | fakeRepository.items
12 | }
13 | fun delete(note: Note) {
14 | fakeRepository.remove(note = note)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/sample/todo/desktop/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat
2 |
3 | plugins {
4 | kotlin("multiplatform")
5 | alias(libs.plugins.jetbrains.compose)
6 | alias(libs.plugins.compose.compiler)
7 | }
8 |
9 | group = "moe.tlaster"
10 | version = "1.0"
11 |
12 | kotlin {
13 | jvm()
14 | sourceSets {
15 | val jvmMain by getting {
16 | dependencies {
17 | implementation(project(":sample:todo:common"))
18 | implementation(compose.desktop.currentOs)
19 | }
20 | }
21 | val jvmTest by getting
22 | }
23 | }
24 |
25 | compose.desktop {
26 | application {
27 | mainClass = "MainKt"
28 | nativeDistributions {
29 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
30 | packageName = "jvm"
31 | packageVersion = "1.0.0"
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/sample/todo/desktop/src/jvmMain/kotlin/Main.kt:
--------------------------------------------------------------------------------
1 |
2 | import androidx.compose.ui.window.Window
3 | import androidx.compose.ui.window.application
4 | import moe.tlaster.common.App
5 | import moe.tlaster.common.di.AppModule
6 | import org.koin.core.context.startKoin
7 |
8 | fun main() {
9 | startKoin {
10 | modules(AppModule.appModule)
11 | }
12 | application {
13 | Window(
14 | title = "PreCompose Sample",
15 | onCloseRequest = {
16 | exitApplication()
17 | },
18 | ) {
19 | App()
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/sample/todo/ios/.gitignore:
--------------------------------------------------------------------------------
1 | prebuilds/fullsdk-linux
2 | prebuilds/fullsdk-darwin
3 | out
4 | *.iml
5 | .gradle
6 | /local.properties
7 | /.idea
8 | /.idea/caches
9 | /.idea/libraries
10 | /.idea/modules.xml
11 | /.idea/workspace.xml
12 | /.idea/navEditor.xml
13 | /.idea/assetWizardSettings.xml
14 | .DS_Store
15 | build/
16 | /captures
17 | .externalNativeBuild
18 | .cxx
19 |
20 | # Ignoring the persistant lockfile until kotlin.js vulnerabilities are fixed
21 | yarn.lock
22 |
23 | *.xcodeproj
--------------------------------------------------------------------------------
/sample/todo/ios/build.gradle.kts:
--------------------------------------------------------------------------------
1 |
2 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
3 |
4 | plugins {
5 | kotlin("multiplatform")
6 | alias(libs.plugins.jetbrains.compose)
7 | alias(libs.plugins.compose.compiler)
8 | }
9 |
10 | kotlin {
11 | ios("uikit") {
12 | binaries {
13 | executable {
14 | entryPoint = "main"
15 | freeCompilerArgs += listOf(
16 | "-linker-option", "-framework", "-linker-option", "Metal",
17 | "-linker-option", "-framework", "-linker-option", "CoreText",
18 | "-linker-option", "-framework", "-linker-option", "CoreGraphics",
19 | )
20 | // TODO: the current compose binary surprises LLVM, so disable checks for now.
21 | freeCompilerArgs += "-Xdisable-phases=VerifyBitcode"
22 | }
23 | }
24 | }
25 |
26 | sourceSets {
27 | val commonMain by getting {
28 | dependencies {
29 | implementation(project(":sample:todo:common"))
30 | }
31 | }
32 |
33 | val commonTest by getting {
34 | dependencies {
35 | implementation(kotlin("test"))
36 | }
37 | }
38 | }
39 | }
40 |
41 | // compose.experimental {
42 | // uikit.application {
43 | // bundleIdPrefix = "moe.tlaster"
44 | // projectName = "PreComposeSample"
45 | // deployConfigurations {
46 | // simulator("Simulator") {
47 | // device = org.jetbrains.compose.experimental.dsl.IOSDevices.IPHONE_13_MINI
48 | // }
49 | // }
50 | // }
51 | // }
52 |
53 | kotlin {
54 | targets.withType {
55 | binaries.all {
56 | // TODO: the current compose binary surprises LLVM, so disable checks for now.
57 | freeCompilerArgs += "-Xdisable-phases=VerifyBitcode"
58 | binaryOptions["memoryModel"] = "experimental"
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/sample/todo/ios/plists/Ios/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | UILaunchStoryboardName
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/sample/todo/ios/src/uikitMain/kotlin/Main.uikit.kt:
--------------------------------------------------------------------------------
1 |
2 | import androidx.compose.foundation.background
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.Spacer
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.material.MaterialTheme
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.unit.dp
10 | import androidx.compose.ui.window.ComposeUIViewController
11 | import kotlinx.cinterop.BetaInteropApi
12 | import kotlinx.cinterop.ExperimentalForeignApi
13 | import kotlinx.cinterop.autoreleasepool
14 | import kotlinx.cinterop.cstr
15 | import kotlinx.cinterop.memScoped
16 | import kotlinx.cinterop.toCValues
17 | import moe.tlaster.common.App
18 | import moe.tlaster.common.di.AppModule
19 | import org.koin.core.context.startKoin
20 | import platform.Foundation.NSStringFromClass
21 | import platform.UIKit.UIApplication
22 | import platform.UIKit.UIApplicationDelegateProtocol
23 | import platform.UIKit.UIApplicationDelegateProtocolMeta
24 | import platform.UIKit.UIApplicationMain
25 | import platform.UIKit.UIResponder
26 | import platform.UIKit.UIResponderMeta
27 | import platform.UIKit.UIScreen
28 | import platform.UIKit.UIWindow
29 |
30 | @OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
31 | fun main() {
32 | val args = emptyArray()
33 | memScoped {
34 | val argc = args.size + 1
35 | val argv = (arrayOf("skikoApp") + args).map { it.cstr.ptr }.toCValues()
36 | autoreleasepool {
37 | UIApplicationMain(argc, argv, null, NSStringFromClass(SkikoAppDelegate))
38 | }
39 | }
40 | }
41 |
42 | class SkikoAppDelegate : UIResponder, UIApplicationDelegateProtocol {
43 | companion object : UIResponderMeta(), UIApplicationDelegateProtocolMeta
44 |
45 | @OptIn(BetaInteropApi::class)
46 | @OverrideInit
47 | constructor() : super()
48 |
49 | private var _window: UIWindow? = null
50 | override fun window() = _window
51 | override fun setWindow(window: UIWindow?) {
52 | _window = window
53 | }
54 |
55 | @OptIn(ExperimentalForeignApi::class)
56 | override fun application(application: UIApplication, didFinishLaunchingWithOptions: Map?): Boolean {
57 | startKoin {
58 | modules(AppModule.appModule)
59 | allowOverride(false)
60 | }
61 | window = UIWindow(frame = UIScreen.mainScreen.bounds).apply {
62 | rootViewController = ComposeUIViewController {
63 | Column {
64 | Spacer(
65 | modifier = Modifier
66 | .height(36.dp)
67 | .fillMaxWidth()
68 | .background(MaterialTheme.colors.primaryVariant),
69 | )
70 | App()
71 | }
72 | }
73 | makeKeyAndVisible()
74 | }
75 | return true
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/sample/todo/js/.gitignore:
--------------------------------------------------------------------------------
1 | prebuilds/fullsdk-linux
2 | prebuilds/fullsdk-darwin
3 | out
4 | *.iml
5 | .gradle
6 | /local.properties
7 | /.idea
8 | /.idea/caches
9 | /.idea/libraries
10 | /.idea/modules.xml
11 | /.idea/workspace.xml
12 | /.idea/navEditor.xml
13 | /.idea/assetWizardSettings.xml
14 | .DS_Store
15 | build/
16 | /captures
17 | .externalNativeBuild
18 | .cxx
19 |
20 | # Ignoring the persistant lockfile until kotlin.js vulnerabilities are fixed
21 | yarn.lock
22 |
23 | *.xcodeproj
--------------------------------------------------------------------------------
/sample/todo/js/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | alias(libs.plugins.jetbrains.compose)
4 | alias(libs.plugins.compose.compiler)
5 | }
6 |
7 | val resourcesDir = "$buildDir/resources/"
8 |
9 | val skikoWasm by configurations.creating
10 |
11 | dependencies {
12 | skikoWasm(libs.skiko.js)
13 | }
14 |
15 | val unzipTask = tasks.register("unzipWasm", Copy::class) {
16 | destinationDir = file(resourcesDir)
17 | from(skikoWasm.map { zipTree(it) })
18 | }
19 |
20 | tasks.withType().configureEach {
21 | dependsOn(unzipTask)
22 | }
23 |
24 | kotlin {
25 | js(IR) {
26 | browser()
27 | binaries.executable()
28 | }
29 |
30 | sourceSets {
31 | val commonMain by getting {
32 | dependencies {
33 | implementation(project(":sample:todo:common"))
34 | implementation(compose.ui)
35 | implementation(compose.html.core)
36 | implementation(libs.skiko)
37 | }
38 |
39 | resources.setSrcDirs(resources.srcDirs)
40 | resources.srcDirs(unzipTask.map { it.destinationDir })
41 | }
42 |
43 | val commonTest by getting {
44 | dependencies {
45 | implementation(kotlin("test"))
46 | }
47 | }
48 |
49 | val jsMain by getting {
50 | dependsOn(commonMain)
51 | }
52 | }
53 | }
54 |
55 | compose.experimental {
56 | web.application {}
57 | }
58 |
59 | tasks.withType {
60 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE
61 | }
62 |
63 | afterEvaluate {
64 | rootProject.extensions.configure {
65 | versions.webpackCli.version = libs.versions.webpackCliVersion.get()
66 | version = libs.versions.nodeVersion.get()
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/sample/todo/js/src/jsMain/kotlin/Main.kt:
--------------------------------------------------------------------------------
1 |
2 | import androidx.compose.ui.ExperimentalComposeUiApi
3 | import androidx.compose.ui.window.CanvasBasedWindow
4 | import moe.tlaster.common.App
5 | import moe.tlaster.common.di.AppModule
6 | import org.jetbrains.skiko.wasm.onWasmReady
7 | import org.koin.core.context.startKoin
8 |
9 | @OptIn(ExperimentalComposeUiApi::class)
10 | fun main() {
11 | startKoin {
12 | modules(AppModule.appModule)
13 | }
14 | onWasmReady {
15 | CanvasBasedWindow(
16 | title = "Sample",
17 | ) {
18 | App()
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sample/todo/js/src/jsMain/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Sample
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/sample/todo/js/src/jsMain/resources/styles.css:
--------------------------------------------------------------------------------
1 | #root {
2 | width: 100%;
3 | height: 100vh;
4 | }
5 |
6 | #root > .compose-web-column > div {
7 | position: relative;
8 | }
9 |
10 | html, body {
11 | overflow: hidden;
12 | margin: 0 !important;
13 | padding: 0 !important;
14 | font-family: Arial, sans-serif;
15 | color: #3e4349;
16 | }
17 |
--------------------------------------------------------------------------------
/sample/todo/macos/.gitignore:
--------------------------------------------------------------------------------
1 | prebuilds/fullsdk-linux
2 | prebuilds/fullsdk-darwin
3 | out
4 | *.iml
5 | .gradle
6 | /local.properties
7 | /.idea
8 | /.idea/caches
9 | /.idea/libraries
10 | /.idea/modules.xml
11 | /.idea/workspace.xml
12 | /.idea/navEditor.xml
13 | /.idea/assetWizardSettings.xml
14 | .DS_Store
15 | build/
16 | /captures
17 | .externalNativeBuild
18 | .cxx
19 |
20 | # Ignoring the persistant lockfile until kotlin.js vulnerabilities are fixed
21 | yarn.lock
22 |
23 | *.xcodeproj
--------------------------------------------------------------------------------
/sample/todo/macos/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.compose.compose
2 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
3 |
4 | plugins {
5 | kotlin("multiplatform")
6 | alias(libs.plugins.jetbrains.compose)
7 | alias(libs.plugins.compose.compiler)
8 | }
9 |
10 | kotlin {
11 | macosX64 {
12 | binaries {
13 | executable {
14 | entryPoint = "main"
15 | freeCompilerArgs += listOf(
16 | "-linker-option", "-framework", "-linker-option", "Metal",
17 | )
18 | }
19 | }
20 | }
21 | macosArm64 {
22 | binaries {
23 | executable {
24 | entryPoint = "main"
25 | freeCompilerArgs += listOf(
26 | "-linker-option", "-framework", "-linker-option", "Metal",
27 | )
28 | }
29 | }
30 | }
31 |
32 | sourceSets {
33 | val commonMain by getting {
34 | dependencies {
35 | implementation(project(":sample:todo:common"))
36 | }
37 | }
38 |
39 | val commonTest by getting {
40 | dependencies {
41 | implementation(kotlin("test"))
42 | }
43 | }
44 | val macosMain by creating {
45 | dependsOn(commonMain)
46 | }
47 | val macosX64Main by getting {
48 | dependsOn(macosMain)
49 | }
50 | val macosArm64Main by getting {
51 | dependsOn(macosMain)
52 | }
53 | }
54 | }
55 |
56 | compose.desktop.nativeApplication {
57 | targets(kotlin.targets.getByName("macosX64"), kotlin.targets.getByName("macosArm64"))
58 | distributions {
59 | targetFormats(org.jetbrains.compose.desktop.application.dsl.TargetFormat.Dmg)
60 | packageName = "PreComposeSample"
61 | packageVersion = "1.0.0"
62 | }
63 | }
64 |
65 | kotlin {
66 | targets.withType {
67 | binaries.all {
68 | // TODO: the current compose binary surprises LLVM, so disable checks for now.
69 | freeCompilerArgs += "-Xdisable-phases=VerifyBitcode"
70 | binaryOptions["memoryModel"] = "experimental"
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/sample/todo/macos/src/macosMain/kotlin/Main.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.runtime.ExperimentalComposeApi
2 | import moe.tlaster.common.App
3 | import moe.tlaster.common.di.AppModule
4 | import moe.tlaster.precompose.PreComposeWindow
5 | import org.koin.core.context.startKoin
6 | import platform.AppKit.NSApp
7 |
8 | @OptIn(ExperimentalComposeApi::class)
9 | fun main() {
10 | startKoin {
11 | modules(AppModule.appModule)
12 | }
13 | PreComposeWindow(
14 | "PreComposeSample",
15 | // onCloseRequest = {
16 | // NSApp?.terminate(null)
17 | // },
18 | ) {
19 | App()
20 | }
21 | NSApp?.run()
22 | }
23 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
7 | }
8 | }
9 |
10 | dependencyResolutionManagement {
11 | // https://youtrack.jetbrains.com/issue/KT-51379
12 | // repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
13 | repositories {
14 | google()
15 | mavenCentral()
16 | maven("https://maven.mozilla.org/maven2/")
17 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
18 | maven("https://jitpack.io")
19 | // TODO: delete when we have all libs in mavenCentral
20 | maven("https://maven.pkg.jetbrains.space/kotlin/p/wasm/experimental")
21 | }
22 | }
23 | rootProject.name = "precompose"
24 |
25 | include(":precompose")
26 | include(":precompose-molecule")
27 | include(":sample:todo:android")
28 | include(":sample:todo:desktop")
29 | include(":sample:todo:common")
30 | include(":sample:todo:ios")
31 | // include(":sample:todo:macos")
32 | include(":sample:todo:js")
33 | include(":sample:molecule:composeApp")
34 |
--------------------------------------------------------------------------------