├── .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 | 13 | 14 | 15 | 17 | 18 | 19 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/ktlint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/moe.tlaster/precompose/badge.svg)](https://maven-badges.herokuapp.com/maven-central/moe.tlaster/precompose) 3 | [![compose-jb-version](https://img.shields.io/badge/compose--jb-1.6.10-blue)](https://github.com/JetBrains/compose-jb) 4 | ![license](https://img.shields.io/github/license/Tlaster/PreCompose) 5 | 6 | ![badge-Android](https://img.shields.io/badge/Platform-Android-brightgreen) 7 | ![badge-iOS](https://img.shields.io/badge/Platform-iOS-lightgray) 8 | ![badge-JVM](https://img.shields.io/badge/Platform-JVM-orange) 9 | ![badge-macOS](https://img.shields.io/badge/Platform-macOS-purple) 10 | ![badge-web](https://img.shields.io/badge/Platform-Web-blue) 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 | JetBrains Logo (Main) logo. 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 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/moe.tlaster/precompose-koin/badge.svg)](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 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/moe.tlaster/precompose-molecule/badge.svg)](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 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/moe.tlaster/precompose-viewmodel/badge.svg)](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 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/moe.tlaster/precompose/badge.svg)](https://maven-badges.herokuapp.com/maven-central/moe.tlaster/precompose) 3 | [![compose-jb-version](https://img.shields.io/badge/compose--jb-1.5.0-blue)](https://github.com/JetBrains/compose-jb) 4 | ![license](https://img.shields.io/github/license/Tlaster/PreCompose) 5 | 6 | ![badge-Android](https://img.shields.io/badge/Platform-Android-brightgreen) 7 | ![badge-iOS](https://img.shields.io/badge/Platform-iOS-lightgray) 8 | ![badge-JVM](https://img.shields.io/badge/Platform-JVM-orange) 9 | ![badge-macOS](https://img.shields.io/badge/Platform-macOS-purple) 10 | ![badge-web](https://img.shields.io/badge/Platform-Web-blue) 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 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/moe.tlaster/precompose/badge.svg)](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 | --------------------------------------------------------------------------------