├── .editorconfig
├── .github
├── dependabot.yml
└── workflows
│ ├── build.yml
│ ├── publish-release.yml
│ └── sample.yml
├── .gitignore
├── .idea
├── .gitignore
├── appInsightsSettings.xml
├── caches
│ └── deviceStreaming.xml
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── deploymentTargetDropDown.xml
├── deploymentTargetSelector.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── jsonSchemas.xml
├── kotlinc.xml
├── misc.xml
├── render.experimental.xml
└── vcs.xml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── RELEASING.md
├── build.gradle.kts
├── channel-event-bus
├── api
│ └── channel-event-bus.api
├── build.gradle.kts
├── gradle.properties
└── src
│ ├── commonMain
│ └── kotlin
│ │ └── com
│ │ └── hoc081098
│ │ └── channeleventbus
│ │ ├── ChannelEvent.kt
│ │ ├── ChannelEventBus.kt
│ │ ├── ChannelEventBusCapacity.kt
│ │ ├── ChannelEventBusException.kt
│ │ ├── ChannelEventBusLogger.kt
│ │ ├── OptionWhenSendingToBusDoesNotExist.kt
│ │ └── ValidationBeforeClosing.kt
│ ├── commonTest
│ └── kotlin
│ │ └── com
│ │ └── hoc081098
│ │ └── channeleventbus
│ │ ├── ChannelEventBusTest.kt
│ │ ├── ChannelEventTest.kt
│ │ └── events.kt
│ └── jvmTest
│ └── kotlin
│ └── com
│ └── hoc081098
│ └── channeleventbus
│ └── ChannelEventBusJvmTest.kt
├── detekt.yml
├── docs
├── .gitkeep
└── images
│ └── logo.jpeg
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── kotlin-js-store
└── yarn.lock
├── logo.png
├── mkdocs.yml
├── renovate.json
├── sample
├── standalone-androidApp
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── com
│ │ │ └── hoc081098
│ │ │ └── channeleventbus
│ │ │ └── sample
│ │ │ └── android
│ │ │ ├── MainActivity.kt
│ │ │ ├── MyApp.kt
│ │ │ ├── Route.kt
│ │ │ ├── common
│ │ │ ├── CollectWithLifecycleEffect.kt
│ │ │ ├── MyApplicationTheme.kt
│ │ │ ├── OnLifecycleEvent.kt
│ │ │ ├── SingleEventChannel.kt
│ │ │ └── debugCheckImmediateMainDispatcher.kt
│ │ │ ├── ui
│ │ │ ├── home
│ │ │ │ ├── detail
│ │ │ │ │ ├── DetailScreen.kt
│ │ │ │ │ └── DetailVM.kt
│ │ │ │ ├── di.kt
│ │ │ │ ├── events.kt
│ │ │ │ └── home
│ │ │ │ │ ├── HomeScreen.kt
│ │ │ │ │ └── HomeVM.kt
│ │ │ └── register
│ │ │ │ ├── RegisterSharedVM.kt
│ │ │ │ ├── contract.kt
│ │ │ │ ├── di.kt
│ │ │ │ ├── events.kt
│ │ │ │ ├── stepone
│ │ │ │ ├── RegisterStepOneScreen.kt
│ │ │ │ └── RegisterStepOneVM.kt
│ │ │ │ ├── stepthree
│ │ │ │ ├── RegisterStepThreeScreen.kt
│ │ │ │ └── RegisterStepThreeVM.kt
│ │ │ │ └── steptwo
│ │ │ │ ├── RegisterStepTwoScreen.kt
│ │ │ │ └── RegisterStepTwoVM.kt
│ │ │ └── utils
│ │ │ ├── NonBlankString.kt
│ │ │ └── launchNow.kt
│ │ └── res
│ │ └── values
│ │ └── styles.xml
└── standalone-composeMultiplatform
│ ├── androidApp
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── com
│ │ │ └── hoc081098
│ │ │ └── channeleventbus
│ │ │ └── sample
│ │ │ └── kmp
│ │ │ └── compose
│ │ │ └── android
│ │ │ ├── MainActivity.kt
│ │ │ └── MyApp.kt
│ │ └── res
│ │ └── values
│ │ └── styles.xml
│ ├── composeApp
│ ├── build.gradle.kts
│ └── src
│ │ ├── androidMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── hoc081098
│ │ │ └── channeleventbus
│ │ │ └── sample
│ │ │ └── kmp
│ │ │ └── compose
│ │ │ └── common
│ │ │ ├── debugCheckImmediateMainDispatcher.android.kt
│ │ │ └── identityHashCode.android.kt
│ │ ├── commonMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── hoc081098
│ │ │ └── channeleventbus
│ │ │ └── sample
│ │ │ └── kmp
│ │ │ └── compose
│ │ │ ├── ChannelEventBusSampleApp.kt
│ │ │ ├── common
│ │ │ ├── CollectWithLifecycleEffect.kt
│ │ │ ├── MyApplicationTheme.kt
│ │ │ ├── OnLifecycleEvent.kt
│ │ │ ├── SingleEventChannel.kt
│ │ │ ├── debugCheckImmediateMainDispatcher.kt
│ │ │ ├── identityHashCode.kt
│ │ │ └── rememberSharedViewModelOnRoute.kt
│ │ │ ├── di.kt
│ │ │ ├── ui
│ │ │ ├── home
│ │ │ │ ├── detail
│ │ │ │ │ ├── DetailScreen.kt
│ │ │ │ │ ├── DetailScreenRoute.kt
│ │ │ │ │ └── DetailVM.kt
│ │ │ │ ├── di.kt
│ │ │ │ ├── events.kt
│ │ │ │ └── home
│ │ │ │ │ ├── HomeScreen.kt
│ │ │ │ │ ├── HomeScreenRoute.kt
│ │ │ │ │ └── HomeVM.kt
│ │ │ └── register
│ │ │ │ ├── RegisterSharedVM.kt
│ │ │ │ ├── contract.kt
│ │ │ │ ├── di.kt
│ │ │ │ ├── events.kt
│ │ │ │ ├── stepone
│ │ │ │ ├── RegisterStepOneScreen.kt
│ │ │ │ ├── RegisterStepOneScreenRoute.kt
│ │ │ │ └── RegisterStepOneVM.kt
│ │ │ │ ├── stepthree
│ │ │ │ ├── RegisterStepThreeScreen.kt
│ │ │ │ ├── RegisterStepThreeScreenRoute.kt
│ │ │ │ └── RegisterStepThreeVM.kt
│ │ │ │ └── steptwo
│ │ │ │ ├── RegisterStepTwoScreen.kt
│ │ │ │ ├── RegisterStepTwoScreenRoute.kt
│ │ │ │ └── RegisterStepTwoVM.kt
│ │ │ └── utils
│ │ │ ├── NonBlankString.kt
│ │ │ └── launchNow.kt
│ │ └── desktopMain
│ │ └── kotlin
│ │ └── com
│ │ └── hoc081098
│ │ └── channeleventbus
│ │ └── sample
│ │ └── kmp
│ │ └── compose
│ │ └── common
│ │ ├── debugCheckImmediateMainDispatcher.desktop.kt
│ │ └── identityHashCode.desktop.kt
│ └── desktopApp
│ ├── build.gradle.kts
│ └── src
│ └── desktopMain
│ └── kotlin
│ └── com
│ └── hoc081098
│ └── channeleventbus
│ └── sample
│ └── kmp
│ └── compose
│ └── main.kt
├── scripts
└── update_docs_url.sh
└── settings.gradle.kts
/.editorconfig:
--------------------------------------------------------------------------------
1 | root=true
2 | [*]
3 | indent_size=2
4 | end_of_line=lf
5 | charset=utf-8
6 | trim_trailing_whitespace=true
7 | insert_final_newline=true
8 | [*.{kt,kts}]
9 | ij_kotlin_imports_layout=*
10 | ij_continuation_indent_size=4
11 | ktlint_standard_filename=disabled
12 | ktlint_standard_package-name=disabled
13 | ktlint_standard_property-naming=disabled
14 | ktlint_standard_function-naming=disabled
15 | filename=disabled
16 | ktlint_experimental=enabled
17 | ij_kotlin_name_count_to_use_star_import=999
18 | ij_kotlin_name_count_to_use_star_import_for_members=999
19 | [*.xml]
20 | indent_size=4
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "gradle" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "weekly"
12 |
--------------------------------------------------------------------------------
/.github/workflows/publish-release.yml:
--------------------------------------------------------------------------------
1 | name: Publish Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '**'
7 |
8 | env:
9 | GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"
10 |
11 | permissions:
12 | contents: write
13 |
14 | jobs:
15 | create-gh-release:
16 | if: ${{ github.repository == 'Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus' }}
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v3
21 |
22 | - name: Extract release notes
23 | id: release_notes
24 | uses: ffurrer2/extract-release-notes@v1
25 |
26 | - name: Create release
27 | uses: softprops/action-gh-release@v1
28 | with:
29 | body: ${{ steps.release_notes.outputs.release_notes }}
30 | env:
31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32 |
33 | publish:
34 | needs: create-gh-release
35 | if: ${{ github.repository == 'Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus' }}
36 | strategy:
37 | matrix:
38 | os: [ macos-latest ]
39 | runs-on: ${{ matrix.os }}
40 | steps:
41 | - name: Checkout
42 | uses: actions/checkout@v3
43 |
44 | - name: Set up JDK
45 | uses: actions/setup-java@v3
46 | with:
47 | distribution: 'zulu'
48 | java-version: '21'
49 |
50 | - name: Cache gradle, wrapper and buildSrc
51 | uses: actions/cache@v3
52 | with:
53 | path: |
54 | ~/.gradle/caches
55 | ~/.gradle/wrapper
56 | key: ${{ matrix.os }}-gradle-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/buildSrc/**/*.kt') }}
57 | restore-keys: |
58 | ${{ matrix.os }}-gradle-
59 | - name: Cache konan
60 | uses: actions/cache@v3
61 | with:
62 | path: |
63 | ~/.konan/cache
64 | ~/.konan/dependencies
65 | ~/.konan/kotlin-native-macos*
66 | ~/.konan/kotlin-native-mingw*
67 | ~/.konan/kotlin-native-windows*
68 | ~/.konan/kotlin-native-linux*
69 | ~/.konan/kotlin-native-prebuilt-macos*
70 | ~/.konan/kotlin-native-prebuilt-mingw*
71 | ~/.konan/kotlin-native-prebuilt-windows*
72 | ~/.konan/kotlin-native-prebuilt-linux*
73 | key: ${{ matrix.os }}-konan-${{ hashFiles('**/*.gradle*') }}
74 | restore-keys: |
75 | ${{ matrix.os }}-konan-
76 | - name: Make gradlew executable
77 | run: chmod +x ./gradlew
78 |
79 | - name: Build release
80 | run: ./gradlew :channel-event-bus:assemble
81 |
82 | - name: Publish release
83 | run: ./gradlew publish --stacktrace
84 | if: contains(matrix.os, 'macos')
85 | env:
86 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
87 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
88 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.MAVEN_SIGNING_PRIVATE_KEY }}
89 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.MAVEN_SIGNING_PASSWORD }}
90 |
91 | - name: Install Python
92 | uses: actions/setup-python@v4
93 | with:
94 | python-version: 3.x
95 |
96 | - name: Install MkDocs Material
97 | run: pip install mkdocs-material
98 |
99 | - name: Generate docs
100 | run: ./gradlew :dokkaHtmlMultiModule --no-parallel --stacktrace
101 |
102 | - name: Copy docs
103 | run: |
104 | cp README.md docs/index.md
105 | mkdir -p docs/API
106 | cp -R build/dokka/htmlMultiModule/. docs/API
107 |
108 | - name: Build MkDocs
109 | run: mkdocs build
110 |
111 | - name: Deploy docs 🚀 to website
112 | if: ${{ contains(matrix.os, 'macos') }}
113 | uses: JamesIves/github-pages-deploy-action@v4.6.9
114 | with:
115 | branch: gh-pages # The branch the action should deploy to.
116 | folder: site # The folder the action should deploy.
117 | target-folder: docs/0.x/
118 | clean: true
119 |
--------------------------------------------------------------------------------
/.github/workflows/sample.yml:
--------------------------------------------------------------------------------
1 | name: Build sample
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | paths-ignore: [ '**.md', '**.MD' ]
7 | tags-ignore:
8 | - '**'
9 | pull_request:
10 | branches: [ master ]
11 | paths-ignore: [ '**.md', '**.MD' ]
12 | workflow_dispatch:
13 |
14 | env:
15 | GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false -XX:+UseParallelGC -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8"
16 |
17 | jobs:
18 | android-android-kmpcompose:
19 | strategy:
20 | matrix:
21 | os: [ ubuntu-latest ]
22 | runs-on: ${{ matrix.os }}
23 | steps:
24 | - uses: actions/checkout@v3
25 |
26 | - name: Set up JDK
27 | uses: actions/setup-java@v3
28 | with:
29 | distribution: 'zulu'
30 | java-version: '21'
31 |
32 | - name: Cache gradle, wrapper and buildSrc
33 | uses: actions/cache@v3
34 | with:
35 | path: |
36 | ~/.gradle/caches
37 | ~/.gradle/wrapper
38 | key: ${{ matrix.os }}-gradle-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/buildSrc/**/*.kt') }}
39 | restore-keys: |
40 | ${{ matrix.os }}-gradle-
41 | - name: Cache konan
42 | uses: actions/cache@v3
43 | with:
44 | path: |
45 | ~/.konan/cache
46 | ~/.konan/dependencies
47 | ~/.konan/kotlin-native-macos*
48 | ~/.konan/kotlin-native-mingw*
49 | ~/.konan/kotlin-native-windows*
50 | ~/.konan/kotlin-native-linux*
51 | ~/.konan/kotlin-native-prebuilt-macos*
52 | ~/.konan/kotlin-native-prebuilt-mingw*
53 | ~/.konan/kotlin-native-prebuilt-windows*
54 | ~/.konan/kotlin-native-prebuilt-linux*
55 | key: ${{ matrix.os }}-konan-${{ hashFiles('**/*.gradle*') }}
56 | restore-keys: |
57 | ${{ matrix.os }}-konan-
58 | - name: Make gradlew executable
59 | run: chmod +x ./gradlew
60 |
61 | - name: Build
62 | run: |
63 | ./gradlew \
64 | :sample:standalone-androidApp:assembleDebug \
65 | :sample:standalone-composeMultiplatform:desktopApp:packageDistributionForCurrentOS \
66 | :sample:standalone-composeMultiplatform:androidApp:assembleDebug \
67 | --stacktrace
68 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Project exclude paths
2 | /.gradle/
3 | /build/
4 | /local.properties
5 | .DS_Store
6 | .idea/shelf
7 | /confluence/target
8 | /dependencies/repo
9 | /android.tests.dependencies
10 | /dependencies/android.tests.dependencies
11 | /dist
12 | /local
13 | /gh-pages
14 | /ideaSDK
15 | /clionSDK
16 | /android-studio/sdk
17 | out/
18 | /tmp
19 | kotlin-ide/
20 | workspace.xml
21 | *.versionsBackup
22 | /idea/testData/debugger/tinyApp/classes*
23 | /jps-plugin/testData/kannotator
24 | /js/js.translator/testData/out/
25 | /js/js.translator/testData/out-min/
26 | /js/js.translator/testData/out-pir/
27 | .gradle/
28 | build/
29 | !**/src/**/build
30 | !**/test/**/build
31 | *.iml
32 | !**/testData/**/*.iml
33 | .idea/libraries/Gradle*.xml
34 | .idea/libraries/Maven*.xml
35 | .idea/artifacts/FlowExt_*.xml
36 | .idea/artifacts/KotlinPlugin.xml
37 | .idea/modules
38 | .idea/runConfigurations/JPS_*.xml
39 | .idea/runConfigurations/PILL_*.xml
40 | .idea/runConfigurations/_FP_*.xml
41 | .idea/runConfigurations/_MT_*.xml
42 | .idea/libraries
43 | .idea/modules.xml
44 | .idea/gradle.xml
45 | .idea/compiler.xml
46 | .idea/inspectionProfiles/profiles_settings.xml
47 | .idea/.name
48 | .idea/artifacts/dist_auto_*
49 | .idea/artifacts/dist.xml
50 | .idea/artifacts/ideaPlugin.xml
51 | .idea/artifacts/kotlinc.xml
52 | .idea/artifacts/kotlin_compiler_jar.xml
53 | .idea/artifacts/kotlin_plugin_jar.xml
54 | .idea/artifacts/kotlin_jps_plugin_jar.xml
55 | .idea/artifacts/kotlin_daemon_client_jar.xml
56 | .idea/artifacts/kotlin_imports_dumper_compiler_plugin_jar.xml
57 | .idea/artifacts/kotlin_main_kts_jar.xml
58 | .idea/artifacts/kotlin_compiler_client_embeddable_jar.xml
59 | .idea/artifacts/kotlin_reflect_jar.xml
60 | .idea/artifacts/kotlin_stdlib_js_ir_*
61 | .idea/artifacts/kotlin_test_js_ir_*
62 | .idea/artifacts/kotlin_stdlib_wasm_*
63 | .idea/jarRepositories.xml
64 | .idea/csv-plugin.xml
65 | kotlin-ultimate/
66 | node_modules/
67 | .rpt2_cache/
68 | libraries/tools/kotlin-test-js-runner/lib/
69 | local.properties
70 | buildSrcTmp/
71 | distTmp/
72 | outTmp/
73 | /test.output
74 | /kotlin-native/dist
75 | # Since Kotlin 2.0.0, .kotlin directory is created at compile time
76 | .kotlin
77 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | /artifacts
5 | # GitHub Copilot persisted chat sessions
6 | /copilot/chatSessions
7 |
--------------------------------------------------------------------------------
/.idea/appInsightsSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
25 |
26 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
16 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetSelector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/.idea/jsonSchemas.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/render.experimental.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## [Unreleased] - TBD
4 |
5 | ## [0.1.0] - Aug 31, 2023
6 |
7 | ### This repository was transferred to [Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus](https://github.com/Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus)
8 |
9 | ### Update dependencies
10 |
11 | - [Kotlin `2.0.20` 🎉](https://github.com/JetBrains/kotlin/releases/tag/v2.0.20).
12 | - [KotlinX Coroutines `1.9.0-RC.2`](https://github.com/Kotlin/kotlinx.coroutines/releases/tag/1.9.0-RC.2).
13 |
14 | ### Added
15 |
16 | - **New**: Add support for Kotlin/Wasm (`wasmJs` target) 🎉 in [#41](https://github.com/Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus/pull/41).
17 |
18 | ### Fixed
19 |
20 | - Do not throw `KotlinNullPointerException` in `ChannelEventBusImpl.markAsNotCollecting` method, because `busMap[key]` can be null if it is removed and closed before calling this method in [#52](https://github.com/Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus/pull/52).
21 |
22 | ### Docs
23 |
24 | - Add [JetBrains Compose Multiplatform sample](https://github.com/Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus/tree/master/sample/standalone-composeMultiplatform) in [#52](https://github.com/Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus/pull/52).
25 | - `0.x release` docs: https://kotlin-multiplatform-foundation.github.io/kotlin-channel-event-bus/docs/0.x
26 | - Snapshot docs: https://kotlin-multiplatform-foundation.github.io/kotlin-channel-event-bus/docs/latest/
27 |
28 | ## [0.0.2] - Dec 4, 2023
29 |
30 | ### Added
31 |
32 | - Support more targets:
33 | - `mingwX64`
34 | - `linuxX64`
35 | - `linuxArm64`
36 | - `watchosDeviceArm64`
37 | - `androidNativeArm32`
38 | - `androidNativeArm64`
39 | - `androidNativeX86`
40 | - `androidNativeX64`
41 |
42 | ## [0.0.1] - Dec 3, 2023 🎉
43 |
44 | - Initial release 🎉
45 | - Kotlin `1.9.21`.
46 | - KotlinX Coroutines `1.7.3`.
47 | - Gradle `8.5`.
48 |
49 | [Unreleased]: https://github.com/Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus/compare/0.1.0...HEAD
50 |
51 | [0.1.0]: https://github.com/Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus/releases/tag/0.1.0
52 |
53 | [0.0.2]: https://github.com/Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus/releases/tag/0.0.2
54 |
55 | [0.0.1]: https://github.com/Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus/releases/tag/0.0.1
56 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | # Releasing
2 |
3 | 1. Update the `VERSION_NAME` in `gradle.properties` to the release version.
4 |
5 | 2. Update the `CHANGELOG.md`.
6 |
7 | 3. Update the `README.md` so the "Download" section reflects the new release version and the
8 | snapshot section reflects the next "SNAPSHOT" version.
9 |
10 | 4. Commit
11 |
12 | ```
13 | $ git commit -am "Prepare version X.Y.Z"
14 | ```
15 |
16 | 5. Tag
17 |
18 | ```
19 | $ git tag -am "Version X.Y.Z" X.Y.Z
20 | ```
21 |
22 | 6. Update the `VERSION_NAME` in `gradle.properties` to the next "SNAPSHOT" version.
23 |
24 | 7. Commit
25 |
26 | ```
27 | $ git commit -am "Prepare next development version"
28 | ```
29 |
30 | 8. Push!
31 |
32 | ```
33 | $ git push && git push --tags
34 | ```
35 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.diffplug.gradle.spotless.SpotlessExtension
2 | import com.diffplug.gradle.spotless.SpotlessPlugin
3 | import io.gitlab.arturbosch.detekt.DetektPlugin
4 | import io.gitlab.arturbosch.detekt.extensions.DetektExtension
5 | import org.gradle.api.tasks.testing.logging.TestExceptionFormat
6 | import org.gradle.api.tasks.testing.logging.TestLogEvent
7 |
8 | @Suppress("DSL_SCOPE_VIOLATION")
9 | plugins {
10 | alias(libs.plugins.kotlin.multiplatform) apply false
11 | alias(libs.plugins.kotlin.android) apply false
12 | alias(libs.plugins.kotlin.serialization) apply false
13 | alias(libs.plugins.kotlin.cocoapods) apply false
14 | alias(libs.plugins.kotlin.compose) apply false
15 | alias(libs.plugins.kotlin.parcelize) apply false
16 |
17 | alias(libs.plugins.jetbrains.compose) apply false
18 |
19 | alias(libs.plugins.android.app) apply false
20 | alias(libs.plugins.android.library) apply false
21 |
22 | alias(libs.plugins.gradle.spotless) apply false
23 | alias(libs.plugins.detekt) apply false
24 | alias(libs.plugins.kotlinx.binary.compatibility.validator) apply false
25 | alias(libs.plugins.kotlinx.kover)
26 | alias(libs.plugins.dokka)
27 |
28 | alias(libs.plugins.vanniktech.maven.publish) apply false
29 | }
30 |
31 | subprojects {
32 | apply()
33 | configure {
34 | source.from(files("src/"))
35 | config.from(files("${project.rootDir}/detekt.yml"))
36 | buildUponDefaultConfig = true
37 | allRules = true
38 | }
39 | afterEvaluate {
40 | dependencies {
41 | "detektPlugins"(libs.compose.rules.detekt)
42 | }
43 | }
44 | }
45 |
46 | dependencies {
47 | kover(project(":channel-event-bus"))
48 | }
49 |
50 | val ktlintVersion = libs.versions.ktlint.get()
51 |
52 | allprojects {
53 | apply()
54 | configure {
55 | kotlin {
56 | target("**/*.kt")
57 |
58 | ktlint(ktlintVersion)
59 |
60 | trimTrailingWhitespace()
61 | indentWithSpaces()
62 | endWithNewline()
63 | }
64 |
65 | format("xml") {
66 | target("**/res/**/*.xml")
67 |
68 | trimTrailingWhitespace()
69 | indentWithSpaces()
70 | endWithNewline()
71 | }
72 |
73 | kotlinGradle {
74 | target("**/*.gradle.kts", "*.gradle.kts")
75 |
76 | ktlint(ktlintVersion)
77 |
78 | trimTrailingWhitespace()
79 | indentWithSpaces()
80 | endWithNewline()
81 | }
82 | }
83 |
84 | tasks.withType {
85 | testLogging {
86 | showExceptions = true
87 | showCauses = true
88 | showStackTraces = true
89 | showStandardStreams = true
90 | events = setOf(
91 | TestLogEvent.PASSED,
92 | TestLogEvent.FAILED,
93 | TestLogEvent.SKIPPED,
94 | TestLogEvent.STANDARD_OUT,
95 | TestLogEvent.STANDARD_ERROR,
96 | )
97 | exceptionFormat = TestExceptionFormat.FULL
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/channel-event-bus/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("ClassName")
2 |
3 | import java.net.URL
4 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
5 |
6 | @Suppress("DSL_SCOPE_VIOLATION")
7 | plugins {
8 | alias(libs.plugins.kotlin.multiplatform)
9 | alias(libs.plugins.vanniktech.maven.publish)
10 | alias(libs.plugins.dokka)
11 | alias(libs.plugins.kotlinx.binary.compatibility.validator)
12 | alias(libs.plugins.kotlinx.kover)
13 | }
14 |
15 | kotlin {
16 | explicitApi()
17 |
18 | jvmToolchain {
19 | languageVersion = JavaLanguageVersion.of(libs.versions.java.toolchain.get())
20 | vendor = JvmVendorSpec.AZUL
21 | }
22 |
23 | jvm {
24 | compilations.configureEach {
25 | compileTaskProvider.configure {
26 | compilerOptions {
27 | jvmTarget = JvmTarget.fromTarget(libs.versions.java.target.get())
28 | }
29 | }
30 | }
31 | }
32 |
33 | js(IR) {
34 | moduleName = property("POM_ARTIFACT_ID")!!.toString()
35 | compilations.configureEach {
36 | compileTaskProvider.configure {
37 | compilerOptions {
38 | sourceMap = true
39 | moduleKind = org.jetbrains.kotlin.gradle.dsl.JsModuleKind.MODULE_COMMONJS
40 | }
41 | }
42 | }
43 | browser()
44 | nodejs()
45 | }
46 | @OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
47 | wasmJs {
48 | // Module name should be different from the one from JS
49 | // otherwise IC tasks that start clashing different modules with the same module name
50 | moduleName = property("POM_ARTIFACT_ID")!!.toString() + "Wasm"
51 | browser()
52 | nodejs()
53 | }
54 |
55 | // According to https://kotlinlang.org/docs/native-target-support.html
56 | // Tier 1: macosX64, macosArm64, iosSimulatorArm64, iosX64
57 | // Tier 2: linuxX64, linuxArm64,
58 | // watchosSimulatorArm64, watchosX64, watchosArm32, watchosArm64,
59 | // tvosSimulatorArm64, tvosX64, tvosArm64,
60 | // iosArm64
61 | // Tier 3: androidNativeArm32, androidNativeArm64, androidNativeX86, androidNativeX64,
62 | // mingwX64,
63 | // watchosDeviceArm64
64 |
65 | iosArm64()
66 | iosX64()
67 | iosSimulatorArm64()
68 |
69 | macosX64()
70 | macosArm64()
71 | mingwX64()
72 | linuxX64()
73 | linuxArm64()
74 |
75 | tvosX64()
76 | tvosSimulatorArm64()
77 | tvosArm64()
78 |
79 | watchosArm32()
80 | watchosArm64()
81 | watchosX64()
82 | watchosSimulatorArm64()
83 | watchosDeviceArm64()
84 |
85 | androidNativeArm32()
86 | androidNativeArm64()
87 | androidNativeX86()
88 | androidNativeX64()
89 |
90 | sourceSets {
91 | commonMain {
92 | dependencies {
93 | api(libs.coroutines.core)
94 | }
95 | }
96 | commonTest {
97 | dependencies {
98 | implementation(kotlin("test-common"))
99 | implementation(kotlin("test-annotations-common"))
100 |
101 | implementation(libs.coroutines.test)
102 | implementation(libs.flowExt)
103 | }
104 | }
105 |
106 | jsTest {
107 | dependencies {
108 | implementation(kotlin("test-js"))
109 | }
110 | }
111 |
112 | val wasmJsTest by getting {
113 | dependencies {
114 | implementation(kotlin("test-wasm-js"))
115 | }
116 | }
117 |
118 | jvmTest {
119 | dependencies {
120 | implementation(kotlin("test-junit"))
121 | }
122 | }
123 | }
124 |
125 | sourceSets.matching { it.name.contains("Test") }.all {
126 | languageSettings {
127 | optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
128 | }
129 | }
130 | }
131 |
132 | mavenPublishing {
133 | publishToMavenCentral(com.vanniktech.maven.publish.SonatypeHost.S01, automaticRelease = true)
134 | signAllPublications()
135 | }
136 |
137 | tasks.withType().configureEach {
138 | dokkaSourceSets {
139 | configureEach {
140 | externalDocumentationLink("https://kotlinlang.org/api/kotlinx.coroutines/")
141 |
142 | sourceLink {
143 | localDirectory = projectDir.resolve("src")
144 | remoteUrl =
145 | URL("https://github.com/Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus/tree/master/channel-event-bus/src")
146 | remoteLineSuffix = "#L"
147 | }
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/channel-event-bus/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_NAME=KotlinChannelEventBus
2 | POM_ARTIFACT_ID=channel-event-bus
3 | POM_DESCRIPTION=Kotlin Channel EventBus. Multi-keys, multi-producers, single-consumer event bus backed by Kotlinx Coroutines Channels.
4 |
--------------------------------------------------------------------------------
/channel-event-bus/src/commonMain/kotlin/com/hoc081098/channeleventbus/ChannelEvent.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus
2 |
3 | import kotlin.jvm.JvmField
4 | import kotlin.jvm.JvmSynthetic
5 | import kotlin.reflect.KClass
6 | import kotlin.reflect.cast
7 | import kotlinx.coroutines.channels.Channel
8 |
9 | /**
10 | * Represents an event that can be sent to a [ChannelEventBus].
11 | */
12 | public interface ChannelEvent> {
13 | /**
14 | * The key to identify a bus for this type of events.
15 | */
16 | public val key: Key
17 |
18 | /**
19 | * The [ChannelEvent.Key] to identify a bus for this type of events.
20 | *
21 | * @param T the type of events.
22 | * @param eventClass the [KClass] of events.
23 | * @param capacity the [ChannelEventBusCapacity] of the [Channel] associated with this key.
24 | * Default is [ChannelEventBusCapacity.UNLIMITED].
25 | *
26 | * @see [ChannelEventBusCapacity]
27 | */
28 | public open class Key>(
29 | @JvmField
30 | internal val eventClass: KClass,
31 | @JvmField
32 | internal val capacity: ChannelEventBusCapacity = ChannelEventBusCapacity.UNLIMITED,
33 | ) {
34 | final override fun equals(other: Any?): Boolean {
35 | if (this === other) return true
36 | if (other !is Key<*>) return false
37 | return eventClass == other.eventClass && capacity == other.capacity
38 | }
39 |
40 | final override fun hashCode(): Int = 31 * eventClass.hashCode() + capacity.hashCode()
41 |
42 | final override fun toString(): String = "ChannelEvent.Key(${eventClass.simpleName}, $capacity)"
43 |
44 | @JvmSynthetic
45 | internal inline fun cast(it: Any): T = eventClass.cast(it)
46 |
47 | @JvmSynthetic
48 | internal inline fun createChannel(): Channel = Channel(capacity.asInt())
49 | }
50 | }
51 |
52 | /**
53 | * Alias for [ChannelEvent.Key].
54 | * @see [ChannelEvent.Key]
55 | */
56 | public typealias ChannelEventKey = ChannelEvent.Key
57 |
58 | /**
59 | * Retrieve the [ChannelEvent.Key] for [T].
60 | *
61 | * You should cache the result of this function to avoid unnecessary object creation, for example:
62 | *
63 | * ```kotlin
64 | * @JvmField
65 | * val awesomeEventKey = channelEventKeyOf()
66 | * bus.receiveAsFlow(awesomeEventKey).collect { e: AwesomeEvent -> println(e) }
67 | * ```
68 | */
69 | public inline fun > channelEventKeyOf(
70 | capacity: ChannelEventBusCapacity = ChannelEventBusCapacity.UNLIMITED,
71 | ): ChannelEventKey = ChannelEventKey(T::class, capacity)
72 |
--------------------------------------------------------------------------------
/channel-event-bus/src/commonMain/kotlin/com/hoc081098/channeleventbus/ChannelEventBusCapacity.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus
2 |
3 | import kotlin.jvm.JvmSynthetic
4 | import kotlinx.coroutines.channels.BufferOverflow
5 | import kotlinx.coroutines.channels.Channel
6 |
7 | @JvmSynthetic
8 | internal inline fun ChannelEventBusCapacity.asInt(): Int {
9 | return when (this) {
10 | ChannelEventBusCapacity.UNLIMITED -> Channel.UNLIMITED
11 | ChannelEventBusCapacity.CONFLATED -> Channel.CONFLATED
12 | }
13 | }
14 |
15 | /**
16 | * The capacity of the [Channel] associated with a [ChannelEvent.Key].
17 | *
18 | * @see [ChannelEvent.Key.capacity]
19 | */
20 | public enum class ChannelEventBusCapacity {
21 | /**
22 | * The [Channel] is unbounded [Channel.UNLIMITED].
23 | *
24 | * The [Channel] associated with a [ChannelEvent.Key] will has an unlimited capacity buffer.
25 | * This means that the [Channel] will never suspend the sender and will never drop elements.
26 | *
27 | * @see [Channel.UNLIMITED]
28 | */
29 | UNLIMITED,
30 |
31 | /**
32 | * The [Channel] is bounded [Channel.CONFLATED].
33 | *
34 | * The [Channel] associated with a [ChannelEvent.Key] will has a conflated capacity buffer.
35 | * The size of buffer is 1 and will keep only the most recently sent element (drop the oldest element).
36 | * This means that the [Channel] will never suspend the sender, but will drop the oldest element when buffer is full.
37 | *
38 | * @see [Channel.CONFLATED]
39 | * @see [BufferOverflow.DROP_OLDEST]
40 | */
41 | CONFLATED,
42 | }
43 |
--------------------------------------------------------------------------------
/channel-event-bus/src/commonMain/kotlin/com/hoc081098/channeleventbus/ChannelEventBusException.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus
2 |
3 | /**
4 | * Represents an exception thrown by [ChannelEventBus].
5 | */
6 | public sealed class ChannelEventBusException(message: String?, cause: Throwable?) : RuntimeException(message, cause) {
7 | public abstract val key: ChannelEventKey<*>
8 |
9 | /**
10 | * Represents an exception thrown when trying to collect a flow that is already collected by another collector.
11 | */
12 | public class FlowAlreadyCollected(
13 | override val key: ChannelEventKey<*>,
14 | ) : ChannelEventBusException("Flow of bus by key=$key is already collected", null)
15 |
16 | /**
17 | * Represents an exception thrown when trying to close a bus.
18 | */
19 | public sealed class CloseException(message: String?, cause: Throwable?) : ChannelEventBusException(message, cause) {
20 | /**
21 | * Represents an exception thrown when trying to close a bus that does not exist.
22 | */
23 | public class BusDoesNotExist(
24 | override val key: ChannelEventKey<*>,
25 | ) : CloseException("Bus by key=$key does not exist", null)
26 |
27 | /**
28 | * Represents an exception thrown when trying to close a bus that is collecting.
29 | */
30 | public class BusIsCollecting(
31 | override val key: ChannelEventKey<*>,
32 | ) : CloseException("Flow of bus by key=$key is collecting, must cancel the collection before closing", null)
33 |
34 | /**
35 | * Represents an exception thrown when trying to close a bus
36 | * that is not empty (all events are not consumed completely).
37 | */
38 | public class BusIsNotEmpty(
39 | override val key: ChannelEventKey<*>,
40 | ) : CloseException("Bus by key=$key is not empty, try to consume all elements before closing", null)
41 | }
42 |
43 | /**
44 | *
45 | */
46 | public sealed class SendException(message: String?, cause: Throwable?) : ChannelEventBusException(message, cause) {
47 | /**
48 | * Represents an exception thrown when trying to send an event to a bus that does not exist.
49 | */
50 | public class BusDoesNotExist(
51 | override val key: ChannelEventKey<*>,
52 | ) : CloseException("Bus by key=$key does not exist", null)
53 |
54 | /**
55 | * Represents an exception thrown when failed to send an event to a bus.
56 | *
57 | * @param event the event that failed to send.
58 | */
59 | public class FailedToSendEvent(
60 | public val event: ChannelEvent<*>,
61 | cause: Throwable?,
62 | ) : SendException("Failed to send event: $event", cause) {
63 | override val key: ChannelEventKey<*> get() = event.key
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/channel-event-bus/src/commonMain/kotlin/com/hoc081098/channeleventbus/ChannelEventBusLogger.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus
2 |
3 | /**
4 | * Logger for [ChannelEventBus].
5 | * It is used to log events of [ChannelEventBus].
6 | */
7 | public interface ChannelEventBusLogger {
8 | /**
9 | * Called when a bus associated with [key] is created.
10 | * @see [ChannelEventBus.send]
11 | */
12 | public fun onCreated(key: ChannelEventKey<*>, bus: ChannelEventBus)
13 |
14 | /**
15 | * Called when an event is sent to a bus.
16 | * @see [ChannelEventBus.send]
17 | */
18 | public fun onSent(event: ChannelEvent<*>, bus: ChannelEventBus)
19 |
20 | /**
21 | * Called when a bus associated with [key] is collecting.
22 | * @see [ChannelEventBus.receiveAsFlow]
23 | */
24 | public fun onStartCollection(key: ChannelEventKey<*>, bus: ChannelEventBus)
25 |
26 | /**
27 | * Called when a bus associated with [key] is stopped collecting.
28 | * @see [ChannelEventBus.receiveAsFlow]
29 | */
30 | public fun onStopCollection(key: ChannelEventKey<*>, bus: ChannelEventBus)
31 |
32 | /**
33 | * Called when a bus associated with [key] is closed.
34 | * @see [ChannelEventBus.closeKey]
35 | */
36 | public fun onClosed(key: ChannelEventKey<*>, bus: ChannelEventBus)
37 |
38 | /**
39 | * Called when all buses are closed.
40 | * @see [ChannelEventBus.close]
41 | */
42 | public fun onClosedAll(keys: Set>, bus: ChannelEventBus)
43 |
44 | public companion object {
45 | public fun noop(): ChannelEventBusLogger = NoopChannelEventBusLogger
46 |
47 | public fun stdout(): ChannelEventBusLogger = StdoutChannelEventBusLogger
48 | }
49 | }
50 |
51 | /**
52 | * The [ChannelEventBusLogger] that simply prints events to the console via [println].
53 | */
54 | private object StdoutChannelEventBusLogger : ChannelEventBusLogger {
55 | override fun onCreated(key: ChannelEventKey<*>, bus: ChannelEventBus): Unit =
56 | println("[$bus] onCreated: key=$key")
57 |
58 | override fun onSent(event: ChannelEvent<*>, bus: ChannelEventBus): Unit =
59 | println("[$bus] onSent: event=$event")
60 |
61 | override fun onStartCollection(key: ChannelEventKey<*>, bus: ChannelEventBus): Unit =
62 | println("[$bus] onStartCollection: key=$key")
63 |
64 | override fun onStopCollection(key: ChannelEventKey<*>, bus: ChannelEventBus): Unit =
65 | println("[$bus] onStopCollection: key=$key")
66 |
67 | override fun onClosed(key: ChannelEventKey<*>, bus: ChannelEventBus): Unit =
68 | println("[$bus] onClosed: key=$key")
69 |
70 | override fun onClosedAll(keys: Set>, bus: ChannelEventBus): Unit =
71 | println("[$bus] onClosedAll: keys=$keys")
72 | }
73 |
74 | /**
75 | * The [ChannelEventBusLogger] that do nothing.
76 | */
77 | private object NoopChannelEventBusLogger : ChannelEventBusLogger {
78 | override fun onCreated(key: ChannelEventKey<*>, bus: ChannelEventBus) = Unit
79 | override fun onSent(event: ChannelEvent<*>, bus: ChannelEventBus) = Unit
80 | override fun onStartCollection(key: ChannelEventKey<*>, bus: ChannelEventBus) = Unit
81 | override fun onStopCollection(key: ChannelEventKey<*>, bus: ChannelEventBus) = Unit
82 | override fun onClosed(key: ChannelEventKey<*>, bus: ChannelEventBus) = Unit
83 | override fun onClosedAll(keys: Set>, bus: ChannelEventBus) = Unit
84 | }
85 |
--------------------------------------------------------------------------------
/channel-event-bus/src/commonMain/kotlin/com/hoc081098/channeleventbus/OptionWhenSendingToBusDoesNotExist.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus
2 |
3 | /**
4 | * Option when sending an event to a bus that does not exist.
5 | * - Create a new bus if the bus associated with [ChannelEvent.key] does not exist.
6 | * - Throw [ChannelEventBusException.SendException.BusDoesNotExist] if the bus associated with [ChannelEvent.key]
7 | * does not exist.
8 | * - Do nothing if the bus associated with [ChannelEvent.key] does not exist.
9 | */
10 | public enum class OptionWhenSendingToBusDoesNotExist {
11 | /**
12 | * Create a new bus if the bus associated with [ChannelEvent.key] does not exist.
13 | * This is the default option.
14 | */
15 | CREATE_NEW_BUS,
16 |
17 | /**
18 | * Throw [ChannelEventBusException.SendException.BusDoesNotExist] if the bus associated with [ChannelEvent.key]
19 | * does not exist.
20 | */
21 | THROW_EXCEPTION,
22 |
23 | /**
24 | * Do nothing if the bus associated with [ChannelEvent.key] does not exist.
25 | * Basically, the event will be ignored (not sent) if the bus does not exist.
26 | */
27 | DO_NOTHING,
28 | }
29 |
--------------------------------------------------------------------------------
/channel-event-bus/src/commonMain/kotlin/com/hoc081098/channeleventbus/ValidationBeforeClosing.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus
2 |
3 | import kotlin.jvm.JvmField
4 |
5 | /**
6 | * Options for validating a bus before closing.
7 | */
8 | public enum class ValidationBeforeClosing {
9 | /**
10 | * Require the flow of the bus must not be collecting by any collector before closing.
11 | * If this requirement is not met, [ChannelEventBusException.CloseException.BusIsCollecting] will be thrown.
12 | */
13 | REQUIRE_FLOW_IS_NOT_COLLECTING,
14 |
15 | /**
16 | * Require the channel of the bus must be empty before closing.
17 | * If this requirement is not met, [ChannelEventBusException.CloseException.BusIsNotEmpty] will be thrown.
18 | */
19 | REQUIRE_BUS_IS_EMPTY,
20 |
21 | /**
22 | * Require the bus must exist before closing.
23 | * If this requirement is not met, [ChannelEventBusException.CloseException.BusDoesNotExist] will be thrown.
24 | */
25 | REQUIRE_BUS_IS_EXISTING,
26 | ;
27 |
28 | public companion object {
29 | @JvmField
30 | public val ALL: Set = entries.toSet()
31 |
32 | public inline val NONE: Set get() = emptySet()
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/channel-event-bus/src/commonTest/kotlin/com/hoc081098/channeleventbus/ChannelEventBusTest.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus
2 |
3 | import com.hoc081098.flowext.interval
4 | import kotlin.test.Test
5 | import kotlin.test.assertContentEquals
6 | import kotlin.test.assertEquals
7 | import kotlin.test.assertFailsWith
8 | import kotlin.time.Duration
9 | import kotlin.time.Duration.Companion.milliseconds
10 | import kotlinx.coroutines.cancelAndJoin
11 | import kotlinx.coroutines.delay
12 | import kotlinx.coroutines.flow.collect
13 | import kotlinx.coroutines.flow.first
14 | import kotlinx.coroutines.flow.flatMapLatest
15 | import kotlinx.coroutines.flow.take
16 | import kotlinx.coroutines.flow.toList
17 | import kotlinx.coroutines.launch
18 | import kotlinx.coroutines.test.StandardTestDispatcher
19 | import kotlinx.coroutines.test.runTest
20 |
21 | class ChannelEventBusTest {
22 | @Test
23 | fun sendAndReceiveMultiple() = runTest {
24 | val bus = ChannelEventBus(ChannelEventBusLogger.noop())
25 |
26 | val sentTestEventInts = mutableListOf()
27 | val sentTestEventStrings = mutableListOf()
28 | val sentTestEventLongs = mutableListOf()
29 |
30 | repeat(100 * 3) { i ->
31 | launch {
32 | when (i % 3) {
33 | 0 -> bus.send(TestEventInt(i).also { sentTestEventInts += it })
34 | 1 -> bus.send(TestEventString(i.toString()).also { sentTestEventStrings += it })
35 | 2 -> bus.send(TestEventLong(i.toLong()).also { sentTestEventLongs += it })
36 | else -> error("Unreachable")
37 | }
38 | }
39 | }
40 |
41 | launch {
42 | val testEventInts = bus
43 | .receiveAsFlow(TestEventInt)
44 | .take(100)
45 | .toList()
46 |
47 | assertContentEquals(
48 | expected = sentTestEventInts,
49 | actual = testEventInts,
50 | )
51 | }
52 |
53 | launch {
54 | val testEventStrings = bus
55 | .receiveAsFlow(TestEventString)
56 | .take(100)
57 | .toList()
58 |
59 | assertContentEquals(
60 | expected = sentTestEventStrings,
61 | actual = testEventStrings,
62 | )
63 | }
64 |
65 | launch {
66 | val testEventStrings = bus
67 | .receiveAsFlow(TestEventLong)
68 | .take(100)
69 | .toList()
70 |
71 | assertContentEquals(
72 | expected = sentTestEventLongs,
73 | actual = testEventStrings,
74 | )
75 | }
76 | }
77 |
78 | @Test
79 | fun onlyOneCollectorAtATime() = runTest {
80 | val bus = ChannelEventBus(ChannelEventBusLogger.noop())
81 |
82 | repeat(10) {
83 | launch { bus.send(TestEventInt(it)) }
84 | }
85 |
86 | val job = launch {
87 | bus.receiveAsFlow(TestEventInt).collect()
88 | }
89 | launch {
90 | val e = assertFailsWith {
91 | bus.receiveAsFlow(TestEventInt).collect()
92 | }
93 | assertEquals(expected = TestEventIntKey, actual = e.key)
94 | job.cancel()
95 | }
96 | }
97 |
98 | @Test
99 | fun cancel_ThenCollectMultipleTimes_DoesNotWorks() = runTest {
100 | val bus = ChannelEventBus(ChannelEventBusLogger.noop())
101 |
102 | repeat(10) {
103 | launch { bus.send(TestEventInt(it)) }
104 | }
105 |
106 | val job = launch {
107 | bus.receiveAsFlow(TestEventInt).collect()
108 | }
109 |
110 | launch {
111 | delay(100)
112 | job.cancel()
113 |
114 | val e = assertFailsWith {
115 | bus.receiveAsFlow(TestEventInt).collect()
116 | }
117 | assertEquals(expected = TestEventIntKey, actual = e.key)
118 | }
119 | }
120 |
121 | @Test
122 | fun cancelAndJoin_ThenCollectMultipleTimes_Works() = runTest {
123 | val bus = ChannelEventBus(ChannelEventBusLogger.noop())
124 |
125 | repeat(10) {
126 | launch { bus.send(TestEventInt(it)) }
127 | }
128 |
129 | val job = launch {
130 | assertEquals(
131 | expected = TestEventInt(0),
132 | actual = bus.receiveAsFlow(TestEventInt).first(),
133 | )
134 | }
135 |
136 | launch {
137 | delay(100)
138 | job.cancelAndJoin()
139 | assertEquals(
140 | expected = TestEventInt(1),
141 | actual = bus.receiveAsFlow(TestEventInt).first(),
142 | )
143 | }
144 | }
145 |
146 | @Test
147 | fun take_ThenCollectMultipleTimes_Works() = runTest {
148 | val bus = ChannelEventBus(ChannelEventBusLogger.noop())
149 |
150 | repeat(10) {
151 | launch { bus.send(TestEventInt(it)) }
152 | }
153 |
154 | launch {
155 | repeat(10) {
156 | bus.receiveAsFlow(TestEventInt).take(1)
157 | }
158 | }
159 | }
160 |
161 | @Test
162 | fun take_ThenCollectMultipleTimes_WithStandardTestDispatcher_Works() = runTest(StandardTestDispatcher()) {
163 | val bus = ChannelEventBus(ChannelEventBusLogger.noop())
164 |
165 | repeat(10) {
166 | launch { bus.send(TestEventInt(it)) }
167 | }
168 |
169 | launch {
170 | repeat(10) {
171 | bus.receiveAsFlow(TestEventInt).take(1)
172 | }
173 | }
174 | }
175 |
176 | @Test
177 | fun flatMapLatest_Works() = runTest {
178 | val bus = ChannelEventBus(ChannelEventBusLogger.noop())
179 | val flow = interval(initialDelay = Duration.ZERO, period = 100.milliseconds)
180 | .take(10)
181 | .flatMapLatest { bus.receiveAsFlow(TestEventInt) }
182 | .take(100)
183 |
184 | launch {
185 | repeat(100) {
186 | delay(33)
187 | bus.send(TestEventInt(it))
188 | }
189 | }
190 | launch {
191 | assertContentEquals(
192 | expected = (0..<100).map { TestEventInt(it) },
193 | actual = flow.toList(),
194 | )
195 | }
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/channel-event-bus/src/commonTest/kotlin/com/hoc081098/channeleventbus/ChannelEventTest.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus
2 |
3 | import kotlin.test.Test
4 | import kotlin.test.assertEquals
5 | import kotlin.test.assertNotEquals
6 |
7 | class ChannelEventTest {
8 | @Test
9 | fun testKeyEquals() {
10 | assertEquals(TestEventIntKey, TestEventInt)
11 | assertEquals(TestEventInt, TestEventInt)
12 | assertEquals(channelEventKeyOf(), TestEventInt)
13 |
14 | assertEquals(TestEventStringKey, TestEventString)
15 | assertEquals(TestEventString, TestEventString)
16 | assertEquals(channelEventKeyOf(), TestEventString)
17 |
18 | assertEquals(TestEventLongKey, TestEventLong)
19 | assertEquals(TestEventLong, TestEventLong)
20 | assertEquals(channelEventKeyOf(), TestEventLong)
21 |
22 | assertNotEquals(TestEventIntKey, TestEventStringKey)
23 | assertNotEquals(TestEventIntKey, TestEventLongKey)
24 | assertNotEquals(channelEventKeyOf(), channelEventKeyOf())
25 | }
26 |
27 | @Test
28 | fun testKeyHashCode() {
29 | assertEquals(TestEventIntKey.hashCode(), TestEventInt.hashCode())
30 | assertEquals(channelEventKeyOf().hashCode(), TestEventInt.hashCode())
31 |
32 | assertEquals(TestEventStringKey.hashCode(), TestEventString.hashCode())
33 | assertEquals(channelEventKeyOf().hashCode(), TestEventString.hashCode())
34 |
35 | assertEquals(TestEventLongKey.hashCode(), TestEventLong.hashCode())
36 | assertEquals(channelEventKeyOf().hashCode(), TestEventLong.hashCode())
37 | }
38 |
39 | @Test
40 | fun testKeyToString() {
41 | assertEquals("ChannelEvent.Key(TestEventInt, UNLIMITED)", TestEventIntKey.toString())
42 | assertEquals("ChannelEvent.Key(TestEventInt, UNLIMITED)", channelEventKeyOf().toString())
43 |
44 | assertEquals("ChannelEvent.Key(TestEventString, UNLIMITED)", TestEventStringKey.toString())
45 | assertEquals("ChannelEvent.Key(TestEventString, UNLIMITED)", channelEventKeyOf().toString())
46 |
47 | assertEquals("ChannelEvent.Key(TestEventLong, UNLIMITED)", TestEventLongKey.toString())
48 | assertEquals("ChannelEvent.Key(TestEventLong, UNLIMITED)", channelEventKeyOf().toString())
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/channel-event-bus/src/commonTest/kotlin/com/hoc081098/channeleventbus/events.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus
2 |
3 | import kotlin.jvm.JvmField
4 |
5 | @JvmField
6 | val TestEventIntKey = ChannelEventKey(TestEventInt::class)
7 |
8 | @JvmField
9 | val TestEventStringKey = ChannelEventKey(TestEventString::class)
10 |
11 | @JvmField
12 | val TestEventLongKey = ChannelEventKey(TestEventLong::class)
13 |
14 | data class TestEventInt(val payload: Int) : ChannelEvent {
15 | override val key get() = Key
16 |
17 | companion object Key : ChannelEventKey(TestEventInt::class)
18 | }
19 |
20 | data class TestEventString(val payload: String) : ChannelEvent {
21 | override val key get() = Key
22 |
23 | companion object Key : ChannelEventKey(TestEventString::class)
24 | }
25 |
26 | data class TestEventLong(val payload: Long) : ChannelEvent {
27 | override val key get() = Key
28 |
29 | companion object Key : ChannelEventKey(TestEventLong::class)
30 | }
31 |
--------------------------------------------------------------------------------
/channel-event-bus/src/jvmTest/kotlin/com/hoc081098/channeleventbus/ChannelEventBusJvmTest.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus
2 |
3 | import com.hoc081098.flowext.interval
4 | import kotlin.test.Ignore
5 | import kotlin.test.Test
6 | import kotlin.test.assertContentEquals
7 | import kotlin.time.Duration
8 | import kotlin.time.Duration.Companion.milliseconds
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.ExperimentalCoroutinesApi
11 | import kotlinx.coroutines.delay
12 | import kotlinx.coroutines.flow.flatMapLatest
13 | import kotlinx.coroutines.flow.flowOn
14 | import kotlinx.coroutines.flow.take
15 | import kotlinx.coroutines.flow.toList
16 | import kotlinx.coroutines.launch
17 | import kotlinx.coroutines.runBlocking
18 |
19 | @OptIn(ExperimentalCoroutinesApi::class)
20 | class ChannelEventBusJvmTest {
21 | @Test
22 | @Ignore // TODO: recheck this
23 | fun flatMapLatest_Works(): Unit = runBlocking(Dispatchers.IO) {
24 | val bus = ChannelEventBus(ChannelEventBusLogger.noop())
25 | val flow = interval(initialDelay = Duration.ZERO, period = 11.milliseconds)
26 | .flowOn(Dispatchers.IO)
27 | .take(10)
28 | .flatMapLatest { bus.receiveAsFlow(TestEventInt) }
29 | .take(50)
30 |
31 | launch {
32 | repeat(50) {
33 | delay(3)
34 | launch { bus.send(TestEventInt(it)) }
35 | }
36 | }
37 |
38 | launch {
39 | assertContentEquals(
40 | expected = (0..<50).map { TestEventInt(it) },
41 | actual = flow.toList().sortedBy { it.payload },
42 | )
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/docs/.gitkeep:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/images/logo.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus/e79999178eab7126474c7e41132e53c166d72922/docs/images/logo.jpeg
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -Dfile.encoding=UTF-8
2 |
3 | kotlin.code.style=official
4 | kotlin.js.generate.executable.default=false
5 |
6 | # android
7 | android.useAndroidX=true
8 | android.defaults.buildfeatures.buildconfig=false
9 |
10 | # gradle
11 | org.gradle.configureondemand=true
12 | org.gradle.caching=true
13 | org.gradle.parallel=true
14 |
15 | # kotlin mpp
16 | kotlin.mpp.stability.nowarn=true
17 | kotlin.js.compiler=ir
18 | kotlin.native.ignoreDisabledTargets=true
19 |
20 | # kotlin incremental
21 | kotlin.incremental.multiplatform=true
22 | kotlin.incremental.useClasspathSnapshot=true
23 | kotlin.incremental=true
24 |
25 | # POM
26 | GROUP=io.github.hoc081098
27 | # HEY! If you change the major version here be sure to update publish-release.yaml doc target folder!
28 | VERSION_NAME=0.1.1-SNAPSHOT
29 | POM_INCEPTION_YEAR=2023
30 |
31 | POM_URL=https://github.com/Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus
32 | POM_SCM_URL=https://github.com/Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus
33 | POM_SCM_CONNECTION=scm:git:git://github.com/Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus
34 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus.git
35 |
36 | POM_LICENCE_NAME=MIT License
37 | POM_LICENCE_URL=https://opensource.org/licenses/mit-license.php
38 | POM_LICENCE_DIST=repo
39 |
40 | POM_DEVELOPER_ID=hoc081098
41 | POM_DEVELOPER_NAME=Petrus Nguyen Thai Hoc
42 | POM_DEVELOPER_URL=https://github.com/hoc081098
43 |
44 | #MPP
45 | kotlin.mpp.enableCInteropCommonization=true
46 | kotlin.mpp.androidSourceSetLayoutVersion=2
47 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus/e79999178eab7126474c7e41132e53c166d72922/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.11.1-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 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus/e79999178eab7126474c7e41132e53c166d72922/logo.png
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Kotlin Channel Event Bus
2 | site_url: https://hoc081098.github.io/kotlin-channel-event-bus/docs/0.x
3 | repo_name: kotlin-channel-event-bus
4 | repo_url: https://github.com/Kotlin-Multiplatform-Foundation/kotlin-channel-event-bus
5 | site_description: "Multi-keys, multi-producers, single-consumer event bus backed by Kotlinx Coroutines Channels"
6 | site_author: Petrus Nguyễn Thái Học
7 | remote_branch: gh-pages
8 | edit_uri: edit/master/docs/
9 |
10 | copyright: 'Copyright © 2023 Petrus Nguyễn Thái Học.'
11 |
12 | theme:
13 | name: 'material'
14 | favicon: images/logo.jpeg
15 | logo: images/logo.jpeg
16 | palette:
17 | primary: 'deep purple'
18 | accent: 'white'
19 | features:
20 | - toc.integrate
21 |
22 | markdown_extensions:
23 | - tables
24 | - attr_list
25 | - md_in_html
26 | - smarty
27 | - codehilite:
28 | guess_lang: false
29 | - footnotes
30 | - meta
31 | - toc:
32 | permalink: true
33 | - pymdownx.betterem:
34 | smart_enable: all
35 | - pymdownx.caret
36 | - pymdownx.inlinehilite
37 | - pymdownx.magiclink
38 | - pymdownx.smartsymbols
39 | - pymdownx.superfences
40 | - pymdownx.tilde
41 | - tables
42 |
43 | nav:
44 | - 'Overview': index.md
45 | - 'API': API
46 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended"
5 | ],
6 | "commitBodyTable": true,
7 | "semanticCommits": "enabled",
8 | "labels": ["dependencies"],
9 | "assignees": [ "hoc081098" ],
10 | "reviewers": [ "hoc081098" ],
11 | "automerge": true,
12 | "platformAutomerge": true,
13 | "assignAutomerge": true,
14 | "rebaseWhen": "conflicted",
15 | "packageRules": [
16 | {
17 | "matchPackagePatterns": [
18 | "*"
19 | ],
20 | "matchUpdateTypes": [
21 | "major",
22 | "minor",
23 | "patch"
24 | ],
25 | "groupName": "all dependencies",
26 | "groupSlug": "all-deps"
27 | }
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @Suppress("DSL_SCOPE_VIOLATION")
2 | plugins {
3 | alias(libs.plugins.android.app)
4 | alias(libs.plugins.kotlin.android)
5 | alias(libs.plugins.kotlin.compose)
6 | }
7 |
8 | android {
9 | namespace = "com.hoc081098.channeleventbus.sample.android"
10 | compileSdk = libs.versions.sample.android.compile.get().toInt()
11 | defaultConfig {
12 | applicationId = "com.hoc081098.channeleventbus.sample.android"
13 | minSdk = libs.versions.android.min.get().toInt()
14 | targetSdk = libs.versions.sample.android.target.get().toInt()
15 | versionCode = 1
16 | versionName = "1.0"
17 | }
18 | buildFeatures {
19 | buildConfig = true
20 | }
21 | packaging {
22 | resources {
23 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
24 | }
25 | }
26 | buildTypes {
27 | getByName("release") {
28 | isMinifyEnabled = false
29 | }
30 | }
31 | compileOptions {
32 | sourceCompatibility = JavaVersion.toVersion(libs.versions.java.target.get())
33 | targetCompatibility = JavaVersion.toVersion(libs.versions.java.target.get())
34 | }
35 | kotlinOptions {
36 | jvmTarget = JavaVersion.toVersion(libs.versions.java.target.get()).toString()
37 | }
38 | }
39 |
40 | kotlin {
41 | jvmToolchain {
42 | languageVersion = JavaLanguageVersion.of(libs.versions.java.toolchain.get())
43 | vendor = JvmVendorSpec.AZUL
44 | }
45 | }
46 |
47 | dependencies {
48 | implementation(project(":channel-event-bus"))
49 |
50 | implementation(platform(libs.androidx.compose.bom))
51 | implementation(libs.androidx.lifecycle.runtime.compose)
52 |
53 | implementation(libs.androidx.compose.ui.ui)
54 | debugImplementation(libs.androidx.compose.ui.tooling)
55 | implementation(libs.androidx.compose.ui.tooling.preview)
56 | implementation(libs.androidx.compose.foundation)
57 | implementation(libs.androidx.compose.material3)
58 | implementation(libs.androidx.compose.material)
59 | implementation(libs.androidx.compose.runtime)
60 | implementation(libs.androidx.activity.compose)
61 | implementation(libs.androidx.navigation.compose)
62 |
63 | implementation(libs.koin.androidx.compose)
64 | implementation(libs.coil.compose)
65 | implementation(libs.flowExt)
66 |
67 | implementation(libs.kotlinx.collections.immutable)
68 | implementation(libs.kmp.viewmodel.savedstate)
69 | implementation(libs.timber)
70 | }
71 |
72 | composeCompiler {
73 | featureFlags.addAll(
74 | org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag.OptimizeNonSkippingGroups,
75 | )
76 |
77 | val composeCompilerDir = layout.buildDirectory.dir("compose_compiler")
78 | if (project.findProperty("composeCompilerReports") == "true") {
79 | reportsDestination = composeCompilerDir
80 | }
81 | if (project.findProperty("composeCompilerMetrics") == "true") {
82 | metricsDestination = composeCompilerDir
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/MyApp.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android
2 |
3 | import android.app.Application
4 | import com.hoc081098.channeleventbus.ChannelEventBus
5 | import com.hoc081098.channeleventbus.ChannelEventBusLogger
6 | import com.hoc081098.channeleventbus.sample.android.common.SingleEventChannel
7 | import com.hoc081098.channeleventbus.sample.android.ui.home.HomeModule
8 | import com.hoc081098.channeleventbus.sample.android.ui.register.RegisterModule
9 | import org.koin.android.ext.koin.androidContext
10 | import org.koin.android.ext.koin.androidLogger
11 | import org.koin.core.context.startKoin
12 | import org.koin.core.logger.Level
13 | import org.koin.dsl.module
14 | import timber.log.Timber
15 |
16 | val ChannelEventBusModule = module {
17 | single {
18 | ChannelEventBus(
19 | if (BuildConfig.DEBUG) {
20 | ChannelEventBusLogger.stdout()
21 | } else {
22 | ChannelEventBusLogger.noop()
23 | },
24 | )
25 | }
26 | }
27 |
28 | class MyApp : Application() {
29 | override fun onCreate() {
30 | super.onCreate()
31 |
32 | if (BuildConfig.DEBUG) {
33 | Timber.plant(Timber.DebugTree())
34 | }
35 |
36 | startKoin {
37 | androidContext(this@MyApp)
38 | androidLogger(
39 | if (BuildConfig.DEBUG) {
40 | Level.DEBUG
41 | } else {
42 | Level.ERROR
43 | },
44 | )
45 |
46 | modules(
47 | ChannelEventBusModule,
48 | RegisterModule,
49 | HomeModule,
50 | module {
51 | factory { SingleEventChannel() }
52 | },
53 | )
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/Route.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Immutable
5 | import androidx.compose.runtime.Stable
6 | import androidx.compose.runtime.State
7 | import androidx.compose.runtime.derivedStateOf
8 | import androidx.compose.runtime.remember
9 | import androidx.navigation.NavBackStackEntry
10 | import kotlinx.collections.immutable.ImmutableList
11 | import kotlinx.collections.immutable.persistentListOf
12 |
13 | @Composable
14 | internal fun rememberCurrentRouteAsState(currentBackStackEntryAsState: State): State =
15 | remember(currentBackStackEntryAsState) {
16 | derivedStateOf {
17 | currentBackStackEntryAsState.value
18 | ?.destination
19 | ?.route
20 | ?.let(Route::ofOrNull)
21 | }
22 | }
23 |
24 | @Immutable
25 | internal sealed class Route {
26 | @Stable
27 | abstract val routePattern: String
28 |
29 | @Stable
30 | fun matches(route: String): Boolean = route == routePattern
31 |
32 | data object RegisterStepOne : Route() {
33 | override val routePattern = "register_step_one"
34 | val route = routePattern
35 | }
36 |
37 | data object RegisterStepTwo : Route() {
38 | override val routePattern = "register_step_two"
39 | val route = routePattern
40 | }
41 |
42 | data object RegisterStepThree : Route() {
43 | override val routePattern = "register_step_three"
44 | val route = routePattern
45 | }
46 |
47 | data object Home : Route() {
48 | override val routePattern = "home"
49 | val route = routePattern
50 | }
51 |
52 | data object Detail : Route() {
53 | override val routePattern = "detail"
54 | val route = routePattern
55 | }
56 |
57 | companion object {
58 | private val VALUES: ImmutableList by lazy {
59 | persistentListOf(
60 | RegisterStepOne,
61 | RegisterStepTwo,
62 | RegisterStepThree,
63 | Home,
64 | Detail,
65 | )
66 | }
67 |
68 | @Stable
69 | fun ofOrNull(route: String): Route? = VALUES.singleOrNull { it.matches(route) }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/common/CollectWithLifecycleEffect.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.common
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Immutable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.compose.runtime.NonRestartableComposable
7 | import androidx.compose.runtime.RememberObserver
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.runtime.rememberUpdatedState
10 | import androidx.compose.ui.platform.LocalLifecycleOwner
11 | import androidx.lifecycle.Lifecycle
12 | import androidx.lifecycle.LifecycleOwner
13 | import androidx.lifecycle.repeatOnLifecycle
14 | import kotlinx.coroutines.CoroutineDispatcher
15 | import kotlinx.coroutines.CoroutineScope
16 | import kotlinx.coroutines.Dispatchers
17 | import kotlinx.coroutines.Job
18 | import kotlinx.coroutines.cancel
19 | import kotlinx.coroutines.flow.Flow
20 | import kotlinx.coroutines.launch
21 |
22 | @Immutable
23 | enum class CollectWithLifecycleEffectDispatcher {
24 | /**
25 | * Use [Dispatchers.Main][kotlinx.coroutines.MainCoroutineDispatcher].
26 | */
27 | Main,
28 |
29 | /**
30 | * Use [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
31 | */
32 | ImmediateMain,
33 |
34 | /**
35 | * Use [androidx.compose.runtime.Composer.applyCoroutineContext].
36 | * Under the hood, it uses Compose [androidx.compose.ui.platform.AndroidUiDispatcher].
37 | */
38 | Composer,
39 | }
40 |
41 | /**
42 | * Collect the given [Flow] in an effect that runs when [LifecycleOwner.lifecycle] is at least at [minActiveState].
43 | *
44 | * - If [dispatcher] is [CollectWithLifecycleEffectDispatcher.ImmediateMain], the effect will run in
45 | * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
46 | * - If [dispatcher] is [CollectWithLifecycleEffectDispatcher.Main], the effect will run in
47 | * [Dispatchers.Main][kotlinx.coroutines.MainCoroutineDispatcher].
48 | * - If [dispatcher] is [CollectWithLifecycleEffectDispatcher.Composer], the effect will run in
49 | * [androidx.compose.runtime.Composer.applyCoroutineContext].
50 | *
51 | * NOTE: When [dispatcher] or [collector] changes, the effect will **NOT** be restarted.
52 | * The latest [collector] will be used to receive values from the [Flow] ([rememberUpdatedState] is used).
53 | * If you want to restart the effect, you need to change [keys].
54 | *
55 | * @param keys Keys to be used to [remember] the effect.
56 | * @param lifecycleOwner The [LifecycleOwner] to be used to [repeatOnLifecycle].
57 | * @param minActiveState The minimum [Lifecycle.State] to be used to [repeatOnLifecycle].
58 | * @param dispatcher The dispatcher to be used to launch the [Flow].
59 | * @param collector The collector to be used to collect the [Flow].
60 | *
61 | * @see [LaunchedEffect]
62 | * @see [CollectWithLifecycleEffectDispatcher]
63 | */
64 | @Composable
65 | fun Flow.CollectWithLifecycleEffect(
66 | vararg keys: Any?,
67 | lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
68 | minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
69 | dispatcher: CollectWithLifecycleEffectDispatcher = CollectWithLifecycleEffectDispatcher.ImmediateMain,
70 | collector: (T) -> Unit,
71 | ) {
72 | val flow = this
73 | val collectorState = rememberUpdatedState(collector)
74 |
75 | val block: suspend CoroutineScope.() -> Unit = {
76 | lifecycleOwner.repeatOnLifecycle(minActiveState) {
77 | // NOTE: we don't use `flow.collect(collectState.value)` because it can use the old value
78 | flow.collect { collectorState.value(it) }
79 | }
80 | }
81 |
82 | when (dispatcher) {
83 | CollectWithLifecycleEffectDispatcher.ImmediateMain -> {
84 | LaunchedEffectInImmediateMain(flow, lifecycleOwner, minActiveState, *keys, block = block)
85 | }
86 |
87 | CollectWithLifecycleEffectDispatcher.Main -> {
88 | LaunchedEffectInMain(flow, lifecycleOwner, minActiveState, *keys, block = block)
89 | }
90 |
91 | CollectWithLifecycleEffectDispatcher.Composer -> {
92 | LaunchedEffect(flow, lifecycleOwner, minActiveState, *keys, block = block)
93 | }
94 | }
95 | }
96 |
97 | @Composable
98 | @NonRestartableComposable
99 | @Suppress("ArrayReturn")
100 | private fun LaunchedEffectInImmediateMain(
101 | vararg keys: Any?,
102 | block: suspend CoroutineScope.() -> Unit,
103 | ) {
104 | remember(*keys) { LaunchedEffectImpl(block, Dispatchers.Main.immediate) }
105 | }
106 |
107 | @Composable
108 | @NonRestartableComposable
109 | @Suppress("ArrayReturn")
110 | private fun LaunchedEffectInMain(
111 | vararg keys: Any?,
112 | block: suspend CoroutineScope.() -> Unit,
113 | ) {
114 | remember(*keys) { LaunchedEffectImpl(block, Dispatchers.Main) }
115 | }
116 |
117 | private class LaunchedEffectImpl(
118 | private val task: suspend CoroutineScope.() -> Unit,
119 | dispatcher: CoroutineDispatcher,
120 | ) : RememberObserver {
121 | private val scope = CoroutineScope(dispatcher)
122 | private var job: Job? = null
123 |
124 | override fun onRemembered() {
125 | job?.cancel("Old job was still running!")
126 | job = scope.launch(block = task)
127 | }
128 |
129 | override fun onForgotten() {
130 | job?.cancel()
131 | job = null
132 | }
133 |
134 | override fun onAbandoned() {
135 | job?.cancel()
136 | job = null
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/common/OnLifecycleEvent.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.common
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.DisposableEffect
5 | import androidx.compose.runtime.Stable
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.runtime.rememberUpdatedState
10 | import androidx.compose.runtime.setValue
11 | import androidx.compose.ui.platform.LocalLifecycleOwner
12 | import androidx.lifecycle.Lifecycle
13 | import androidx.lifecycle.LifecycleEventObserver
14 | import androidx.lifecycle.LifecycleOwner
15 |
16 | @Suppress("unused")
17 | @Composable
18 | fun OnLifecycleEvent(
19 | vararg keys: Any?,
20 | lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
21 | onEvent: (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit,
22 | ) {
23 | val eventHandler by rememberUpdatedState(onEvent)
24 |
25 | DisposableEffect(*keys, lifecycleOwner) {
26 | val observer = LifecycleEventObserver { owner, event ->
27 | eventHandler(owner, event)
28 | }
29 | lifecycleOwner.lifecycle.addObserver(observer)
30 |
31 | onDispose {
32 | lifecycleOwner.lifecycle.removeObserver(observer)
33 | }
34 | }
35 | }
36 |
37 | typealias LifecycleEventListener = (owner: LifecycleOwner) -> Unit
38 | typealias LifecycleEachEventListener = (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit
39 |
40 | @DslMarker
41 | annotation class LifecycleEventBuilderMarker
42 |
43 | @Stable
44 | @LifecycleEventBuilderMarker
45 | class LifecycleEventBuilder {
46 | private var onCreate: LifecycleEventListener? by mutableStateOf(null)
47 | private var onStart: LifecycleEventListener? by mutableStateOf(null)
48 | private var onResume: LifecycleEventListener? by mutableStateOf(null)
49 | private var onPause: LifecycleEventListener? by mutableStateOf(null)
50 | private var onStop: LifecycleEventListener? by mutableStateOf(null)
51 | private var onDestroy: LifecycleEventListener? by mutableStateOf(null)
52 | private var onEach: LifecycleEachEventListener? by mutableStateOf(null)
53 |
54 | @LifecycleEventBuilderMarker
55 | fun onCreate(block: LifecycleEventListener) {
56 | onCreate = block
57 | }
58 |
59 | @LifecycleEventBuilderMarker
60 | fun onStart(block: LifecycleEventListener) {
61 | onStart = block
62 | }
63 |
64 | @LifecycleEventBuilderMarker
65 | fun onResume(block: LifecycleEventListener) {
66 | onResume = block
67 | }
68 |
69 | @LifecycleEventBuilderMarker
70 | fun onPause(block: LifecycleEventListener) {
71 | onPause = block
72 | }
73 |
74 | @LifecycleEventBuilderMarker
75 | fun onStop(block: LifecycleEventListener) {
76 | onStop = block
77 | }
78 |
79 | @LifecycleEventBuilderMarker
80 | fun onDestroy(block: LifecycleEventListener) {
81 | onDestroy = block
82 | }
83 |
84 | @LifecycleEventBuilderMarker
85 | fun onEach(block: LifecycleEachEventListener) {
86 | onEach = block
87 | }
88 |
89 | internal fun buildLifecycleEventObserver() = LifecycleEventObserver { owner, event ->
90 | when (event) {
91 | Lifecycle.Event.ON_CREATE -> onCreate
92 | Lifecycle.Event.ON_START -> onStart
93 | Lifecycle.Event.ON_RESUME -> onResume
94 | Lifecycle.Event.ON_PAUSE -> onPause
95 | Lifecycle.Event.ON_STOP -> onStop
96 | Lifecycle.Event.ON_DESTROY -> onDestroy
97 | Lifecycle.Event.ON_ANY -> null
98 | }?.invoke(owner)
99 |
100 | onEach?.invoke(owner, event)
101 | }
102 | }
103 |
104 | @Composable
105 | fun OnLifecycleEventWithBuilder(
106 | vararg keys: Any?,
107 | lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
108 | builder: LifecycleEventBuilder.() -> Unit,
109 | ) {
110 | val lifecycleEventBuilder = remember { LifecycleEventBuilder() }
111 | val observer = remember { lifecycleEventBuilder.buildLifecycleEventObserver() }
112 |
113 | // When builder or lifecycleOwner or keys changes, we need to re-execute the effect
114 | DisposableEffect(builder, lifecycleOwner, *keys) {
115 | // This make sure all callbacks are always up to date.
116 | builder(lifecycleEventBuilder)
117 |
118 | lifecycleOwner.lifecycle.addObserver(observer)
119 | onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/common/SingleEventChannel.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.common
2 |
3 | import androidx.annotation.MainThread
4 | import androidx.lifecycle.ViewModel
5 | import java.io.Closeable
6 | import java.lang.System.identityHashCode
7 | import kotlin.LazyThreadSafetyMode.NONE
8 | import kotlinx.coroutines.channels.Channel
9 | import kotlinx.coroutines.channels.onClosed
10 | import kotlinx.coroutines.channels.onFailure
11 | import kotlinx.coroutines.channels.onSuccess
12 | import kotlinx.coroutines.flow.Flow
13 | import kotlinx.coroutines.flow.FlowCollector
14 | import kotlinx.coroutines.flow.emitAll
15 | import kotlinx.coroutines.flow.receiveAsFlow
16 | import timber.log.Timber
17 |
18 | sealed interface SingleEventFlow : Flow {
19 | /**
20 | * Must collect in [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
21 | * Safe to call in the coroutines launched by [androidx.lifecycle.lifecycleScope].
22 | *
23 | * In Compose, we can use [CollectWithLifecycleEffect] with [CollectWithLifecycleEffectDispatcher.ImmediateMain].
24 | */
25 | @MainThread
26 | override suspend fun collect(collector: FlowCollector)
27 | }
28 |
29 | @MainThread
30 | interface HasSingleEventFlow {
31 | /**
32 | * Must collect in [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
33 | * Safe to call in the coroutines launched by [androidx.lifecycle.lifecycleScope].
34 | *
35 | * In Compose, we can use [CollectWithLifecycleEffect] with [CollectWithLifecycleEffectDispatcher.ImmediateMain].
36 | */
37 | val singleEventFlow: SingleEventFlow
38 | }
39 |
40 | @MainThread
41 | sealed interface SingleEventFlowSender {
42 | /**
43 | * Must call in [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
44 | * Safe to call in the coroutines launched by [androidx.lifecycle.viewModelScope].
45 | */
46 | suspend fun sendEvent(event: E)
47 | }
48 |
49 | private class SingleEventFlowImpl(private val channel: Channel) : SingleEventFlow {
50 | override suspend fun collect(collector: FlowCollector) {
51 | debugCheckImmediateMainDispatcher()
52 | return collector.emitAll(channel.receiveAsFlow())
53 | }
54 | }
55 |
56 | @MainThread
57 | class SingleEventChannel :
58 | Closeable,
59 | HasSingleEventFlow,
60 | SingleEventFlowSender {
61 | private val _eventChannel = Channel(Channel.UNLIMITED)
62 |
63 | override val singleEventFlow: SingleEventFlow by lazy(NONE) { SingleEventFlowImpl(_eventChannel) }
64 |
65 | init {
66 | Timber.d("[EventChannel] created: hashCode=${identityHashCode(this)}")
67 | }
68 |
69 | /**
70 | * Must be called in Dispatchers.Main.immediate, otherwise it will throw an exception.
71 | * If you want to send an event from other Dispatcher,
72 | * use `withContext(Dispatchers.Main.immediate) { eventChannel.send(event) }`
73 | */
74 | @MainThread
75 | override suspend fun sendEvent(event: E) {
76 | debugCheckImmediateMainDispatcher()
77 |
78 | _eventChannel
79 | .trySend(event)
80 | .onClosed { return }
81 | .onFailure { Timber.e(it, "[EventChannel] Failed to send event: $event, hashCode=${identityHashCode(this)}") }
82 | .onSuccess { Timber.d("[EventChannel] Sent event: $event, hashCode=${identityHashCode(this)}") }
83 | }
84 |
85 | override fun close() {
86 | _eventChannel.close()
87 | Timber.d("[EventChannel] closed: hashCode=${identityHashCode(this)}")
88 | }
89 | }
90 |
91 | @Suppress("NOTHING_TO_INLINE")
92 | inline fun SingleEventChannel.addToViewModel(viewModel: ViewModel) =
93 | apply { viewModel.addCloseable(this) }
94 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/common/debugCheckImmediateMainDispatcher.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.common
2 |
3 | import com.hoc081098.channeleventbus.sample.android.BuildConfig
4 | import kotlinx.coroutines.CoroutineDispatcher
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.currentCoroutineContext
7 |
8 | @OptIn(ExperimentalStdlibApi::class)
9 | suspend inline fun debugCheckImmediateMainDispatcher() {
10 | if (BuildConfig.DEBUG) {
11 | val dispatcher = currentCoroutineContext()[CoroutineDispatcher]!!
12 |
13 | check(
14 | dispatcher === Dispatchers.Main.immediate ||
15 | !dispatcher.isDispatchNeeded(Dispatchers.Main.immediate),
16 | ) {
17 | "Expected CoroutineDispatcher to be Dispatchers.Main.immediate but was $dispatcher"
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/home/detail/DetailScreen.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.ui.home.detail
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.text.KeyboardActions
12 | import androidx.compose.foundation.text.KeyboardOptions
13 | import androidx.compose.material3.ElevatedButton
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.material3.OutlinedTextField
16 | import androidx.compose.material3.Text
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.getValue
19 | import androidx.compose.runtime.remember
20 | import androidx.compose.runtime.rememberUpdatedState
21 | import androidx.compose.ui.Alignment
22 | import androidx.compose.ui.Modifier
23 | import androidx.compose.ui.text.input.ImeAction
24 | import androidx.compose.ui.text.style.TextAlign
25 | import androidx.compose.ui.unit.dp
26 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
27 | import com.hoc081098.channeleventbus.sample.android.common.CollectWithLifecycleEffect
28 | import kotlinx.coroutines.Dispatchers
29 | import org.koin.androidx.compose.koinViewModel
30 |
31 | @Composable
32 | fun DetailScreen(
33 | navigateBack: () -> Unit,
34 | modifier: Modifier = Modifier,
35 | vm: DetailVM = koinViewModel(),
36 | ) {
37 | val currentNavigateBack by rememberUpdatedState(navigateBack)
38 | vm.singleEventFlow.CollectWithLifecycleEffect { event ->
39 | when (event) {
40 | DetailSingleEvent.Complete -> currentNavigateBack()
41 | }
42 | }
43 |
44 | val text by vm
45 | .textStateFlow
46 | .collectAsStateWithLifecycle(context = Dispatchers.Main.immediate)
47 |
48 | Box(
49 | modifier = modifier.fillMaxSize(),
50 | contentAlignment = Alignment.Center,
51 | ) {
52 | Column(
53 | modifier = Modifier.matchParentSize(),
54 | horizontalAlignment = Alignment.CenterHorizontally,
55 | verticalArrangement = Arrangement.Top,
56 | ) {
57 | Text(
58 | text = "Detail",
59 | textAlign = TextAlign.Center,
60 | style = MaterialTheme.typography.titleLarge,
61 | )
62 |
63 | Spacer(modifier = Modifier.height(16.dp))
64 |
65 | OutlinedTextField(
66 | modifier = Modifier
67 | .fillMaxWidth()
68 | .padding(horizontal = 16.dp),
69 | value = text,
70 | onValueChange = remember { vm::onTextChanged },
71 | singleLine = true,
72 | maxLines = 1,
73 | label = { Text("Enter the text") },
74 | keyboardActions = KeyboardActions.Default,
75 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
76 | )
77 |
78 | Spacer(modifier = Modifier.height(16.dp))
79 |
80 | ElevatedButton(
81 | enabled = text.isNotBlank(),
82 | onClick = remember { vm::sendResultToHome },
83 | ) {
84 | Text(text = "Send to home screen")
85 | }
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/home/detail/DetailVM.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.ui.home.detail
2 |
3 | import androidx.compose.runtime.Immutable
4 | import androidx.lifecycle.SavedStateHandle
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.hoc081098.channeleventbus.ChannelEventBus
8 | import com.hoc081098.channeleventbus.sample.android.common.HasSingleEventFlow
9 | import com.hoc081098.channeleventbus.sample.android.common.SingleEventChannel
10 | import com.hoc081098.channeleventbus.sample.android.ui.home.DetailResultToHomeEvent
11 | import com.hoc081098.channeleventbus.sample.android.utils.NonBlankString.Companion.toNonBlankString
12 | import com.hoc081098.channeleventbus.sample.android.utils.launchNowIn
13 | import com.hoc081098.flowext.flowFromSuspend
14 | import com.hoc081098.kmp.viewmodel.safe.NonNullSavedStateHandleKey
15 | import com.hoc081098.kmp.viewmodel.safe.safe
16 | import com.hoc081098.kmp.viewmodel.safe.string
17 | import kotlinx.coroutines.ExperimentalCoroutinesApi
18 | import kotlinx.coroutines.delay
19 | import kotlinx.coroutines.flow.Flow
20 | import kotlinx.coroutines.flow.MutableSharedFlow
21 | import kotlinx.coroutines.flow.StateFlow
22 | import kotlinx.coroutines.flow.flatMapLatest
23 | import kotlinx.coroutines.launch
24 | import timber.log.Timber
25 |
26 | @Immutable
27 | sealed interface DetailSingleEvent {
28 | data object Complete : DetailSingleEvent
29 | }
30 |
31 | @OptIn(ExperimentalCoroutinesApi::class)
32 | class DetailVM(
33 | private val channelEventBus: ChannelEventBus,
34 | private val singleEventChannel: SingleEventChannel,
35 | private val savedStateHandle: SavedStateHandle,
36 | ) : ViewModel(singleEventChannel),
37 | HasSingleEventFlow by singleEventChannel {
38 | private val sendResultFlow = MutableSharedFlow(extraBufferCapacity = 1)
39 |
40 | internal val textStateFlow: StateFlow = savedStateHandle.safe.getStateFlow(TextKey)
41 |
42 | init {
43 | fun process(): Flow = flowFromSuspend {
44 | delay(@Suppress("MagicNumber") 500) // simulate a long-running task
45 |
46 | textStateFlow.value
47 | .toNonBlankString()
48 | .map(::DetailResultToHomeEvent)
49 | .onSuccess(channelEventBus::send)
50 | .onSuccess { singleEventChannel.sendEvent(DetailSingleEvent.Complete) }
51 | .onFailure { Timber.e(it, "Error while sending result to home") }
52 | }
53 |
54 | sendResultFlow
55 | .flatMapLatest { process() }
56 | .launchNowIn(viewModelScope)
57 | }
58 |
59 | internal fun onTextChanged(text: String) {
60 | savedStateHandle.safe[TextKey] = text
61 | }
62 |
63 | internal fun sendResultToHome() {
64 | viewModelScope.launch { sendResultFlow.emit(Unit) }
65 | }
66 |
67 | private companion object {
68 | private val TextKey = NonNullSavedStateHandleKey.string("text", "")
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/home/di.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.ui.home
2 |
3 | import com.hoc081098.channeleventbus.sample.android.ui.home.detail.DetailVM
4 | import com.hoc081098.channeleventbus.sample.android.ui.home.home.HomeVM
5 | import org.koin.androidx.viewmodel.dsl.viewModelOf
6 | import org.koin.dsl.module
7 |
8 | val HomeModule = module {
9 | viewModelOf(::DetailVM)
10 | viewModelOf(::HomeVM)
11 | }
12 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/home/events.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.ui.home
2 |
3 | import com.hoc081098.channeleventbus.ChannelEvent
4 | import com.hoc081098.channeleventbus.ChannelEventKey
5 | import com.hoc081098.channeleventbus.sample.android.utils.NonBlankString
6 |
7 | internal data class DetailResultToHomeEvent(val value: NonBlankString) : ChannelEvent {
8 | override val key get() = Key
9 |
10 | companion object Key : ChannelEventKey(DetailResultToHomeEvent::class)
11 | }
12 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/home/home/HomeScreen.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.ui.home.home
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.PaddingValues
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.lazy.LazyColumn
11 | import androidx.compose.foundation.lazy.items
12 | import androidx.compose.material3.ElevatedButton
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.getValue
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.text.style.TextAlign
20 | import androidx.compose.ui.text.style.TextOverflow
21 | import androidx.compose.ui.unit.dp
22 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
23 | import org.koin.androidx.compose.koinViewModel
24 |
25 | @Composable
26 | fun HomeScreen(
27 | navigateToDetail: () -> Unit,
28 | modifier: Modifier = Modifier,
29 | vm: HomeVM = koinViewModel(),
30 | ) {
31 | val detailResults by vm.detailResultsStateFlow.collectAsStateWithLifecycle()
32 |
33 | Box(
34 | modifier = modifier.fillMaxSize(),
35 | contentAlignment = Alignment.Center,
36 | ) {
37 | Column(
38 | modifier = Modifier.matchParentSize(),
39 | horizontalAlignment = Alignment.CenterHorizontally,
40 | verticalArrangement = Arrangement.Center,
41 | ) {
42 | Text(
43 | text = "Home",
44 | textAlign = TextAlign.Center,
45 | style = MaterialTheme.typography.titleLarge,
46 | )
47 |
48 | Spacer(modifier = Modifier.height(8.dp))
49 |
50 | ElevatedButton(
51 | onClick = navigateToDetail,
52 | ) {
53 | Text(
54 | text = "Click to go to detail",
55 | )
56 | }
57 |
58 | LazyColumn(
59 | modifier = Modifier.weight(1f),
60 | contentPadding = PaddingValues(all = 16.dp),
61 | verticalArrangement = Arrangement.spacedBy(8.dp),
62 | ) {
63 | items(
64 | items = detailResults,
65 | key = { it.asString() },
66 | contentType = { "DetailResult" },
67 | ) { result ->
68 | Text(
69 | modifier = Modifier.fillParentMaxWidth(),
70 | text = result.asString(),
71 | style = MaterialTheme.typography.bodyMedium,
72 | textAlign = TextAlign.Start,
73 | maxLines = 2,
74 | overflow = TextOverflow.Ellipsis,
75 | )
76 | }
77 | }
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/home/home/HomeVM.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.ui.home.home
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.viewModelScope
5 | import com.hoc081098.channeleventbus.ChannelEventBus
6 | import com.hoc081098.channeleventbus.sample.android.ui.home.DetailResultToHomeEvent
7 | import com.hoc081098.channeleventbus.sample.android.utils.NonBlankString
8 | import kotlinx.collections.immutable.PersistentList
9 | import kotlinx.collections.immutable.persistentListOf
10 | import kotlinx.collections.immutable.plus
11 | import kotlinx.coroutines.CancellationException
12 | import kotlinx.coroutines.flow.SharingStarted
13 | import kotlinx.coroutines.flow.StateFlow
14 | import kotlinx.coroutines.flow.map
15 | import kotlinx.coroutines.flow.onCompletion
16 | import kotlinx.coroutines.flow.scan
17 | import kotlinx.coroutines.flow.stateIn
18 |
19 | class HomeVM(
20 | channelEventBus: ChannelEventBus,
21 | ) : ViewModel() {
22 | internal val detailResultsStateFlow: StateFlow> = channelEventBus
23 | .receiveAsFlow(DetailResultToHomeEvent)
24 | .onCompletion {
25 | check(it is CancellationException) { "Expected CancellationException but was $it" }
26 | // Close the bus when this ViewModel is cleared.
27 | channelEventBus.closeKey(DetailResultToHomeEvent)
28 | }
29 | .map { it.value }
30 | .scan(persistentListOf()) { acc, value ->
31 | if (value in acc) {
32 | acc
33 | } else {
34 | acc + value
35 | }
36 | }
37 | .stateIn(
38 | scope = viewModelScope,
39 | // Using SharingStarted.Lazily is enough, because the event bus is backed by a channel.
40 | started = SharingStarted.Lazily,
41 | initialValue = persistentListOf(),
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/register/RegisterSharedVM.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.ui.register
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.hoc081098.channeleventbus.ChannelEventBus
7 | import com.hoc081098.channeleventbus.ValidationBeforeClosing.Companion.NONE
8 | import com.hoc081098.channeleventbus.sample.android.BuildConfig
9 | import com.hoc081098.kmp.viewmodel.safe.safe
10 | import kotlinx.coroutines.flow.SharingStarted
11 | import kotlinx.coroutines.flow.StateFlow
12 | import kotlinx.coroutines.flow.combine
13 | import kotlinx.coroutines.flow.distinctUntilChanged
14 | import kotlinx.coroutines.flow.launchIn
15 | import kotlinx.coroutines.flow.onEach
16 | import kotlinx.coroutines.flow.stateIn
17 | import timber.log.Timber
18 |
19 | class RegisterSharedVM(
20 | private val channelEventBus: ChannelEventBus,
21 | private val savedStateHandle: SavedStateHandle,
22 | ) : ViewModel() {
23 | internal val uiStateFlow: StateFlow = combine(
24 | savedStateHandle.safe.getStateFlow(FirstNameKey),
25 | savedStateHandle.safe.getStateFlow(LastNameKey),
26 | savedStateHandle.safe.getStateFlow(GenderKey),
27 | ) { firstName, lastName, gender ->
28 | RegisterUiState.from(
29 | firstName = firstName,
30 | lastName = lastName,
31 | gender = gender,
32 | )
33 | }.stateIn(
34 | scope = viewModelScope,
35 | started = SharingStarted.WhileSubscribed(stopTimeoutMillis = @Suppress("MagicNumber") 5_000),
36 | initialValue = RegisterUiState.Unfilled,
37 | )
38 |
39 | init {
40 | Timber.d("$this::init")
41 | addCloseable {
42 | arrayOf(SubmitFirstNameEvent, SubmitLastNameEvent, SubmitGenderEvent).forEach {
43 | channelEventBus.closeKey(key = it, validations = NONE)
44 | }
45 | Timber.d("$this::close")
46 | }
47 |
48 | debugPrint()
49 | observeSubmitFirstNameEvent()
50 | observeSubmitLastNameEvent()
51 | observeSubmitGenderEvent()
52 | }
53 |
54 | private fun observeSubmitLastNameEvent() {
55 | channelEventBus
56 | .receiveAsFlow(SubmitLastNameEvent)
57 | .onEach { savedStateHandle.safe[LastNameKey] = it.value }
58 | .launchIn(viewModelScope)
59 | }
60 |
61 | private fun observeSubmitFirstNameEvent() {
62 | channelEventBus
63 | .receiveAsFlow(SubmitFirstNameEvent)
64 | .onEach { savedStateHandle.safe[FirstNameKey] = it.value }
65 | .launchIn(viewModelScope)
66 | }
67 |
68 | private fun observeSubmitGenderEvent() {
69 | channelEventBus
70 | .receiveAsFlow(SubmitGenderEvent)
71 | .onEach { savedStateHandle.safe[GenderKey] = it.value }
72 | .launchIn(viewModelScope)
73 | }
74 |
75 | private fun debugPrint() {
76 | if (!BuildConfig.DEBUG) {
77 | return
78 | }
79 |
80 | val line = "-".repeat(@Suppress("MagicNumber") 80)
81 |
82 | combine(
83 | savedStateHandle.safe.getStateFlow(FirstNameKey),
84 | savedStateHandle.safe.getStateFlow(LastNameKey),
85 | savedStateHandle.safe.getStateFlow(GenderKey),
86 | ) { array: Array<*> -> array.joinToString(separator = "\n") { " $it" } }
87 | .distinctUntilChanged()
88 | .onEach {
89 | Timber.d(
90 | """
91 | |$this::debugPrint $line
92 | |$it
93 | |$line
94 | """.trimMargin(),
95 | )
96 | }
97 | .launchIn(viewModelScope)
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/register/contract.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.ui.register
2 |
3 | import androidx.compose.runtime.Immutable
4 | import androidx.compose.runtime.Stable
5 | import com.hoc081098.kmp.viewmodel.safe.NullableSavedStateHandleKey
6 | import com.hoc081098.kmp.viewmodel.safe.serializable
7 | import com.hoc081098.kmp.viewmodel.safe.string
8 | import com.hoc081098.kmp.viewmodel.serializable.JvmSerializable
9 | import kotlin.LazyThreadSafetyMode.PUBLICATION
10 |
11 | @Immutable
12 | internal enum class Gender : JvmSerializable {
13 | MALE,
14 | FEMALE,
15 | }
16 |
17 | @Stable
18 | internal val Gender.displayName: String
19 | get() = when (this) {
20 | Gender.MALE -> "Male"
21 | Gender.FEMALE -> "Female"
22 | }
23 |
24 | @Immutable
25 | internal sealed interface RegisterUiState {
26 | data object Unfilled : RegisterUiState
27 |
28 | data class Filled(
29 | val firstName: String,
30 | val lastName: String,
31 | val gender: Gender,
32 | ) : RegisterUiState
33 |
34 | companion object
35 | }
36 |
37 | internal fun RegisterUiState.Companion.from(
38 | firstName: String?,
39 | lastName: String?,
40 | gender: Gender?,
41 | ): RegisterUiState = if (
42 | firstName != null &&
43 | lastName != null &&
44 | gender != null
45 | ) {
46 | RegisterUiState.Filled(
47 | firstName = firstName,
48 | lastName = lastName,
49 | gender = gender,
50 | )
51 | } else {
52 | RegisterUiState.Unfilled
53 | }
54 |
55 | internal val FirstNameKey by lazy(PUBLICATION) {
56 | NullableSavedStateHandleKey.string(
57 | key = "first_name",
58 | defaultValue = null,
59 | )
60 | }
61 | internal val LastNameKey by lazy(PUBLICATION) {
62 | NullableSavedStateHandleKey.string(
63 | key = "last_name",
64 | defaultValue = null,
65 | )
66 | }
67 | internal val GenderKey by lazy(PUBLICATION) {
68 | NullableSavedStateHandleKey.serializable(
69 | key = "gender",
70 | defaultValue = null,
71 | )
72 | }
73 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/register/di.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.ui.register
2 |
3 | import com.hoc081098.channeleventbus.sample.android.ui.register.stepone.RegisterStepOneVM
4 | import com.hoc081098.channeleventbus.sample.android.ui.register.stepthree.RegisterStepThreeVM
5 | import com.hoc081098.channeleventbus.sample.android.ui.register.steptwo.RegisterStepTwoVM
6 | import org.koin.androidx.viewmodel.dsl.viewModelOf
7 | import org.koin.dsl.module
8 |
9 | val RegisterModule = module {
10 | viewModelOf(::RegisterSharedVM)
11 | viewModelOf(::RegisterStepOneVM)
12 | viewModelOf(::RegisterStepTwoVM)
13 | viewModelOf(::RegisterStepThreeVM)
14 | }
15 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/register/events.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.ui.register
2 |
3 | import com.hoc081098.channeleventbus.ChannelEvent
4 | import com.hoc081098.channeleventbus.ChannelEventBusCapacity.CONFLATED
5 | import com.hoc081098.channeleventbus.ChannelEventKey
6 |
7 | internal data class SubmitFirstNameEvent(val value: String?) : ChannelEvent {
8 | override val key get() = Key
9 |
10 | companion object Key : ChannelEventKey(SubmitFirstNameEvent::class, CONFLATED)
11 | }
12 |
13 | internal data class SubmitLastNameEvent(val value: String?) : ChannelEvent {
14 | override val key get() = Key
15 |
16 | companion object Key : ChannelEventKey(SubmitLastNameEvent::class, CONFLATED)
17 | }
18 |
19 | internal data class SubmitGenderEvent(val value: Gender?) : ChannelEvent {
20 | override val key get() = Key
21 |
22 | companion object Key : ChannelEventKey(SubmitGenderEvent::class, CONFLATED)
23 | }
24 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/register/stepone/RegisterStepOneScreen.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.ui.register.stepone
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.height
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.text.KeyboardActions
10 | import androidx.compose.foundation.text.KeyboardOptions
11 | import androidx.compose.material3.ElevatedButton
12 | import androidx.compose.material3.OutlinedTextField
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.getValue
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.text.input.ImeAction
20 | import androidx.compose.ui.unit.dp
21 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
22 | import com.hoc081098.channeleventbus.sample.android.ui.register.RegisterSharedVM
23 | import kotlinx.coroutines.Dispatchers
24 | import org.koin.androidx.compose.koinViewModel
25 |
26 | @Composable
27 | fun RegisterStepOneScreen(
28 | registerSharedVM: RegisterSharedVM,
29 | navigateToRegisterStepTwo: () -> Unit,
30 | modifier: Modifier = Modifier,
31 | vm: RegisterStepOneVM = koinViewModel(),
32 | ) {
33 | registerSharedVM.toString()
34 |
35 | val firstName by vm
36 | .firstNameStateFlow
37 | .collectAsStateWithLifecycle(context = Dispatchers.Main.immediate)
38 |
39 | val lastName by vm
40 | .lastNameStateFlow
41 | .collectAsStateWithLifecycle(context = Dispatchers.Main.immediate)
42 |
43 | Column(
44 | modifier = modifier,
45 | verticalArrangement = Arrangement.Top,
46 | horizontalAlignment = Alignment.CenterHorizontally,
47 | ) {
48 | Spacer(modifier = Modifier.height(16.dp))
49 |
50 | OutlinedTextField(
51 | modifier = Modifier
52 | .fillMaxWidth()
53 | .padding(horizontal = 16.dp),
54 | value = firstName.orEmpty(),
55 | onValueChange = remember { vm::onFirstNameChanged },
56 | singleLine = true,
57 | maxLines = 1,
58 | label = { Text("First name") },
59 | keyboardActions = KeyboardActions.Default,
60 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
61 | )
62 |
63 | Spacer(modifier = Modifier.height(16.dp))
64 |
65 | OutlinedTextField(
66 | modifier = Modifier
67 | .fillMaxWidth()
68 | .padding(horizontal = 16.dp),
69 | value = lastName.orEmpty(),
70 | onValueChange = remember { vm::onLastNameChanged },
71 | singleLine = true,
72 | maxLines = 1,
73 | label = { Text("Last name") },
74 | keyboardActions = KeyboardActions.Default,
75 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
76 | )
77 |
78 | Spacer(modifier = Modifier.weight(1f))
79 |
80 | ElevatedButton(onClick = navigateToRegisterStepTwo) {
81 | Text(text = "Next")
82 | }
83 |
84 | Spacer(modifier = Modifier.height(16.dp))
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/register/stepone/RegisterStepOneVM.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.ui.register.stepone
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.hoc081098.channeleventbus.ChannelEventBus
7 | import com.hoc081098.channeleventbus.OptionWhenSendingToBusDoesNotExist
8 | import com.hoc081098.channeleventbus.sample.android.ui.register.FirstNameKey
9 | import com.hoc081098.channeleventbus.sample.android.ui.register.LastNameKey
10 | import com.hoc081098.channeleventbus.sample.android.ui.register.SubmitFirstNameEvent
11 | import com.hoc081098.channeleventbus.sample.android.ui.register.SubmitLastNameEvent
12 | import com.hoc081098.kmp.viewmodel.safe.safe
13 | import kotlinx.coroutines.CancellationException
14 | import kotlinx.coroutines.flow.StateFlow
15 | import kotlinx.coroutines.flow.launchIn
16 | import kotlinx.coroutines.flow.onCompletion
17 | import kotlinx.coroutines.flow.onEach
18 |
19 | class RegisterStepOneVM(
20 | private val savedStateHandle: SavedStateHandle,
21 | private val channelEventBus: ChannelEventBus,
22 | ) : ViewModel() {
23 | internal val firstNameStateFlow: StateFlow = savedStateHandle.safe.getStateFlow(FirstNameKey)
24 | internal val lastNameStateFlow: StateFlow = savedStateHandle.safe.getStateFlow(LastNameKey)
25 |
26 | init {
27 | sendSubmitFirstNameEventAfterChanged()
28 | sendSubmitLastNameEventAfterChanged()
29 | }
30 |
31 | /**
32 | * Send [SubmitFirstNameEvent] to [ChannelEventBus] when [firstNameStateFlow] emits a new value
33 | */
34 | private fun sendSubmitFirstNameEventAfterChanged() {
35 | firstNameStateFlow
36 | .onEach { channelEventBus.send(SubmitFirstNameEvent(it)) }
37 | .onCompletion {
38 | check(it is CancellationException) { "Expected CancellationException but was $it" }
39 |
40 | // Send null to bus when this ViewModel is cleared, to clear the value in RegisterSharedVM.
41 | // Do nothing if the bus does not exist (ie. there is no active collector for this bus or the bus is closed).
42 | channelEventBus.send(
43 | event = SubmitFirstNameEvent(null),
44 | option = OptionWhenSendingToBusDoesNotExist.DO_NOTHING,
45 | )
46 | }
47 | .launchIn(viewModelScope)
48 | }
49 |
50 | /**
51 | * Send [SubmitLastNameEvent] to [ChannelEventBus] when [lastNameStateFlow] emits a new value
52 | */
53 | private fun sendSubmitLastNameEventAfterChanged() {
54 | lastNameStateFlow
55 | .onEach { channelEventBus.send(SubmitLastNameEvent(it)) }
56 | .onCompletion {
57 | check(it is CancellationException) { "Expected CancellationException but was $it" }
58 |
59 | // Send null to bus when this ViewModel is cleared, to clear the value in RegisterSharedVM.
60 | // Do nothing if the bus does not exist (ie. there is no active collector for this bus or the bus is closed).
61 | channelEventBus.send(
62 | event = SubmitLastNameEvent(null),
63 | option = OptionWhenSendingToBusDoesNotExist.DO_NOTHING,
64 | )
65 | }
66 | .launchIn(viewModelScope)
67 | }
68 |
69 | internal fun onFirstNameChanged(value: String) {
70 | savedStateHandle.safe[FirstNameKey] = value
71 | }
72 |
73 | internal fun onLastNameChanged(value: String) {
74 | savedStateHandle.safe[LastNameKey] = value
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/register/stepthree/RegisterStepThreeScreen.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.ui.register.stepthree
2 |
3 | import android.widget.Toast
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.width
12 | import androidx.compose.material3.CircularProgressIndicator
13 | import androidx.compose.material3.ElevatedButton
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.material3.Text
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.runtime.getValue
18 | import androidx.compose.runtime.rememberUpdatedState
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.platform.LocalContext
22 | import androidx.compose.ui.text.font.FontWeight
23 | import androidx.compose.ui.text.style.TextAlign
24 | import androidx.compose.ui.unit.dp
25 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
26 | import com.hoc081098.channeleventbus.sample.android.common.CollectWithLifecycleEffect
27 | import com.hoc081098.channeleventbus.sample.android.ui.register.RegisterSharedVM
28 | import com.hoc081098.channeleventbus.sample.android.ui.register.RegisterUiState
29 | import com.hoc081098.channeleventbus.sample.android.ui.register.displayName
30 | import org.koin.androidx.compose.koinViewModel
31 |
32 | @Suppress("LongMethod")
33 | @Composable
34 | fun RegisterStepThreeScreen(
35 | registerSharedVM: RegisterSharedVM,
36 | navigateToHome: () -> Unit,
37 | modifier: Modifier = Modifier,
38 | vm: RegisterStepThreeVM = koinViewModel(),
39 | ) {
40 | val registerUiState by registerSharedVM
41 | .uiStateFlow
42 | .collectAsStateWithLifecycle()
43 |
44 | val uiState by vm.uiStateFlow.collectAsStateWithLifecycle()
45 |
46 | val currentNavigateToHome by rememberUpdatedState(navigateToHome)
47 | val context by rememberUpdatedState(LocalContext.current)
48 |
49 | vm.singleEventFlow.CollectWithLifecycleEffect { event ->
50 | when (event) {
51 | is RegisterStepThreeSingleEvent.Failure -> {
52 | Toast
53 | .makeText(
54 | context,
55 | "Register failed: ${event.throwable.message}",
56 | Toast.LENGTH_SHORT,
57 | )
58 | .show()
59 | }
60 |
61 | RegisterStepThreeSingleEvent.Success ->
62 | currentNavigateToHome()
63 | }
64 | }
65 |
66 | Column(
67 | modifier = modifier,
68 | verticalArrangement = Arrangement.Top,
69 | horizontalAlignment = Alignment.CenterHorizontally,
70 | ) {
71 | Spacer(modifier = Modifier.height(16.dp))
72 |
73 | when (val s = registerUiState) {
74 | is RegisterUiState.Filled -> {
75 | RegisterInfo(
76 | modifier = Modifier
77 | .fillMaxWidth()
78 | .padding(horizontal = 16.dp),
79 | registerUiState = s,
80 | )
81 | }
82 |
83 | RegisterUiState.Unfilled -> {
84 | Text(
85 | modifier = Modifier
86 | .fillMaxWidth()
87 | .padding(horizontal = 16.dp),
88 | text = "Please fill all information",
89 | style = MaterialTheme.typography.titleLarge,
90 | fontWeight = FontWeight.Bold,
91 | textAlign = TextAlign.Center,
92 | )
93 | }
94 | }
95 |
96 | Spacer(modifier = Modifier.weight(1f))
97 |
98 | when (uiState) {
99 | RegisterStepThreeUiState.Idle,
100 | is RegisterStepThreeUiState.Failure,
101 | RegisterStepThreeUiState.Success,
102 | -> Unit
103 |
104 | RegisterStepThreeUiState.Registering ->
105 | CircularProgressIndicator(
106 | modifier = Modifier.padding(16.dp),
107 | )
108 | }
109 |
110 | ElevatedButton(
111 | enabled = registerUiState is RegisterUiState.Filled,
112 | onClick = {
113 | vm.register(
114 | registerSharedVM
115 | .uiStateFlow
116 | .value,
117 | )
118 | },
119 | ) {
120 | Text(text = "Register")
121 | }
122 |
123 | Spacer(modifier = Modifier.height(16.dp))
124 | }
125 | }
126 |
127 | @Composable
128 | private fun RegisterInfo(
129 | registerUiState: RegisterUiState.Filled,
130 | modifier: Modifier = Modifier,
131 | ) {
132 | Column(
133 | modifier = modifier,
134 | ) {
135 | SimpleTile(
136 | title = "First name: ",
137 | content = registerUiState.firstName,
138 | )
139 |
140 | Spacer(modifier = Modifier.height(8.dp))
141 |
142 | SimpleTile(
143 | title = "Last name: ",
144 | content = registerUiState.lastName,
145 | )
146 |
147 | Spacer(modifier = Modifier.height(8.dp))
148 |
149 | SimpleTile(
150 | title = "Gender: ",
151 | content = registerUiState.gender.displayName,
152 | )
153 |
154 | Spacer(modifier = Modifier.height(8.dp))
155 | }
156 | }
157 |
158 | @Composable
159 | private fun SimpleTile(
160 | title: String,
161 | content: String,
162 | modifier: Modifier = Modifier,
163 | ) {
164 | Row(
165 | modifier = modifier
166 | .fillMaxWidth(),
167 | ) {
168 | Text(
169 | text = title,
170 | style = MaterialTheme.typography.titleLarge,
171 | fontWeight = FontWeight.Bold,
172 | textAlign = TextAlign.Start,
173 | )
174 |
175 | Spacer(modifier = Modifier.width(8.dp))
176 |
177 | Text(
178 | modifier = Modifier
179 | .weight(1f),
180 | text = content,
181 | style = MaterialTheme.typography.bodyMedium,
182 | fontWeight = FontWeight.Normal,
183 | textAlign = TextAlign.End,
184 | )
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/register/stepthree/RegisterStepThreeVM.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.ui.register.stepthree
2 |
3 | import androidx.compose.runtime.Immutable
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.hoc081098.channeleventbus.sample.android.common.HasSingleEventFlow
7 | import com.hoc081098.channeleventbus.sample.android.common.SingleEventChannel
8 | import com.hoc081098.channeleventbus.sample.android.ui.register.RegisterUiState
9 | import com.hoc081098.flowext.flatMapFirst
10 | import com.hoc081098.flowext.flowFromSuspend
11 | import com.hoc081098.flowext.startWith
12 | import java.io.IOException
13 | import kotlin.random.Random
14 | import kotlinx.coroutines.delay
15 | import kotlinx.coroutines.flow.MutableSharedFlow
16 | import kotlinx.coroutines.flow.SharingStarted
17 | import kotlinx.coroutines.flow.StateFlow
18 | import kotlinx.coroutines.flow.catch
19 | import kotlinx.coroutines.flow.map
20 | import kotlinx.coroutines.flow.onEach
21 | import kotlinx.coroutines.flow.stateIn
22 | import kotlinx.coroutines.launch
23 | import timber.log.Timber
24 |
25 | @Immutable
26 | sealed interface RegisterStepThreeSingleEvent {
27 | data object Success : RegisterStepThreeSingleEvent
28 | data class Failure(val throwable: Throwable) : RegisterStepThreeSingleEvent
29 | }
30 |
31 | @Immutable
32 | internal sealed interface RegisterStepThreeUiState {
33 | data object Idle : RegisterStepThreeUiState
34 | data object Registering : RegisterStepThreeUiState
35 | data object Success : RegisterStepThreeUiState
36 | data class Failure(val throwable: Throwable) : RegisterStepThreeUiState
37 | }
38 |
39 | class RegisterStepThreeVM(
40 | private val singleEventChannel: SingleEventChannel,
41 | ) : ViewModel(singleEventChannel),
42 | HasSingleEventFlow by singleEventChannel {
43 | private val _registerFlow = MutableSharedFlow(extraBufferCapacity = 1)
44 |
45 | internal val uiStateFlow: StateFlow = _registerFlow
46 | .flatMapFirst { state ->
47 | flowFromSuspend { doRegister(state) }
48 | .map { RegisterStepThreeUiState.Success }
49 | .startWith(RegisterStepThreeUiState.Registering)
50 | .catch { emit(RegisterStepThreeUiState.Idle) }
51 | }
52 | .onEach(::sendEvent)
53 | .stateIn(
54 | scope = viewModelScope,
55 | started = SharingStarted.Eagerly,
56 | initialValue = RegisterStepThreeUiState.Idle,
57 | )
58 |
59 | private suspend fun sendEvent(state: RegisterStepThreeUiState) = when (state) {
60 | RegisterStepThreeUiState.Idle, RegisterStepThreeUiState.Registering ->
61 | Unit
62 |
63 | is RegisterStepThreeUiState.Failure ->
64 | singleEventChannel.sendEvent(RegisterStepThreeSingleEvent.Failure(state.throwable))
65 |
66 | RegisterStepThreeUiState.Success ->
67 | singleEventChannel.sendEvent(RegisterStepThreeSingleEvent.Success)
68 | }
69 |
70 | internal fun register(state: RegisterUiState) {
71 | when (state) {
72 | RegisterUiState.Unfilled -> {
73 | Timber.w("Unfilled state")
74 | return
75 | }
76 |
77 | is RegisterUiState.Filled -> {
78 | viewModelScope.launch { _registerFlow.emit(state) }
79 | }
80 | }
81 | }
82 | }
83 |
84 | private suspend fun doRegister(state: RegisterUiState.Filled) {
85 | Timber.d("doRegister $state")
86 |
87 | // simulate network request
88 | delay(@Suppress("MagicNumber") 2_000)
89 | if (Random.nextBoolean()) {
90 | throw IOException("Network error")
91 | .also { Timber.e(it, "Register failed") }
92 | } else {
93 | Timber.d("Register success")
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/register/steptwo/RegisterStepTwoScreen.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.ui.register.steptwo
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.width
11 | import androidx.compose.foundation.selection.selectable
12 | import androidx.compose.material3.ElevatedButton
13 | import androidx.compose.material3.RadioButton
14 | import androidx.compose.material3.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.getValue
17 | import androidx.compose.runtime.remember
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.unit.dp
21 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
22 | import com.hoc081098.channeleventbus.sample.android.ui.register.Gender
23 | import com.hoc081098.channeleventbus.sample.android.ui.register.RegisterSharedVM
24 | import com.hoc081098.channeleventbus.sample.android.ui.register.displayName
25 | import kotlinx.coroutines.Dispatchers
26 | import org.koin.androidx.compose.koinViewModel
27 |
28 | @Composable
29 | fun RegisterStepTwoScreen(
30 | registerSharedVM: RegisterSharedVM,
31 | navigateToRegisterStepThree: () -> Unit,
32 | modifier: Modifier = Modifier,
33 | vm: RegisterStepTwoVM = koinViewModel(),
34 | ) {
35 | registerSharedVM.toString()
36 |
37 | val selectedGender by vm
38 | .genderStateFlow
39 | .collectAsStateWithLifecycle(context = Dispatchers.Main.immediate)
40 |
41 | Column(
42 | modifier = modifier,
43 | verticalArrangement = Arrangement.Top,
44 | horizontalAlignment = Alignment.CenterHorizontally,
45 | ) {
46 | Spacer(modifier = Modifier.height(16.dp))
47 |
48 | GenderSection(
49 | modifier = Modifier.fillMaxWidth(),
50 | selectedGender = selectedGender,
51 | onGenderChange = remember { vm::onGenderChanged },
52 | )
53 |
54 | Spacer(modifier = Modifier.weight(1f))
55 |
56 | ElevatedButton(onClick = navigateToRegisterStepThree) {
57 | Text(text = "Next")
58 | }
59 |
60 | Spacer(modifier = Modifier.height(16.dp))
61 | }
62 | }
63 |
64 | @Composable
65 | private fun GenderSection(
66 | selectedGender: Gender?,
67 | onGenderChange: (Gender) -> Unit,
68 | modifier: Modifier = Modifier,
69 | ) {
70 | Column(modifier = modifier) {
71 | Gender.entries.forEach { item ->
72 | val onClick = { onGenderChange(item) }
73 | val selected = selectedGender == item
74 |
75 | Row(
76 | modifier = Modifier
77 | .fillMaxWidth()
78 | .selectable(
79 | selected = selected,
80 | onClick = onClick,
81 | )
82 | .padding(horizontal = 16.dp),
83 | verticalAlignment = Alignment.CenterVertically,
84 | horizontalArrangement = Arrangement.Start,
85 | ) {
86 | RadioButton(
87 | selected = selected,
88 | onClick = onClick,
89 | )
90 |
91 | Spacer(modifier = Modifier.width(8.dp))
92 |
93 | Text(
94 | modifier = Modifier.weight(1f),
95 | text = item.displayName,
96 | )
97 | }
98 |
99 | Spacer(modifier = Modifier.height(16.dp))
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/ui/register/steptwo/RegisterStepTwoVM.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.ui.register.steptwo
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.hoc081098.channeleventbus.ChannelEventBus
7 | import com.hoc081098.channeleventbus.OptionWhenSendingToBusDoesNotExist
8 | import com.hoc081098.channeleventbus.sample.android.ui.register.Gender
9 | import com.hoc081098.channeleventbus.sample.android.ui.register.GenderKey
10 | import com.hoc081098.channeleventbus.sample.android.ui.register.SubmitGenderEvent
11 | import com.hoc081098.kmp.viewmodel.safe.safe
12 | import kotlinx.coroutines.CancellationException
13 | import kotlinx.coroutines.flow.StateFlow
14 | import kotlinx.coroutines.flow.launchIn
15 | import kotlinx.coroutines.flow.onCompletion
16 | import kotlinx.coroutines.flow.onEach
17 |
18 | class RegisterStepTwoVM(
19 | private val savedStateHandle: SavedStateHandle,
20 | private val channelEventBus: ChannelEventBus,
21 | ) : ViewModel() {
22 | internal val genderStateFlow: StateFlow = savedStateHandle.safe.getStateFlow(GenderKey)
23 |
24 | init {
25 | sendSubmitGenderEventAfterChanged()
26 | }
27 |
28 | /**
29 | * Send [SubmitGenderEvent] to [ChannelEventBus] when [genderStateFlow] emits a new value
30 | */
31 | private fun sendSubmitGenderEventAfterChanged() {
32 | genderStateFlow
33 | .onEach { channelEventBus.send(SubmitGenderEvent(it)) }
34 | .onCompletion {
35 | check(it is CancellationException) { "Expected CancellationException but was $it" }
36 |
37 | // Send null to bus when this ViewModel is cleared, to clear the value in RegisterSharedVM.
38 | // Do nothing if the bus does not exist (ie. there is no active collector for this bus or the bus is closed).
39 | channelEventBus.send(
40 | event = SubmitGenderEvent(null),
41 | option = OptionWhenSendingToBusDoesNotExist.DO_NOTHING,
42 | )
43 | }
44 | .launchIn(viewModelScope)
45 | }
46 |
47 | internal fun onGenderChanged(value: Gender) {
48 | savedStateHandle.safe[GenderKey] = value
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/utils/NonBlankString.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.utils
2 |
3 | /**
4 | * Representation of strings that have at least one character, excluding
5 | * whitespaces.
6 | */
7 | @JvmInline
8 | value class NonBlankString private constructor(private val value: String) : Comparable {
9 | init {
10 | require(value.isNotBlank()) { NotBlankStringException.message }
11 | }
12 |
13 | /**
14 | * Compares this string alphabetically with the [other] one for order.
15 | * Returns zero if this string equals the [other] one, a negative number if
16 | * it's less than the [other] one, or a positive number if it's greater than
17 | * the [other] one.
18 | */
19 | override infix fun compareTo(other: NonBlankString): Int = value.compareTo(other.value)
20 |
21 | /** Returns this string as a [String]. */
22 | override fun toString(): String = value
23 |
24 | fun asString(): String = value
25 |
26 | /** Returns the length of this string. */
27 | val length: Int get() = value.length
28 |
29 | companion object {
30 | /**
31 | * Returns this string as an encapsulated [NonBlankString],
32 | * or returns an encapsulated [IllegalArgumentException] if this string is
33 | * [blank][String.isBlank].
34 | */
35 | fun String.toNonBlankString(): Result =
36 | runCatching { NonBlankString(this) }
37 | }
38 | }
39 |
40 | internal object NotBlankStringException : IllegalArgumentException() {
41 | override val message: String = "Given string shouldn't be blank."
42 | }
43 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/java/com/hoc081098/channeleventbus/sample/android/utils/launchNow.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.android.utils
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.CoroutineStart
6 | import kotlinx.coroutines.Job
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.collect
9 | import kotlinx.coroutines.launch
10 |
11 | /**
12 | * Launch a coroutine immediately to collect the flow.
13 | * It is a shortcut for `scope.launch(CoroutineStart.UNDISPATCHED) { flow.collect() }`.
14 | *
15 | * This differs from [kotlinx.coroutines.flow.launchIn] in that the collection is started immediately
16 | * _in the current thread_
17 | * until the first suspension point, without dispatching to the [CoroutineDispatcher] of the scope context.
18 | * However, when the coroutine is resumed from suspension, it is dispatched to the [CoroutineDispatcher] in its context.
19 | *
20 | * This is useful when collecting a [kotlinx.coroutines.flow.SharedFlow] which does not replay or buffer values,
21 | * and you don't want to miss any values due to the dispatching to the [CoroutineDispatcher].
22 | *
23 | * @see kotlinx.coroutines.flow.launchIn
24 | * @see CoroutineStart.UNDISPATCHED
25 | */
26 | fun Flow.launchNowIn(scope: CoroutineScope): Job =
27 | scope.launch(start = CoroutineStart.UNDISPATCHED) {
28 | collect() // tail-call
29 | }
30 |
--------------------------------------------------------------------------------
/sample/standalone-androidApp/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/androidApp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @Suppress("DSL_SCOPE_VIOLATION")
2 | plugins {
3 | alias(libs.plugins.android.app)
4 | alias(libs.plugins.kotlin.android)
5 | alias(libs.plugins.jetbrains.compose)
6 | alias(libs.plugins.kotlin.compose)
7 | }
8 |
9 | android {
10 | namespace = "com.hoc081098.channeleventbus.sample.kmp.compose.android"
11 | compileSdk = libs.versions.sample.android.compile.get().toInt()
12 | defaultConfig {
13 | applicationId = "com.hoc081098.channeleventbus.sample.kmp.compose.android"
14 | minSdk = libs.versions.android.min.get().toInt()
15 | targetSdk = libs.versions.sample.android.target.get().toInt()
16 | versionCode = 1
17 | versionName = "1.0"
18 | }
19 | buildFeatures {
20 | buildConfig = true
21 | }
22 | packaging {
23 | resources {
24 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
25 | }
26 | }
27 | buildTypes {
28 | getByName("release") {
29 | isMinifyEnabled = false
30 | }
31 | }
32 | compileOptions {
33 | sourceCompatibility = JavaVersion.toVersion(libs.versions.java.target.get())
34 | targetCompatibility = JavaVersion.toVersion(libs.versions.java.target.get())
35 | }
36 | kotlinOptions {
37 | jvmTarget = JavaVersion.toVersion(libs.versions.java.target.get()).toString()
38 | }
39 | }
40 |
41 | kotlin {
42 | jvmToolchain {
43 | languageVersion = JavaLanguageVersion.of(libs.versions.java.toolchain.get())
44 | vendor = JvmVendorSpec.AZUL
45 | }
46 | }
47 |
48 | dependencies {
49 | // Compose app
50 | implementation(projects.sample.standaloneComposeMultiplatform.composeApp)
51 |
52 | implementation(libs.androidx.activity.compose)
53 | implementation(libs.koin.androidx.compose)
54 |
55 | implementation(libs.flowExt)
56 |
57 | implementation(libs.timber)
58 | }
59 |
60 | composeCompiler {
61 | featureFlags.addAll(
62 | org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag.OptimizeNonSkippingGroups,
63 | )
64 |
65 | val composeCompilerDir = layout.buildDirectory.dir("compose_compiler")
66 | if (project.findProperty("composeCompilerReports") == "true") {
67 | reportsDestination = composeCompilerDir
68 | }
69 | if (project.findProperty("composeCompilerMetrics") == "true") {
70 | metricsDestination = composeCompilerDir
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/androidApp/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/androidApp/src/main/java/com/hoc081098/channeleventbus/sample/kmp/compose/android/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.android
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import com.hoc081098.channeleventbus.sample.kmp.compose.ChannelEventBusSampleApp
7 | import org.koin.androidx.compose.KoinAndroidContext
8 |
9 | class MainActivity : ComponentActivity() {
10 | override fun onCreate(savedInstanceState: Bundle?) {
11 | super.onCreate(savedInstanceState)
12 |
13 | setContent {
14 | KoinAndroidContext {
15 | ChannelEventBusSampleApp()
16 | }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/androidApp/src/main/java/com/hoc081098/channeleventbus/sample/kmp/compose/android/MyApp.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.android
2 |
3 | import android.app.Application
4 | import com.hoc081098.channeleventbus.sample.kmp.compose.setupNapier
5 | import com.hoc081098.channeleventbus.sample.kmp.compose.startKoinCommon
6 | import org.koin.android.ext.koin.androidContext
7 | import org.koin.android.ext.koin.androidLogger
8 | import org.koin.core.logger.Level
9 |
10 | class MyApp : Application() {
11 | override fun onCreate() {
12 | super.onCreate()
13 |
14 | setupNapier()
15 | startKoinCommon {
16 | androidContext(this@MyApp)
17 | androidLogger(
18 | if (BuildConfig.DEBUG) {
19 | Level.DEBUG
20 | } else {
21 | Level.ERROR
22 | },
23 | )
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/androidApp/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2 |
3 | plugins {
4 | alias(libs.plugins.kotlin.multiplatform)
5 | alias(libs.plugins.jetbrains.compose)
6 | alias(libs.plugins.android.library)
7 | alias(libs.plugins.kotlin.parcelize)
8 | alias(libs.plugins.kotlin.compose)
9 | }
10 |
11 | kotlin {
12 | jvmToolchain {
13 | languageVersion = JavaLanguageVersion.of(libs.versions.java.toolchain.get())
14 | vendor = JvmVendorSpec.AZUL
15 | }
16 |
17 | androidTarget {
18 | compilations.configureEach {
19 | compileTaskProvider.configure {
20 | compilerOptions {
21 | jvmTarget = JvmTarget.fromTarget(libs.versions.java.target.get())
22 | }
23 | }
24 | }
25 | }
26 |
27 | jvm("desktop") {
28 | compilations.configureEach {
29 | compileTaskProvider.configure {
30 | compilerOptions {
31 | jvmTarget = JvmTarget.fromTarget(libs.versions.java.target.get())
32 | }
33 | }
34 | }
35 | }
36 |
37 | sourceSets {
38 | val desktopMain by getting
39 |
40 | androidMain.dependencies {
41 | api(libs.androidx.compose.ui.tooling.preview)
42 | api(libs.androidx.activity.compose)
43 |
44 | // Koin
45 | api(libs.koin.android)
46 | api(libs.koin.androidx.compose)
47 | }
48 | commonMain.dependencies {
49 | // Channel event bus
50 | implementation(projects.channelEventBus)
51 |
52 | implementation(compose.runtime)
53 | implementation(compose.foundation)
54 | implementation(compose.material3)
55 | implementation(compose.ui)
56 | implementation(compose.components.resources)
57 |
58 | // KMP View Model & Solivagant navigation
59 | api(libs.kmp.viewmodel)
60 | api(libs.kmp.viewmodel.savedstate)
61 | api(libs.kmp.viewmodel.compose)
62 | api(libs.kmp.viewmodel.koin.compose)
63 | api(libs.solivagant.navigation)
64 |
65 | // Koin
66 | api(libs.koin.core)
67 | api(libs.koin.compose)
68 |
69 | // Coroutines & FlowExt
70 | api(libs.coroutines.core)
71 | api(libs.flowExt)
72 |
73 | // Immutable collections
74 | api(libs.kotlinx.collections.immutable)
75 |
76 | // Napier logger
77 | api(libs.napier)
78 | }
79 | desktopMain.dependencies {
80 | implementation(compose.desktop.currentOs)
81 | }
82 | }
83 | }
84 |
85 | android {
86 | namespace = "com.hoc081098.channeleventbus.sample.kmp.compose"
87 | compileSdk = libs.versions.sample.android.compile.get().toInt()
88 |
89 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
90 | sourceSets["main"].res.srcDirs("src/androidMain/res")
91 | sourceSets["main"].resources.srcDirs("src/commonMain/resources")
92 |
93 | defaultConfig {
94 | minSdk = libs.versions.android.min.get().toInt()
95 | }
96 | packaging {
97 | resources {
98 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
99 | }
100 | }
101 | buildTypes {
102 | getByName("release") {
103 | isMinifyEnabled = false
104 | }
105 | }
106 | compileOptions {
107 | sourceCompatibility = JavaVersion.VERSION_11
108 | targetCompatibility = JavaVersion.VERSION_11
109 | }
110 | buildFeatures {
111 | buildConfig = true
112 | }
113 | dependencies {
114 | debugImplementation(libs.androidx.compose.ui.tooling)
115 | }
116 | }
117 |
118 | composeCompiler {
119 | featureFlags.addAll(
120 | org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag.OptimizeNonSkippingGroups,
121 | )
122 |
123 | val composeCompilerDir = layout.buildDirectory.dir("compose_compiler")
124 | if (project.findProperty("composeCompilerReports") == "true") {
125 | reportsDestination = composeCompilerDir
126 | }
127 | if (project.findProperty("composeCompilerMetrics") == "true") {
128 | metricsDestination = composeCompilerDir
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/androidMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/common/debugCheckImmediateMainDispatcher.android.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.common
2 |
3 | import com.hoc081098.channeleventbus.sample.kmp.compose.BuildConfig
4 |
5 | actual fun isBuildDebug(): Boolean = BuildConfig.DEBUG
6 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/androidMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/common/identityHashCode.android.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.common
2 |
3 | internal actual fun Any?.identityHashCode(): Int = System.identityHashCode(this)
4 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ChannelEventBusSampleApp.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose
2 |
3 | import androidx.compose.animation.AnimatedContentTransitionScope
4 | import androidx.compose.animation.fadeIn
5 | import androidx.compose.animation.fadeOut
6 | import androidx.compose.foundation.layout.consumeWindowInsets
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.material.icons.Icons
10 | import androidx.compose.material.icons.automirrored.filled.ArrowBack
11 | import androidx.compose.material3.CenterAlignedTopAppBar
12 | import androidx.compose.material3.ExperimentalMaterial3Api
13 | import androidx.compose.material3.Icon
14 | import androidx.compose.material3.IconButton
15 | import androidx.compose.material3.MaterialTheme
16 | import androidx.compose.material3.Scaffold
17 | import androidx.compose.material3.Surface
18 | import androidx.compose.material3.Text
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.runtime.Stable
21 | import androidx.compose.runtime.getValue
22 | import androidx.compose.runtime.mutableStateOf
23 | import androidx.compose.runtime.remember
24 | import androidx.compose.runtime.saveable.rememberSaveable
25 | import androidx.compose.runtime.setValue
26 | import androidx.compose.ui.Modifier
27 | import com.hoc081098.channeleventbus.sample.kmp.compose.common.MyApplicationTheme
28 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.home.detail.DetailScreenDestination
29 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.home.home.HomeScreenRouteDestination
30 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.stepone.RegisterStepOneScreenDestination
31 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.stepone.RegisterStepOneScreenRoute
32 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.stepthree.RegisterStepThreeScreenDestination
33 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.steptwo.RegisterStepTwoScreenDestination
34 | import com.hoc081098.solivagant.navigation.BaseRoute
35 | import com.hoc081098.solivagant.navigation.NavDestination
36 | import com.hoc081098.solivagant.navigation.NavEventNavigator
37 | import com.hoc081098.solivagant.navigation.NavHost
38 | import com.hoc081098.solivagant.navigation.NavHostDefaults
39 | import com.hoc081098.solivagant.navigation.NavRoot
40 | import kotlinx.collections.immutable.ImmutableSet
41 | import kotlinx.collections.immutable.adapters.ImmutableSetAdapter
42 | import org.koin.compose.KoinContext
43 | import org.koin.compose.koinInject
44 |
45 | @Stable
46 | private val AllDestinations: ImmutableSet by lazy {
47 | ImmutableSetAdapter(
48 | hashSetOf(
49 | RegisterStepOneScreenDestination,
50 | RegisterStepTwoScreenDestination,
51 | RegisterStepThreeScreenDestination,
52 | HomeScreenRouteDestination,
53 | DetailScreenDestination,
54 | ),
55 | )
56 | }
57 |
58 | @OptIn(ExperimentalMaterial3Api::class)
59 | @Suppress("LongMethod")
60 | @Composable
61 | fun ChannelEventBusSampleApp(
62 | modifier: Modifier = Modifier,
63 | navigator: NavEventNavigator = koinInject(),
64 | ) {
65 | var currentRoute by rememberSaveable { mutableStateOf(null) }
66 |
67 | KoinContext {
68 | MyApplicationTheme(darkTheme = false) {
69 | Surface(
70 | modifier = modifier.fillMaxSize(),
71 | color = MaterialTheme.colorScheme.background,
72 | ) {
73 | Scaffold(
74 | modifier = Modifier.fillMaxSize(),
75 | topBar = {
76 | CenterAlignedTopAppBar(
77 | title = { Text(text = currentRoute.toString()) },
78 | navigationIcon = {
79 | if (currentRoute !is NavRoot) {
80 | IconButton(onClick = remember { navigator::navigateBack }) {
81 | Icon(
82 | imageVector = Icons.AutoMirrored.Filled.ArrowBack,
83 | contentDescription = "Back",
84 | )
85 | }
86 | }
87 | },
88 | )
89 | },
90 | ) { innerPadding ->
91 | NavHost(
92 | modifier = Modifier.fillMaxSize()
93 | .padding(innerPadding)
94 | .consumeWindowInsets(innerPadding),
95 | startRoute = RegisterStepOneScreenRoute,
96 | destinations = AllDestinations,
97 | navEventNavigator = navigator,
98 | destinationChangedCallback = { currentRoute = it },
99 | transitionAnimations = NavHostDefaults.transitionAnimations(
100 | enterTransition = {
101 | slideIntoContainer(
102 | towards = AnimatedContentTransitionScope.SlideDirection.Left,
103 | )
104 | },
105 | exitTransition = {
106 | slideOutOfContainer(
107 | towards = AnimatedContentTransitionScope.SlideDirection.Left,
108 | )
109 | },
110 | popEnterTransition = {
111 | slideIntoContainer(
112 | towards = AnimatedContentTransitionScope.SlideDirection.Right,
113 | )
114 | },
115 | popExitTransition = {
116 | slideOutOfContainer(
117 | towards = AnimatedContentTransitionScope.SlideDirection.Right,
118 | )
119 | },
120 | replaceEnterTransition = { fadeIn() },
121 | replaceExitTransition = { fadeOut() },
122 | ),
123 | )
124 | }
125 | }
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/common/CollectWithLifecycleEffect.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.common
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Immutable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.compose.runtime.NonRestartableComposable
7 | import androidx.compose.runtime.RememberObserver
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.runtime.rememberUpdatedState
10 | import com.hoc081098.solivagant.lifecycle.Lifecycle
11 | import com.hoc081098.solivagant.lifecycle.LifecycleOwner
12 | import com.hoc081098.solivagant.lifecycle.LocalLifecycleOwner
13 | import com.hoc081098.solivagant.lifecycle.repeatOnLifecycle
14 | import kotlinx.coroutines.CoroutineDispatcher
15 | import kotlinx.coroutines.CoroutineScope
16 | import kotlinx.coroutines.Dispatchers
17 | import kotlinx.coroutines.Job
18 | import kotlinx.coroutines.cancel
19 | import kotlinx.coroutines.flow.Flow
20 | import kotlinx.coroutines.launch
21 |
22 | @Immutable
23 | enum class CollectWithLifecycleEffectDispatcher {
24 | /**
25 | * Use [Dispatchers.Main][kotlinx.coroutines.MainCoroutineDispatcher].
26 | */
27 | Main,
28 |
29 | /**
30 | * Use [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
31 | */
32 | ImmediateMain,
33 |
34 | /**
35 | * Use [androidx.compose.runtime.Composer.applyCoroutineContext].
36 | * Under the hood, it uses Compose [androidx.compose.ui.platform.AndroidUiDispatcher].
37 | */
38 | Composer,
39 | }
40 |
41 | /**
42 | * Collect the given [Flow] in an effect that runs when [LifecycleOwner.lifecycle] is at least at [minActiveState].
43 | *
44 | * - If [dispatcher] is [CollectWithLifecycleEffectDispatcher.ImmediateMain], the effect will run in
45 | * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
46 | * - If [dispatcher] is [CollectWithLifecycleEffectDispatcher.Main], the effect will run in
47 | * [Dispatchers.Main][kotlinx.coroutines.MainCoroutineDispatcher].
48 | * - If [dispatcher] is [CollectWithLifecycleEffectDispatcher.Composer], the effect will run in
49 | * [androidx.compose.runtime.Composer.applyCoroutineContext].
50 | *
51 | * NOTE: When [dispatcher] or [collector] changes, the effect will **NOT** be restarted.
52 | * The latest [collector] will be used to receive values from the [Flow] ([rememberUpdatedState] is used).
53 | * If you want to restart the effect, you need to change [keys].
54 | *
55 | * @param keys Keys to be used to [remember] the effect.
56 | * @param lifecycleOwner The [LifecycleOwner] to be used to [repeatOnLifecycle].
57 | * @param minActiveState The minimum [Lifecycle.State] to be used to [repeatOnLifecycle].
58 | * @param dispatcher The dispatcher to be used to launch the [Flow].
59 | * @param collector The collector to be used to collect the [Flow].
60 | *
61 | * @see [LaunchedEffect]
62 | * @see [CollectWithLifecycleEffectDispatcher]
63 | */
64 | @Composable
65 | fun Flow.CollectWithLifecycleEffect(
66 | vararg keys: Any?,
67 | lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
68 | minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
69 | dispatcher: CollectWithLifecycleEffectDispatcher = CollectWithLifecycleEffectDispatcher.ImmediateMain,
70 | collector: (T) -> Unit,
71 | ) {
72 | val flow = this
73 | val collectorState = rememberUpdatedState(collector)
74 |
75 | val block: suspend CoroutineScope.() -> Unit = {
76 | lifecycleOwner.repeatOnLifecycle(minActiveState) {
77 | // NOTE: we don't use `flow.collect(collectState.value)` because it can use the old value
78 | flow.collect { collectorState.value(it) }
79 | }
80 | }
81 |
82 | when (dispatcher) {
83 | CollectWithLifecycleEffectDispatcher.ImmediateMain -> {
84 | LaunchedEffectInImmediateMain(flow, lifecycleOwner, minActiveState, *keys, block = block)
85 | }
86 |
87 | CollectWithLifecycleEffectDispatcher.Main -> {
88 | LaunchedEffectInMain(flow, lifecycleOwner, minActiveState, *keys, block = block)
89 | }
90 |
91 | CollectWithLifecycleEffectDispatcher.Composer -> {
92 | LaunchedEffect(flow, lifecycleOwner, minActiveState, *keys, block = block)
93 | }
94 | }
95 | }
96 |
97 | @Composable
98 | @NonRestartableComposable
99 | @Suppress("ArrayReturn")
100 | private fun LaunchedEffectInImmediateMain(
101 | vararg keys: Any?,
102 | block: suspend CoroutineScope.() -> Unit,
103 | ) {
104 | remember(*keys) { LaunchedEffectImpl(block, Dispatchers.Main.immediate) }
105 | }
106 |
107 | @Composable
108 | @NonRestartableComposable
109 | @Suppress("ArrayReturn")
110 | private fun LaunchedEffectInMain(
111 | vararg keys: Any?,
112 | block: suspend CoroutineScope.() -> Unit,
113 | ) {
114 | remember(*keys) { LaunchedEffectImpl(block, Dispatchers.Main) }
115 | }
116 |
117 | private class LaunchedEffectImpl(
118 | private val task: suspend CoroutineScope.() -> Unit,
119 | dispatcher: CoroutineDispatcher,
120 | ) : RememberObserver {
121 | private val scope = CoroutineScope(dispatcher)
122 | private var job: Job? = null
123 |
124 | override fun onRemembered() {
125 | job?.cancel("Old job was still running!")
126 | job = scope.launch(block = task)
127 | }
128 |
129 | override fun onForgotten() {
130 | job?.cancel()
131 | job = null
132 | }
133 |
134 | override fun onAbandoned() {
135 | job?.cancel()
136 | job = null
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/common/OnLifecycleEvent.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.common
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.DisposableEffect
5 | import androidx.compose.runtime.Stable
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.runtime.rememberUpdatedState
10 | import androidx.compose.runtime.setValue
11 | import com.hoc081098.solivagant.lifecycle.Lifecycle
12 | import com.hoc081098.solivagant.lifecycle.LifecycleOwner
13 | import com.hoc081098.solivagant.lifecycle.LocalLifecycleOwner
14 |
15 | @Suppress("unused")
16 | @Composable
17 | fun OnLifecycleEvent(
18 | vararg keys: Any?,
19 | lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
20 | onEvent: (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit,
21 | ) {
22 | val eventHandler by rememberUpdatedState(onEvent)
23 |
24 | DisposableEffect(*keys, lifecycleOwner) {
25 | val observer = Lifecycle.Observer { event ->
26 | eventHandler(lifecycleOwner, event)
27 | }
28 | val cancellable = lifecycleOwner.lifecycle.subscribe(observer)
29 |
30 | onDispose(cancellable::cancel)
31 | }
32 | }
33 |
34 | typealias LifecycleEventListener = (owner: LifecycleOwner) -> Unit
35 | typealias LifecycleEachEventListener = (owner: LifecycleOwner, event: Lifecycle.Event) -> Unit
36 |
37 | @DslMarker
38 | annotation class LifecycleEventBuilderMarker
39 |
40 | @Stable
41 | @LifecycleEventBuilderMarker
42 | class LifecycleEventBuilder {
43 | private var onCreate: LifecycleEventListener? by mutableStateOf(null)
44 | private var onStart: LifecycleEventListener? by mutableStateOf(null)
45 | private var onResume: LifecycleEventListener? by mutableStateOf(null)
46 | private var onPause: LifecycleEventListener? by mutableStateOf(null)
47 | private var onStop: LifecycleEventListener? by mutableStateOf(null)
48 | private var onDestroy: LifecycleEventListener? by mutableStateOf(null)
49 | private var onEach: LifecycleEachEventListener? by mutableStateOf(null)
50 |
51 | @LifecycleEventBuilderMarker
52 | fun onCreate(block: LifecycleEventListener) {
53 | onCreate = block
54 | }
55 |
56 | @LifecycleEventBuilderMarker
57 | fun onStart(block: LifecycleEventListener) {
58 | onStart = block
59 | }
60 |
61 | @LifecycleEventBuilderMarker
62 | fun onResume(block: LifecycleEventListener) {
63 | onResume = block
64 | }
65 |
66 | @LifecycleEventBuilderMarker
67 | fun onPause(block: LifecycleEventListener) {
68 | onPause = block
69 | }
70 |
71 | @LifecycleEventBuilderMarker
72 | fun onStop(block: LifecycleEventListener) {
73 | onStop = block
74 | }
75 |
76 | @LifecycleEventBuilderMarker
77 | fun onDestroy(block: LifecycleEventListener) {
78 | onDestroy = block
79 | }
80 |
81 | @LifecycleEventBuilderMarker
82 | fun onEach(block: LifecycleEachEventListener) {
83 | onEach = block
84 | }
85 |
86 | internal fun buildLifecycleEventObserver(owner: LifecycleOwner) = Lifecycle.Observer { event ->
87 | when (event) {
88 | Lifecycle.Event.ON_CREATE -> onCreate
89 | Lifecycle.Event.ON_START -> onStart
90 | Lifecycle.Event.ON_RESUME -> onResume
91 | Lifecycle.Event.ON_PAUSE -> onPause
92 | Lifecycle.Event.ON_STOP -> onStop
93 | Lifecycle.Event.ON_DESTROY -> onDestroy
94 | }?.invoke(owner)
95 |
96 | onEach?.invoke(owner, event)
97 | }
98 | }
99 |
100 | @Composable
101 | fun OnLifecycleEventWithBuilder(
102 | vararg keys: Any?,
103 | lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
104 | builder: LifecycleEventBuilder.() -> Unit,
105 | ) {
106 | val lifecycleEventBuilder = remember { LifecycleEventBuilder() }
107 | val observer = remember(lifecycleOwner) { lifecycleEventBuilder.buildLifecycleEventObserver(lifecycleOwner) }
108 |
109 | // When builder or lifecycleOwner or keys changes, we need to re-execute the effect
110 | DisposableEffect(builder, lifecycleOwner, *keys) {
111 | // This make sure all callbacks are always up to date.
112 | builder(lifecycleEventBuilder)
113 |
114 | val cancellable = lifecycleOwner.lifecycle.subscribe(observer)
115 | onDispose(cancellable::cancel)
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/common/SingleEventChannel.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.common
2 |
3 | import com.hoc081098.kmp.viewmodel.MainThread
4 | import com.hoc081098.kmp.viewmodel.ViewModel
5 | import io.github.aakira.napier.Napier
6 | import java.io.Closeable
7 | import kotlin.LazyThreadSafetyMode.NONE
8 | import kotlinx.coroutines.channels.Channel
9 | import kotlinx.coroutines.channels.onClosed
10 | import kotlinx.coroutines.channels.onFailure
11 | import kotlinx.coroutines.channels.onSuccess
12 | import kotlinx.coroutines.flow.Flow
13 | import kotlinx.coroutines.flow.FlowCollector
14 | import kotlinx.coroutines.flow.emitAll
15 | import kotlinx.coroutines.flow.receiveAsFlow
16 |
17 | sealed interface SingleEventFlow : Flow {
18 | /**
19 | * Must collect in [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
20 | * Safe to call in the coroutines launched by [androidx.lifecycle.lifecycleScope].
21 | *
22 | * In Compose, we can use [CollectWithLifecycleEffect] with [CollectWithLifecycleEffectDispatcher.ImmediateMain].
23 | */
24 | @MainThread
25 | override suspend fun collect(collector: FlowCollector)
26 | }
27 |
28 | @MainThread
29 | interface HasSingleEventFlow {
30 | /**
31 | * Must collect in [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
32 | * Safe to call in the coroutines launched by [androidx.lifecycle.lifecycleScope].
33 | *
34 | * In Compose, we can use [CollectWithLifecycleEffect] with [CollectWithLifecycleEffectDispatcher.ImmediateMain].
35 | */
36 | val singleEventFlow: SingleEventFlow
37 | }
38 |
39 | @MainThread
40 | sealed interface SingleEventFlowSender {
41 | /**
42 | * Must call in [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
43 | * Safe to call in the coroutines launched by [androidx.lifecycle.viewModelScope].
44 | */
45 | suspend fun sendEvent(event: E)
46 | }
47 |
48 | private class SingleEventFlowImpl(private val channel: Channel) : SingleEventFlow {
49 | override suspend fun collect(collector: FlowCollector) {
50 | debugCheckImmediateMainDispatcher()
51 | return collector.emitAll(channel.receiveAsFlow())
52 | }
53 | }
54 |
55 | @MainThread
56 | class SingleEventChannel :
57 | Closeable,
58 | HasSingleEventFlow,
59 | SingleEventFlowSender {
60 | private val _eventChannel = Channel(Channel.UNLIMITED)
61 |
62 | override val singleEventFlow: SingleEventFlow by lazy(NONE) { SingleEventFlowImpl(_eventChannel) }
63 |
64 | init {
65 | Napier.d("[EventChannel] created: hashCode=${identityHashCode()}")
66 | }
67 |
68 | /**
69 | * Must be called in Dispatchers.Main.immediate, otherwise it will throw an exception.
70 | * If you want to send an event from other Dispatcher,
71 | * use `withContext(Dispatchers.Main.immediate) { eventChannel.send(event) }`
72 | */
73 | @MainThread
74 | override suspend fun sendEvent(event: E) {
75 | debugCheckImmediateMainDispatcher()
76 |
77 | _eventChannel
78 | .trySend(event)
79 | .onClosed { return }
80 | .onFailure { Napier.e("[EventChannel] Failed to send event: $event, hashCode=${identityHashCode()}", it) }
81 | .onSuccess { Napier.d("[EventChannel] Sent event: $event, hashCode=${identityHashCode()}") }
82 | }
83 |
84 | override fun close() {
85 | _eventChannel.close()
86 | Napier.d("[EventChannel] closed: hashCode=${identityHashCode()}")
87 | }
88 | }
89 |
90 | @Suppress("NOTHING_TO_INLINE")
91 | inline fun SingleEventChannel.addToViewModel(viewModel: ViewModel) =
92 | apply { viewModel.addCloseable(this) }
93 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/common/debugCheckImmediateMainDispatcher.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.common
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.currentCoroutineContext
6 |
7 | expect fun isBuildDebug(): Boolean
8 |
9 | @OptIn(ExperimentalStdlibApi::class)
10 | suspend inline fun debugCheckImmediateMainDispatcher() {
11 | if (isBuildDebug()) {
12 | val dispatcher = currentCoroutineContext()[CoroutineDispatcher]!!
13 |
14 | check(
15 | dispatcher === Dispatchers.Main.immediate ||
16 | !dispatcher.isDispatchNeeded(Dispatchers.Main.immediate),
17 | ) {
18 | "Expected CoroutineDispatcher to be Dispatchers.Main.immediate but was $dispatcher"
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/common/identityHashCode.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.common
2 |
3 | internal expect fun Any?.identityHashCode(): Int
4 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/common/rememberSharedViewModelOnRoute.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.common
2 |
3 | import androidx.compose.runtime.Composable
4 | import com.hoc081098.kmp.viewmodel.Closeable
5 | import com.hoc081098.kmp.viewmodel.ViewModel
6 | import com.hoc081098.kmp.viewmodel.ViewModelStore
7 | import com.hoc081098.kmp.viewmodel.ViewModelStoreOwner
8 | import com.hoc081098.kmp.viewmodel.koin.compose.koinKmpViewModel
9 | import com.hoc081098.solivagant.navigation.BaseRoute
10 | import com.hoc081098.solivagant.navigation.rememberCloseableOnRoute
11 |
12 | @Composable
13 | inline fun koinSharedViewModelOnRoute(route: BaseRoute): T {
14 | val viewModelStoreOwner = rememberCloseableOnRoute(
15 | route = route,
16 | factory = ::SharedVMStoreOwner,
17 | )
18 | return koinKmpViewModel(viewModelStoreOwner = viewModelStoreOwner)
19 | }
20 |
21 | @PublishedApi
22 | internal class SharedVMStoreOwner : ViewModelStoreOwner, Closeable {
23 | @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
24 | // TODO: When Solivagant supports getting ViewModelStoreOwner by NavRoute, remove this workaround.
25 | override val viewModelStore: ViewModelStore = com.hoc081098.solivagant.navigation.internal.createViewModelStore()
26 |
27 | override fun close() = viewModelStore.clear()
28 | }
29 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/di.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose
2 |
3 | import com.hoc081098.channeleventbus.ChannelEventBus
4 | import com.hoc081098.channeleventbus.ChannelEventBusLogger
5 | import com.hoc081098.channeleventbus.sample.kmp.compose.common.SingleEventChannel
6 | import com.hoc081098.channeleventbus.sample.kmp.compose.common.isBuildDebug
7 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.home.HomeModule
8 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.RegisterModule
9 | import com.hoc081098.solivagant.navigation.NavEventNavigator
10 | import io.github.aakira.napier.DebugAntilog
11 | import io.github.aakira.napier.Napier
12 | import org.koin.core.context.startKoin
13 | import org.koin.core.module.KoinApplicationDslMarker
14 | import org.koin.core.module.dsl.singleOf
15 | import org.koin.dsl.KoinAppDeclaration
16 | import org.koin.dsl.module
17 |
18 | val ChannelEventBusModule = module {
19 | single {
20 | ChannelEventBus(
21 | if (isBuildDebug()) {
22 | ChannelEventBusLogger.stdout()
23 | } else {
24 | ChannelEventBusLogger.noop()
25 | },
26 | )
27 | }
28 |
29 | factory { SingleEventChannel() }
30 | }
31 |
32 | val NavigatorModule = module {
33 | singleOf(::NavEventNavigator)
34 | }
35 |
36 | val CommonModule = module {
37 | includes(
38 | ChannelEventBusModule,
39 | RegisterModule,
40 | HomeModule,
41 | NavigatorModule,
42 | )
43 | }
44 |
45 | @KoinApplicationDslMarker
46 | fun startKoinCommon(appDeclaration: KoinAppDeclaration = {}) {
47 | startKoin {
48 | appDeclaration()
49 | modules(CommonModule)
50 | }
51 | }
52 |
53 | fun setupNapier() {
54 | if (isBuildDebug()) {
55 | Napier.base(DebugAntilog())
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/home/detail/DetailScreen.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.home.detail
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.text.KeyboardActions
12 | import androidx.compose.foundation.text.KeyboardOptions
13 | import androidx.compose.material3.ElevatedButton
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.material3.OutlinedTextField
16 | import androidx.compose.material3.Text
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.getValue
19 | import androidx.compose.runtime.remember
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.text.input.ImeAction
23 | import androidx.compose.ui.text.style.TextAlign
24 | import androidx.compose.ui.unit.dp
25 | import com.hoc081098.channeleventbus.sample.kmp.compose.common.CollectWithLifecycleEffect
26 | import com.hoc081098.kmp.viewmodel.koin.compose.koinKmpViewModel
27 | import com.hoc081098.solivagant.lifecycle.compose.collectAsStateWithLifecycle
28 | import kotlinx.coroutines.Dispatchers
29 |
30 | @Composable
31 | fun DetailScreen(
32 | modifier: Modifier = Modifier,
33 | vm: DetailVM = koinKmpViewModel(),
34 | ) {
35 | vm.singleEventFlow.CollectWithLifecycleEffect { event ->
36 | when (event) {
37 | DetailSingleEvent.Complete ->
38 | Unit
39 | }
40 | }
41 |
42 | val text by vm
43 | .textStateFlow
44 | .collectAsStateWithLifecycle(context = Dispatchers.Main.immediate)
45 |
46 | Box(
47 | modifier = modifier.fillMaxSize(),
48 | contentAlignment = Alignment.Center,
49 | ) {
50 | Column(
51 | modifier = Modifier.matchParentSize(),
52 | horizontalAlignment = Alignment.CenterHorizontally,
53 | verticalArrangement = Arrangement.Top,
54 | ) {
55 | Text(
56 | text = "Detail",
57 | textAlign = TextAlign.Center,
58 | style = MaterialTheme.typography.titleLarge,
59 | )
60 |
61 | Spacer(modifier = Modifier.height(16.dp))
62 |
63 | OutlinedTextField(
64 | modifier = Modifier
65 | .fillMaxWidth()
66 | .padding(horizontal = 16.dp),
67 | value = text,
68 | onValueChange = remember { vm::onTextChanged },
69 | singleLine = true,
70 | maxLines = 1,
71 | label = { Text("Enter the text") },
72 | keyboardActions = KeyboardActions.Default,
73 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
74 | )
75 |
76 | Spacer(modifier = Modifier.height(16.dp))
77 |
78 | ElevatedButton(
79 | enabled = text.isNotBlank(),
80 | onClick = remember { vm::sendResultToHome },
81 | ) {
82 | Text(text = "Send to home screen")
83 | }
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/home/detail/DetailScreenRoute.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.home.detail
2 |
3 | import androidx.compose.runtime.Immutable
4 | import com.hoc081098.kmp.viewmodel.parcelable.Parcelize
5 | import com.hoc081098.solivagant.navigation.NavDestination
6 | import com.hoc081098.solivagant.navigation.NavRoute
7 | import com.hoc081098.solivagant.navigation.ScreenDestination
8 |
9 | @Immutable
10 | @Parcelize
11 | data object DetailScreenRoute : NavRoute
12 |
13 | @JvmField
14 | val DetailScreenDestination: NavDestination =
15 | ScreenDestination { _, modifier ->
16 | DetailScreen(
17 | modifier = modifier,
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/home/detail/DetailVM.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.home.detail
2 |
3 | import androidx.compose.runtime.Immutable
4 | import com.hoc081098.channeleventbus.ChannelEventBus
5 | import com.hoc081098.channeleventbus.sample.kmp.compose.common.HasSingleEventFlow
6 | import com.hoc081098.channeleventbus.sample.kmp.compose.common.SingleEventChannel
7 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.home.DetailResultToHomeEvent
8 | import com.hoc081098.channeleventbus.sample.kmp.compose.utils.NonBlankString.Companion.toNonBlankString
9 | import com.hoc081098.channeleventbus.sample.kmp.compose.utils.launchNowIn
10 | import com.hoc081098.flowext.flowFromSuspend
11 | import com.hoc081098.kmp.viewmodel.SavedStateHandle
12 | import com.hoc081098.kmp.viewmodel.ViewModel
13 | import com.hoc081098.kmp.viewmodel.safe.NonNullSavedStateHandleKey
14 | import com.hoc081098.kmp.viewmodel.safe.safe
15 | import com.hoc081098.kmp.viewmodel.safe.string
16 | import com.hoc081098.solivagant.navigation.NavEventNavigator
17 | import io.github.aakira.napier.Napier
18 | import kotlinx.coroutines.ExperimentalCoroutinesApi
19 | import kotlinx.coroutines.delay
20 | import kotlinx.coroutines.flow.Flow
21 | import kotlinx.coroutines.flow.MutableSharedFlow
22 | import kotlinx.coroutines.flow.StateFlow
23 | import kotlinx.coroutines.flow.flatMapLatest
24 | import kotlinx.coroutines.launch
25 |
26 | @Immutable
27 | sealed interface DetailSingleEvent {
28 | data object Complete : DetailSingleEvent
29 | }
30 |
31 | @OptIn(ExperimentalCoroutinesApi::class)
32 | class DetailVM(
33 | private val channelEventBus: ChannelEventBus,
34 | private val singleEventChannel: SingleEventChannel,
35 | private val savedStateHandle: SavedStateHandle,
36 | private val navigator: NavEventNavigator,
37 | ) : ViewModel(singleEventChannel),
38 | HasSingleEventFlow by singleEventChannel {
39 | private val sendResultFlow = MutableSharedFlow(extraBufferCapacity = 1)
40 |
41 | internal val textStateFlow: StateFlow = savedStateHandle.safe.getStateFlow(TextKey)
42 |
43 | init {
44 | fun process(): Flow = flowFromSuspend {
45 | delay(@Suppress("MagicNumber") 500) // simulate a long-running task
46 |
47 | textStateFlow.value
48 | .toNonBlankString()
49 | .map(::DetailResultToHomeEvent)
50 | .onSuccess(channelEventBus::send)
51 | .onSuccess {
52 | singleEventChannel.sendEvent(DetailSingleEvent.Complete)
53 | navigator.navigateBack()
54 | }
55 | .onFailure { Napier.e("Error while sending result to home", it) }
56 | }
57 |
58 | sendResultFlow
59 | .flatMapLatest { process() }
60 | .launchNowIn(viewModelScope)
61 | }
62 |
63 | internal fun onTextChanged(text: String) {
64 | savedStateHandle.safe[TextKey] = text
65 | }
66 |
67 | internal fun sendResultToHome() {
68 | viewModelScope.launch { sendResultFlow.emit(Unit) }
69 | }
70 |
71 | private companion object {
72 | private val TextKey = NonNullSavedStateHandleKey.string("text", "")
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/home/di.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.home
2 |
3 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.home.detail.DetailVM
4 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.home.home.HomeVM
5 | import org.koin.core.module.dsl.factoryOf
6 | import org.koin.dsl.module
7 |
8 | val HomeModule = module {
9 | factoryOf(::DetailVM)
10 | factoryOf(::HomeVM)
11 | }
12 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/home/events.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.home
2 |
3 | import com.hoc081098.channeleventbus.ChannelEvent
4 | import com.hoc081098.channeleventbus.ChannelEventKey
5 | import com.hoc081098.channeleventbus.sample.kmp.compose.utils.NonBlankString
6 |
7 | internal data class DetailResultToHomeEvent(val value: NonBlankString) : ChannelEvent {
8 | override val key get() = Key
9 |
10 | companion object Key : ChannelEventKey(DetailResultToHomeEvent::class)
11 | }
12 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/home/home/HomeScreen.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.home.home
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.PaddingValues
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.lazy.LazyColumn
11 | import androidx.compose.foundation.lazy.items
12 | import androidx.compose.material3.ElevatedButton
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.getValue
17 | import androidx.compose.runtime.remember
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.text.style.TextAlign
21 | import androidx.compose.ui.text.style.TextOverflow
22 | import androidx.compose.ui.unit.dp
23 | import com.hoc081098.kmp.viewmodel.koin.compose.koinKmpViewModel
24 | import com.hoc081098.solivagant.lifecycle.compose.collectAsStateWithLifecycle
25 |
26 | @Composable
27 | fun HomeScreen(
28 | modifier: Modifier = Modifier,
29 | vm: HomeVM = koinKmpViewModel(),
30 | ) {
31 | val detailResults by vm.detailResultsStateFlow.collectAsStateWithLifecycle()
32 |
33 | Box(
34 | modifier = modifier.fillMaxSize(),
35 | contentAlignment = Alignment.Center,
36 | ) {
37 | Column(
38 | modifier = Modifier.matchParentSize(),
39 | horizontalAlignment = Alignment.CenterHorizontally,
40 | verticalArrangement = Arrangement.Center,
41 | ) {
42 | Text(
43 | text = "Home",
44 | textAlign = TextAlign.Center,
45 | style = MaterialTheme.typography.titleLarge,
46 | )
47 |
48 | Spacer(modifier = Modifier.height(8.dp))
49 |
50 | ElevatedButton(
51 | onClick = remember(vm) { vm::navigateToDetail },
52 | ) {
53 | Text(
54 | text = "Click to go to detail",
55 | )
56 | }
57 |
58 | LazyColumn(
59 | modifier = Modifier.weight(1f),
60 | contentPadding = PaddingValues(all = 16.dp),
61 | verticalArrangement = Arrangement.spacedBy(8.dp),
62 | ) {
63 | items(
64 | items = detailResults,
65 | key = { it.asString() },
66 | contentType = { "DetailResult" },
67 | ) { result ->
68 | Text(
69 | modifier = Modifier.fillParentMaxWidth(),
70 | text = result.asString(),
71 | style = MaterialTheme.typography.bodyMedium,
72 | textAlign = TextAlign.Start,
73 | maxLines = 2,
74 | overflow = TextOverflow.Ellipsis,
75 | )
76 | }
77 | }
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/home/home/HomeScreenRoute.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.home.home
2 |
3 | import androidx.compose.runtime.Immutable
4 | import com.hoc081098.kmp.viewmodel.parcelable.Parcelize
5 | import com.hoc081098.solivagant.navigation.NavDestination
6 | import com.hoc081098.solivagant.navigation.NavRoot
7 | import com.hoc081098.solivagant.navigation.ScreenDestination
8 |
9 | @Immutable
10 | @Parcelize
11 | data object HomeScreenRoute : NavRoot
12 |
13 | @JvmField
14 | val HomeScreenRouteDestination: NavDestination =
15 | ScreenDestination { _, modifier ->
16 | HomeScreen(
17 | modifier = modifier,
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/home/home/HomeVM.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.home.home
2 |
3 | import com.hoc081098.channeleventbus.ChannelEventBus
4 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.home.DetailResultToHomeEvent
5 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.home.detail.DetailScreenRoute
6 | import com.hoc081098.channeleventbus.sample.kmp.compose.utils.NonBlankString
7 | import com.hoc081098.kmp.viewmodel.ViewModel
8 | import com.hoc081098.solivagant.navigation.NavEventNavigator
9 | import kotlinx.collections.immutable.PersistentList
10 | import kotlinx.collections.immutable.persistentListOf
11 | import kotlinx.collections.immutable.plus
12 | import kotlinx.coroutines.CancellationException
13 | import kotlinx.coroutines.flow.SharingStarted
14 | import kotlinx.coroutines.flow.StateFlow
15 | import kotlinx.coroutines.flow.map
16 | import kotlinx.coroutines.flow.onCompletion
17 | import kotlinx.coroutines.flow.scan
18 | import kotlinx.coroutines.flow.stateIn
19 |
20 | class HomeVM(
21 | channelEventBus: ChannelEventBus,
22 | private val navigator: NavEventNavigator,
23 | ) : ViewModel() {
24 | internal val detailResultsStateFlow: StateFlow> = channelEventBus
25 | .receiveAsFlow(DetailResultToHomeEvent)
26 | .onCompletion {
27 | check(it is CancellationException) { "Expected CancellationException but was $it" }
28 |
29 | // Close the bus when this ViewModel is cleared.
30 | channelEventBus.closeKey(DetailResultToHomeEvent)
31 | }
32 | .map { it.value }
33 | .scan(persistentListOf()) { acc, value ->
34 | if (value in acc) {
35 | acc
36 | } else {
37 | acc + value
38 | }
39 | }
40 | .stateIn(
41 | scope = viewModelScope,
42 | // Using SharingStarted.Lazily is enough, because the event bus is backed by a channel.
43 | started = SharingStarted.Lazily,
44 | initialValue = persistentListOf(),
45 | )
46 |
47 | internal fun navigateToDetail() = navigator.navigateTo(DetailScreenRoute)
48 | }
49 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/register/RegisterSharedVM.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.register
2 |
3 | import com.hoc081098.channeleventbus.ChannelEventBus
4 | import com.hoc081098.channeleventbus.ValidationBeforeClosing.Companion.NONE
5 | import com.hoc081098.channeleventbus.sample.kmp.compose.common.isBuildDebug
6 | import com.hoc081098.kmp.viewmodel.SavedStateHandle
7 | import com.hoc081098.kmp.viewmodel.ViewModel
8 | import com.hoc081098.kmp.viewmodel.safe.safe
9 | import io.github.aakira.napier.Napier
10 | import kotlinx.coroutines.flow.SharingStarted
11 | import kotlinx.coroutines.flow.StateFlow
12 | import kotlinx.coroutines.flow.combine
13 | import kotlinx.coroutines.flow.distinctUntilChanged
14 | import kotlinx.coroutines.flow.launchIn
15 | import kotlinx.coroutines.flow.onEach
16 | import kotlinx.coroutines.flow.stateIn
17 |
18 | class RegisterSharedVM(
19 | private val channelEventBus: ChannelEventBus,
20 | private val savedStateHandle: SavedStateHandle,
21 | ) : ViewModel() {
22 | internal val uiStateFlow: StateFlow = combine(
23 | savedStateHandle.safe.getStateFlow(FirstNameKey),
24 | savedStateHandle.safe.getStateFlow(LastNameKey),
25 | savedStateHandle.safe.getStateFlow(GenderKey),
26 | ) { firstName, lastName, gender ->
27 | RegisterUiState.from(
28 | firstName = firstName,
29 | lastName = lastName,
30 | gender = gender,
31 | )
32 | }.stateIn(
33 | scope = viewModelScope,
34 | started = SharingStarted.WhileSubscribed(stopTimeoutMillis = @Suppress("MagicNumber") 5_000),
35 | initialValue = RegisterUiState.Unfilled,
36 | )
37 |
38 | init {
39 | Napier.d("$this::init")
40 | addCloseable {
41 | arrayOf(SubmitFirstNameEvent, SubmitLastNameEvent, SubmitGenderEvent).forEach {
42 | channelEventBus.closeKey(key = it, validations = NONE)
43 | }
44 | Napier.d("$this::close")
45 | }
46 |
47 | debugPrint()
48 | observeSubmitFirstNameEvent()
49 | observeSubmitLastNameEvent()
50 | observeSubmitGenderEvent()
51 | }
52 |
53 | private fun observeSubmitLastNameEvent() {
54 | channelEventBus
55 | .receiveAsFlow(SubmitLastNameEvent)
56 | .onEach { savedStateHandle.safe[LastNameKey] = it.value }
57 | .launchIn(viewModelScope)
58 | }
59 |
60 | private fun observeSubmitFirstNameEvent() {
61 | channelEventBus
62 | .receiveAsFlow(SubmitFirstNameEvent)
63 | .onEach { savedStateHandle.safe[FirstNameKey] = it.value }
64 | .launchIn(viewModelScope)
65 | }
66 |
67 | private fun observeSubmitGenderEvent() {
68 | channelEventBus
69 | .receiveAsFlow(SubmitGenderEvent)
70 | .onEach { savedStateHandle.safe[GenderKey] = it.value }
71 | .launchIn(viewModelScope)
72 | }
73 |
74 | private fun debugPrint() {
75 | if (!isBuildDebug()) {
76 | return
77 | }
78 |
79 | val line = "-".repeat(@Suppress("MagicNumber") 80)
80 |
81 | combine(
82 | savedStateHandle.safe.getStateFlow(FirstNameKey),
83 | savedStateHandle.safe.getStateFlow(LastNameKey),
84 | savedStateHandle.safe.getStateFlow(GenderKey),
85 | ) { array: Array<*> -> array.joinToString(separator = "\n") { " $it" } }
86 | .distinctUntilChanged()
87 | .onEach {
88 | Napier.d(
89 | """
90 | |$this::debugPrint $line
91 | |$it
92 | |$line
93 | """.trimMargin(),
94 | )
95 | }
96 | .launchIn(viewModelScope)
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/register/contract.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.register
2 |
3 | import androidx.compose.runtime.Immutable
4 | import androidx.compose.runtime.Stable
5 | import com.hoc081098.kmp.viewmodel.safe.NullableSavedStateHandleKey
6 | import com.hoc081098.kmp.viewmodel.safe.serializable
7 | import com.hoc081098.kmp.viewmodel.safe.string
8 | import com.hoc081098.kmp.viewmodel.serializable.JvmSerializable
9 | import kotlin.LazyThreadSafetyMode.PUBLICATION
10 |
11 | @Immutable
12 | internal enum class Gender : JvmSerializable {
13 | MALE,
14 | FEMALE,
15 | }
16 |
17 | @Stable
18 | internal val Gender.displayName: String
19 | get() = when (this) {
20 | Gender.MALE -> "Male"
21 | Gender.FEMALE -> "Female"
22 | }
23 |
24 | @Immutable
25 | internal sealed interface RegisterUiState {
26 | data object Unfilled : RegisterUiState
27 |
28 | data class Filled(
29 | val firstName: String,
30 | val lastName: String,
31 | val gender: Gender,
32 | ) : RegisterUiState
33 |
34 | companion object
35 | }
36 |
37 | internal fun RegisterUiState.Companion.from(
38 | firstName: String?,
39 | lastName: String?,
40 | gender: Gender?,
41 | ): RegisterUiState = if (
42 | firstName != null &&
43 | lastName != null &&
44 | gender != null
45 | ) {
46 | RegisterUiState.Filled(
47 | firstName = firstName,
48 | lastName = lastName,
49 | gender = gender,
50 | )
51 | } else {
52 | RegisterUiState.Unfilled
53 | }
54 |
55 | internal val FirstNameKey by lazy(PUBLICATION) {
56 | NullableSavedStateHandleKey.string(
57 | key = "first_name",
58 | defaultValue = null,
59 | )
60 | }
61 | internal val LastNameKey by lazy(PUBLICATION) {
62 | NullableSavedStateHandleKey.string(
63 | key = "last_name",
64 | defaultValue = null,
65 | )
66 | }
67 | internal val GenderKey by lazy(PUBLICATION) {
68 | NullableSavedStateHandleKey.serializable(
69 | key = "gender",
70 | defaultValue = null,
71 | )
72 | }
73 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/register/di.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.register
2 |
3 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.stepone.RegisterStepOneVM
4 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.stepthree.RegisterStepThreeVM
5 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.steptwo.RegisterStepTwoVM
6 | import org.koin.core.module.dsl.factoryOf
7 | import org.koin.dsl.module
8 |
9 | val RegisterModule = module {
10 | factoryOf(::RegisterSharedVM)
11 | factoryOf(::RegisterStepOneVM)
12 | factoryOf(::RegisterStepTwoVM)
13 | factoryOf(::RegisterStepThreeVM)
14 | }
15 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/register/events.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.register
2 |
3 | import com.hoc081098.channeleventbus.ChannelEvent
4 | import com.hoc081098.channeleventbus.ChannelEventBusCapacity.CONFLATED
5 | import com.hoc081098.channeleventbus.ChannelEventKey
6 |
7 | internal data class SubmitFirstNameEvent(val value: String?) : ChannelEvent {
8 | override val key get() = Key
9 |
10 | companion object Key : ChannelEventKey(SubmitFirstNameEvent::class, CONFLATED)
11 | }
12 |
13 | internal data class SubmitLastNameEvent(val value: String?) : ChannelEvent {
14 | override val key get() = Key
15 |
16 | companion object Key : ChannelEventKey(SubmitLastNameEvent::class, CONFLATED)
17 | }
18 |
19 | internal data class SubmitGenderEvent(val value: Gender?) : ChannelEvent {
20 | override val key get() = Key
21 |
22 | companion object Key : ChannelEventKey(SubmitGenderEvent::class, CONFLATED)
23 | }
24 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/register/stepone/RegisterStepOneScreen.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.stepone
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.height
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.text.KeyboardActions
10 | import androidx.compose.foundation.text.KeyboardOptions
11 | import androidx.compose.material3.ElevatedButton
12 | import androidx.compose.material3.OutlinedTextField
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.getValue
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.text.input.ImeAction
20 | import androidx.compose.ui.unit.dp
21 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.RegisterSharedVM
22 | import com.hoc081098.kmp.viewmodel.koin.compose.koinKmpViewModel
23 | import com.hoc081098.solivagant.lifecycle.compose.collectAsStateWithLifecycle
24 | import kotlinx.coroutines.Dispatchers
25 |
26 | @Composable
27 | fun RegisterStepOneScreen(
28 | registerSharedVM: RegisterSharedVM,
29 | modifier: Modifier = Modifier,
30 | vm: RegisterStepOneVM = koinKmpViewModel(),
31 | ) {
32 | registerSharedVM.toString()
33 |
34 | val firstName by vm
35 | .firstNameStateFlow
36 | .collectAsStateWithLifecycle(context = Dispatchers.Main.immediate)
37 |
38 | val lastName by vm
39 | .lastNameStateFlow
40 | .collectAsStateWithLifecycle(context = Dispatchers.Main.immediate)
41 |
42 | Column(
43 | modifier = modifier,
44 | verticalArrangement = Arrangement.Top,
45 | horizontalAlignment = Alignment.CenterHorizontally,
46 | ) {
47 | Spacer(modifier = Modifier.height(16.dp))
48 |
49 | OutlinedTextField(
50 | modifier = Modifier
51 | .fillMaxWidth()
52 | .padding(horizontal = 16.dp),
53 | value = firstName.orEmpty(),
54 | onValueChange = remember { vm::onFirstNameChanged },
55 | singleLine = true,
56 | maxLines = 1,
57 | label = { Text("First name") },
58 | keyboardActions = KeyboardActions.Default,
59 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
60 | )
61 |
62 | Spacer(modifier = Modifier.height(16.dp))
63 |
64 | OutlinedTextField(
65 | modifier = Modifier
66 | .fillMaxWidth()
67 | .padding(horizontal = 16.dp),
68 | value = lastName.orEmpty(),
69 | onValueChange = remember { vm::onLastNameChanged },
70 | singleLine = true,
71 | maxLines = 1,
72 | label = { Text("Last name") },
73 | keyboardActions = KeyboardActions.Default,
74 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
75 | )
76 |
77 | Spacer(modifier = Modifier.weight(1f))
78 |
79 | ElevatedButton(onClick = remember(vm) { vm::navigateToStepTwo }) {
80 | Text(text = "Next")
81 | }
82 |
83 | Spacer(modifier = Modifier.height(16.dp))
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/register/stepone/RegisterStepOneScreenRoute.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.stepone
2 |
3 | import androidx.compose.runtime.Immutable
4 | import com.hoc081098.channeleventbus.sample.kmp.compose.common.koinSharedViewModelOnRoute
5 | import com.hoc081098.kmp.viewmodel.parcelable.Parcelize
6 | import com.hoc081098.solivagant.navigation.NavDestination
7 | import com.hoc081098.solivagant.navigation.NavRoot
8 | import com.hoc081098.solivagant.navigation.ScreenDestination
9 |
10 | @Immutable
11 | @Parcelize
12 | data object RegisterStepOneScreenRoute : NavRoot
13 |
14 | @JvmField
15 | val RegisterStepOneScreenDestination: NavDestination =
16 | ScreenDestination { _, modifier ->
17 | RegisterStepOneScreen(
18 | registerSharedVM = koinSharedViewModelOnRoute(route = RegisterStepOneScreenRoute),
19 | modifier = modifier,
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/register/stepone/RegisterStepOneVM.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.stepone
2 |
3 | import com.hoc081098.channeleventbus.ChannelEventBus
4 | import com.hoc081098.channeleventbus.OptionWhenSendingToBusDoesNotExist
5 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.FirstNameKey
6 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.LastNameKey
7 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.SubmitFirstNameEvent
8 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.SubmitLastNameEvent
9 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.steptwo.RegisterStepTwoScreenRoute
10 | import com.hoc081098.kmp.viewmodel.SavedStateHandle
11 | import com.hoc081098.kmp.viewmodel.ViewModel
12 | import com.hoc081098.kmp.viewmodel.safe.safe
13 | import com.hoc081098.solivagant.navigation.NavEventNavigator
14 | import kotlinx.coroutines.CancellationException
15 | import kotlinx.coroutines.flow.StateFlow
16 | import kotlinx.coroutines.flow.launchIn
17 | import kotlinx.coroutines.flow.onCompletion
18 | import kotlinx.coroutines.flow.onEach
19 |
20 | class RegisterStepOneVM(
21 | private val savedStateHandle: SavedStateHandle,
22 | private val channelEventBus: ChannelEventBus,
23 | private val navigator: NavEventNavigator,
24 | ) : ViewModel() {
25 | internal val firstNameStateFlow: StateFlow = savedStateHandle.safe.getStateFlow(FirstNameKey)
26 | internal val lastNameStateFlow: StateFlow = savedStateHandle.safe.getStateFlow(LastNameKey)
27 |
28 | init {
29 | sendSubmitFirstNameEventAfterChanged()
30 | sendSubmitLastNameEventAfterChanged()
31 | }
32 |
33 | /**
34 | * Send [SubmitFirstNameEvent] to [ChannelEventBus] when [firstNameStateFlow] emits a new value
35 | */
36 | private fun sendSubmitFirstNameEventAfterChanged() {
37 | firstNameStateFlow
38 | .onEach { channelEventBus.send(SubmitFirstNameEvent(it)) }
39 | .onCompletion {
40 | check(it is CancellationException) { "Expected CancellationException but was $it" }
41 |
42 | // Send null to bus when this ViewModel is cleared, to clear the value in RegisterSharedVM.
43 | // Do nothing if the bus does not exist (ie. there is no active collector for this bus or the bus is closed).
44 | channelEventBus.send(
45 | event = SubmitFirstNameEvent(null),
46 | option = OptionWhenSendingToBusDoesNotExist.DO_NOTHING,
47 | )
48 | }
49 | .launchIn(viewModelScope)
50 | }
51 |
52 | /**
53 | * Send [SubmitLastNameEvent] to [ChannelEventBus] when [lastNameStateFlow] emits a new value
54 | */
55 | private fun sendSubmitLastNameEventAfterChanged() {
56 | lastNameStateFlow
57 | .onEach { channelEventBus.send(SubmitLastNameEvent(it)) }
58 | .onCompletion {
59 | check(it is CancellationException) { "Expected CancellationException but was $it" }
60 |
61 | // Send null to bus when this ViewModel is cleared, to clear the value in RegisterSharedVM.
62 | // Do nothing if the bus does not exist (ie. there is no active collector for this bus or the bus is closed).
63 | channelEventBus.send(
64 | event = SubmitLastNameEvent(null),
65 | option = OptionWhenSendingToBusDoesNotExist.DO_NOTHING,
66 | )
67 | }
68 | .launchIn(viewModelScope)
69 | }
70 |
71 | internal fun onFirstNameChanged(value: String) {
72 | savedStateHandle.safe[FirstNameKey] = value
73 | }
74 |
75 | internal fun onLastNameChanged(value: String) {
76 | savedStateHandle.safe[LastNameKey] = value
77 | }
78 |
79 | internal fun navigateToStepTwo() = navigator.navigateTo(RegisterStepTwoScreenRoute)
80 | }
81 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/register/stepthree/RegisterStepThreeScreen.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.stepthree
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.width
11 | import androidx.compose.material3.CircularProgressIndicator
12 | import androidx.compose.material3.ElevatedButton
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.getValue
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.text.font.FontWeight
20 | import androidx.compose.ui.text.style.TextAlign
21 | import androidx.compose.ui.unit.dp
22 | import com.hoc081098.channeleventbus.sample.kmp.compose.common.CollectWithLifecycleEffect
23 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.RegisterSharedVM
24 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.RegisterUiState
25 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.displayName
26 | import com.hoc081098.kmp.viewmodel.koin.compose.koinKmpViewModel
27 | import com.hoc081098.solivagant.lifecycle.compose.collectAsStateWithLifecycle
28 |
29 | @Suppress("LongMethod")
30 | @Composable
31 | fun RegisterStepThreeScreen(
32 | registerSharedVM: RegisterSharedVM,
33 | modifier: Modifier = Modifier,
34 | vm: RegisterStepThreeVM = koinKmpViewModel(),
35 | ) {
36 | val registerUiState by registerSharedVM
37 | .uiStateFlow
38 | .collectAsStateWithLifecycle()
39 |
40 | val uiState by vm.uiStateFlow.collectAsStateWithLifecycle()
41 |
42 | vm.singleEventFlow.CollectWithLifecycleEffect { event ->
43 | when (event) {
44 | is RegisterStepThreeSingleEvent.Failure -> {
45 | // TODO: Show toast
46 | }
47 |
48 | RegisterStepThreeSingleEvent.Success -> {
49 | // TODO: Show toast
50 | }
51 | }
52 | }
53 |
54 | Column(
55 | modifier = modifier,
56 | verticalArrangement = Arrangement.Top,
57 | horizontalAlignment = Alignment.CenterHorizontally,
58 | ) {
59 | Spacer(modifier = Modifier.height(16.dp))
60 |
61 | when (val s = registerUiState) {
62 | is RegisterUiState.Filled -> {
63 | RegisterInfo(
64 | modifier = Modifier
65 | .fillMaxWidth()
66 | .padding(horizontal = 16.dp),
67 | registerUiState = s,
68 | )
69 | }
70 |
71 | RegisterUiState.Unfilled -> {
72 | Text(
73 | modifier = Modifier
74 | .fillMaxWidth()
75 | .padding(horizontal = 16.dp),
76 | text = "Please fill all information",
77 | style = MaterialTheme.typography.titleLarge,
78 | fontWeight = FontWeight.Bold,
79 | textAlign = TextAlign.Center,
80 | )
81 | }
82 | }
83 |
84 | Spacer(modifier = Modifier.weight(1f))
85 |
86 | when (uiState) {
87 | RegisterStepThreeUiState.Idle,
88 | is RegisterStepThreeUiState.Failure,
89 | RegisterStepThreeUiState.Success,
90 | -> Unit
91 |
92 | RegisterStepThreeUiState.Registering ->
93 | CircularProgressIndicator(
94 | modifier = Modifier.padding(16.dp),
95 | )
96 | }
97 |
98 | ElevatedButton(
99 | enabled = registerUiState is RegisterUiState.Filled,
100 | onClick = {
101 | vm.register(
102 | registerSharedVM
103 | .uiStateFlow
104 | .value,
105 | )
106 | },
107 | ) {
108 | Text(text = "Register")
109 | }
110 |
111 | Spacer(modifier = Modifier.height(16.dp))
112 | }
113 | }
114 |
115 | @Composable
116 | private fun RegisterInfo(
117 | registerUiState: RegisterUiState.Filled,
118 | modifier: Modifier = Modifier,
119 | ) {
120 | Column(
121 | modifier = modifier,
122 | ) {
123 | SimpleTile(
124 | title = "First name: ",
125 | content = registerUiState.firstName,
126 | )
127 |
128 | Spacer(modifier = Modifier.height(8.dp))
129 |
130 | SimpleTile(
131 | title = "Last name: ",
132 | content = registerUiState.lastName,
133 | )
134 |
135 | Spacer(modifier = Modifier.height(8.dp))
136 |
137 | SimpleTile(
138 | title = "Gender: ",
139 | content = registerUiState.gender.displayName,
140 | )
141 |
142 | Spacer(modifier = Modifier.height(8.dp))
143 | }
144 | }
145 |
146 | @Composable
147 | private fun SimpleTile(
148 | title: String,
149 | content: String,
150 | modifier: Modifier = Modifier,
151 | ) {
152 | Row(
153 | modifier = modifier
154 | .fillMaxWidth(),
155 | ) {
156 | Text(
157 | text = title,
158 | style = MaterialTheme.typography.titleLarge,
159 | fontWeight = FontWeight.Bold,
160 | textAlign = TextAlign.Start,
161 | )
162 |
163 | Spacer(modifier = Modifier.width(8.dp))
164 |
165 | Text(
166 | modifier = Modifier
167 | .weight(1f),
168 | text = content,
169 | style = MaterialTheme.typography.bodyMedium,
170 | fontWeight = FontWeight.Normal,
171 | textAlign = TextAlign.End,
172 | )
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/register/stepthree/RegisterStepThreeScreenRoute.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.stepthree
2 |
3 | import androidx.compose.runtime.Immutable
4 | import com.hoc081098.channeleventbus.sample.kmp.compose.common.koinSharedViewModelOnRoute
5 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.stepone.RegisterStepOneScreenRoute
6 | import com.hoc081098.kmp.viewmodel.parcelable.Parcelize
7 | import com.hoc081098.solivagant.navigation.NavDestination
8 | import com.hoc081098.solivagant.navigation.NavRoute
9 | import com.hoc081098.solivagant.navigation.ScreenDestination
10 |
11 | @Immutable
12 | @Parcelize
13 | data object RegisterStepThreeScreenRoute : NavRoute
14 |
15 | @JvmField
16 | val RegisterStepThreeScreenDestination: NavDestination =
17 | ScreenDestination { route, modifier ->
18 | RegisterStepThreeScreen(
19 | registerSharedVM = koinSharedViewModelOnRoute(route = RegisterStepOneScreenRoute),
20 | modifier = modifier,
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/register/stepthree/RegisterStepThreeVM.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.stepthree
2 |
3 | import androidx.compose.runtime.Immutable
4 | import com.hoc081098.channeleventbus.sample.kmp.compose.common.HasSingleEventFlow
5 | import com.hoc081098.channeleventbus.sample.kmp.compose.common.SingleEventChannel
6 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.home.home.HomeScreenRoute
7 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.RegisterUiState
8 | import com.hoc081098.flowext.FlowExtPreview
9 | import com.hoc081098.flowext.catchAndReturn
10 | import com.hoc081098.flowext.flatMapFirst
11 | import com.hoc081098.flowext.flowFromSuspend
12 | import com.hoc081098.flowext.mapTo
13 | import com.hoc081098.flowext.startWith
14 | import com.hoc081098.kmp.viewmodel.ViewModel
15 | import com.hoc081098.solivagant.navigation.NavEventNavigator
16 | import io.github.aakira.napier.Napier
17 | import java.io.IOException
18 | import kotlin.random.Random
19 | import kotlinx.coroutines.delay
20 | import kotlinx.coroutines.flow.MutableSharedFlow
21 | import kotlinx.coroutines.flow.SharingStarted
22 | import kotlinx.coroutines.flow.StateFlow
23 | import kotlinx.coroutines.flow.onEach
24 | import kotlinx.coroutines.flow.stateIn
25 | import kotlinx.coroutines.launch
26 |
27 | @Immutable
28 | sealed interface RegisterStepThreeSingleEvent {
29 | data object Success : RegisterStepThreeSingleEvent
30 | data class Failure(val throwable: Throwable) : RegisterStepThreeSingleEvent
31 | }
32 |
33 | @Immutable
34 | internal sealed interface RegisterStepThreeUiState {
35 | data object Idle : RegisterStepThreeUiState
36 | data object Registering : RegisterStepThreeUiState
37 | data object Success : RegisterStepThreeUiState
38 | data class Failure(val throwable: Throwable) : RegisterStepThreeUiState
39 | }
40 |
41 | @OptIn(FlowExtPreview::class)
42 | class RegisterStepThreeVM(
43 | private val singleEventChannel: SingleEventChannel,
44 | private val navigator: NavEventNavigator,
45 | ) : ViewModel(singleEventChannel),
46 | HasSingleEventFlow by singleEventChannel {
47 | private val _registerFlow = MutableSharedFlow(extraBufferCapacity = 1)
48 |
49 | internal val uiStateFlow: StateFlow = _registerFlow
50 | .flatMapFirst { state ->
51 | flowFromSuspend { doRegister(state) }
52 | .mapTo(RegisterStepThreeUiState.Success)
53 | .startWith(RegisterStepThreeUiState.Registering)
54 | .catchAndReturn(RegisterStepThreeUiState.Idle)
55 | }
56 | .onEach(::sendEvent)
57 | .stateIn(
58 | scope = viewModelScope,
59 | started = SharingStarted.Eagerly,
60 | initialValue = RegisterStepThreeUiState.Idle,
61 | )
62 |
63 | private suspend fun sendEvent(state: RegisterStepThreeUiState) = when (state) {
64 | RegisterStepThreeUiState.Idle, RegisterStepThreeUiState.Registering ->
65 | Unit
66 |
67 | is RegisterStepThreeUiState.Failure ->
68 | singleEventChannel.sendEvent(RegisterStepThreeSingleEvent.Failure(state.throwable))
69 |
70 | RegisterStepThreeUiState.Success -> {
71 | singleEventChannel.sendEvent(RegisterStepThreeSingleEvent.Success)
72 | navigator.replaceAll(HomeScreenRoute)
73 | }
74 | }
75 |
76 | internal fun register(state: RegisterUiState) {
77 | when (state) {
78 | RegisterUiState.Unfilled -> {
79 | Napier.w("Unfilled state")
80 | return
81 | }
82 |
83 | is RegisterUiState.Filled -> {
84 | viewModelScope.launch { _registerFlow.emit(state) }
85 | }
86 | }
87 | }
88 | }
89 |
90 | private suspend fun doRegister(state: RegisterUiState.Filled) {
91 | Napier.d("doRegister $state")
92 |
93 | // simulate network request
94 | delay(@Suppress("MagicNumber") 2_000)
95 |
96 | if (Random.nextBoolean()) {
97 | Napier.e("Register failed")
98 | throw IOException("Network error")
99 | } else {
100 | Napier.d("Register success")
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/register/steptwo/RegisterStepTwoScreen.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.steptwo
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.width
11 | import androidx.compose.foundation.selection.selectable
12 | import androidx.compose.material3.ElevatedButton
13 | import androidx.compose.material3.RadioButton
14 | import androidx.compose.material3.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.getValue
17 | import androidx.compose.runtime.remember
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.unit.dp
21 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.Gender
22 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.RegisterSharedVM
23 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.displayName
24 | import com.hoc081098.kmp.viewmodel.koin.compose.koinKmpViewModel
25 | import com.hoc081098.solivagant.lifecycle.compose.collectAsStateWithLifecycle
26 | import kotlinx.coroutines.Dispatchers
27 |
28 | @Composable
29 | fun RegisterStepTwoScreen(
30 | registerSharedVM: RegisterSharedVM,
31 | modifier: Modifier = Modifier,
32 | vm: RegisterStepTwoVM = koinKmpViewModel(),
33 | ) {
34 | registerSharedVM.toString()
35 |
36 | val selectedGender by vm
37 | .genderStateFlow
38 | .collectAsStateWithLifecycle(context = Dispatchers.Main.immediate)
39 |
40 | Column(
41 | modifier = modifier,
42 | verticalArrangement = Arrangement.Top,
43 | horizontalAlignment = Alignment.CenterHorizontally,
44 | ) {
45 | Spacer(modifier = Modifier.height(16.dp))
46 |
47 | GenderSection(
48 | modifier = Modifier.fillMaxWidth(),
49 | selectedGender = selectedGender,
50 | onGenderChange = remember { vm::onGenderChanged },
51 | )
52 |
53 | Spacer(modifier = Modifier.weight(1f))
54 |
55 | ElevatedButton(onClick = remember(vm) { vm::navigateToStepThree }) {
56 | Text(text = "Next")
57 | }
58 |
59 | Spacer(modifier = Modifier.height(16.dp))
60 | }
61 | }
62 |
63 | @Composable
64 | private fun GenderSection(
65 | selectedGender: Gender?,
66 | onGenderChange: (Gender) -> Unit,
67 | modifier: Modifier = Modifier,
68 | ) {
69 | Column(modifier = modifier) {
70 | Gender.entries.forEach { item ->
71 | val onClick = { onGenderChange(item) }
72 | val selected = selectedGender == item
73 |
74 | Row(
75 | modifier = Modifier
76 | .fillMaxWidth()
77 | .selectable(
78 | selected = selected,
79 | onClick = onClick,
80 | )
81 | .padding(horizontal = 16.dp),
82 | verticalAlignment = Alignment.CenterVertically,
83 | horizontalArrangement = Arrangement.Start,
84 | ) {
85 | RadioButton(
86 | selected = selected,
87 | onClick = onClick,
88 | )
89 |
90 | Spacer(modifier = Modifier.width(8.dp))
91 |
92 | Text(
93 | modifier = Modifier.weight(1f),
94 | text = item.displayName,
95 | )
96 | }
97 |
98 | Spacer(modifier = Modifier.height(16.dp))
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/register/steptwo/RegisterStepTwoScreenRoute.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.steptwo
2 |
3 | import androidx.compose.runtime.Immutable
4 | import com.hoc081098.channeleventbus.sample.kmp.compose.common.koinSharedViewModelOnRoute
5 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.stepone.RegisterStepOneScreenRoute
6 | import com.hoc081098.kmp.viewmodel.parcelable.Parcelize
7 | import com.hoc081098.solivagant.navigation.NavDestination
8 | import com.hoc081098.solivagant.navigation.NavRoute
9 | import com.hoc081098.solivagant.navigation.ScreenDestination
10 |
11 | @Immutable
12 | @Parcelize
13 | data object RegisterStepTwoScreenRoute : NavRoute
14 |
15 | @JvmField
16 | val RegisterStepTwoScreenDestination: NavDestination =
17 | ScreenDestination { route, modifier ->
18 | RegisterStepTwoScreen(
19 | registerSharedVM = koinSharedViewModelOnRoute(route = RegisterStepOneScreenRoute),
20 | modifier = modifier,
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/ui/register/steptwo/RegisterStepTwoVM.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.steptwo
2 |
3 | import com.hoc081098.channeleventbus.ChannelEventBus
4 | import com.hoc081098.channeleventbus.OptionWhenSendingToBusDoesNotExist
5 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.Gender
6 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.GenderKey
7 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.SubmitGenderEvent
8 | import com.hoc081098.channeleventbus.sample.kmp.compose.ui.register.stepthree.RegisterStepThreeScreenRoute
9 | import com.hoc081098.kmp.viewmodel.SavedStateHandle
10 | import com.hoc081098.kmp.viewmodel.ViewModel
11 | import com.hoc081098.kmp.viewmodel.safe.safe
12 | import com.hoc081098.solivagant.navigation.NavEventNavigator
13 | import kotlinx.coroutines.CancellationException
14 | import kotlinx.coroutines.flow.StateFlow
15 | import kotlinx.coroutines.flow.launchIn
16 | import kotlinx.coroutines.flow.onCompletion
17 | import kotlinx.coroutines.flow.onEach
18 |
19 | class RegisterStepTwoVM(
20 | private val savedStateHandle: SavedStateHandle,
21 | private val channelEventBus: ChannelEventBus,
22 | private val navigator: NavEventNavigator,
23 | ) : ViewModel() {
24 | internal val genderStateFlow: StateFlow = savedStateHandle.safe.getStateFlow(GenderKey)
25 |
26 | init {
27 | sendSubmitGenderEventAfterChanged()
28 | }
29 |
30 | /**
31 | * Send [SubmitGenderEvent] to [ChannelEventBus] when [genderStateFlow] emits a new value
32 | */
33 | private fun sendSubmitGenderEventAfterChanged() {
34 | genderStateFlow
35 | .onEach { channelEventBus.send(SubmitGenderEvent(it)) }
36 | .onCompletion {
37 | check(it is CancellationException) { "Expected CancellationException but was $it" }
38 |
39 | // Send null to bus when this ViewModel is cleared, to clear the value in RegisterSharedVM.
40 | // Do nothing if the bus does not exist (ie. there is no active collector for this bus or the bus is closed).
41 | channelEventBus.send(
42 | event = SubmitGenderEvent(null),
43 | option = OptionWhenSendingToBusDoesNotExist.DO_NOTHING,
44 | )
45 | }
46 | .launchIn(viewModelScope)
47 | }
48 |
49 | internal fun onGenderChanged(value: Gender) {
50 | savedStateHandle.safe[GenderKey] = value
51 | }
52 |
53 | internal fun navigateToStepThree() =
54 | navigator.navigateTo(RegisterStepThreeScreenRoute)
55 | }
56 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/utils/NonBlankString.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.utils
2 |
3 | /**
4 | * Representation of strings that have at least one character, excluding
5 | * whitespaces.
6 | */
7 | @JvmInline
8 | value class NonBlankString private constructor(private val value: String) : Comparable {
9 | init {
10 | require(value.isNotBlank()) { NotBlankStringException.message }
11 | }
12 |
13 | /**
14 | * Compares this string alphabetically with the [other] one for order.
15 | * Returns zero if this string equals the [other] one, a negative number if
16 | * it's less than the [other] one, or a positive number if it's greater than
17 | * the [other] one.
18 | */
19 | override infix fun compareTo(other: NonBlankString): Int = value.compareTo(other.value)
20 |
21 | /** Returns this string as a [String]. */
22 | override fun toString(): String = value
23 |
24 | fun asString(): String = value
25 |
26 | /** Returns the length of this string. */
27 | val length: Int get() = value.length
28 |
29 | companion object {
30 | /**
31 | * Returns this string as an encapsulated [NonBlankString],
32 | * or returns an encapsulated [IllegalArgumentException] if this string is
33 | * [blank][String.isBlank].
34 | */
35 | fun String.toNonBlankString(): Result =
36 | runCatching { NonBlankString(this) }
37 | }
38 | }
39 |
40 | internal object NotBlankStringException : IllegalArgumentException() {
41 | @Suppress("UnusedPrivateMember")
42 | private fun readResolve(): Any = NotBlankStringException
43 |
44 | override val message: String = "Given string shouldn't be blank."
45 | }
46 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/commonMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/utils/launchNow.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.utils
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.CoroutineStart
6 | import kotlinx.coroutines.Job
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.collect
9 | import kotlinx.coroutines.launch
10 |
11 | /**
12 | * Launch a coroutine immediately to collect the flow.
13 | * It is a shortcut for `scope.launch(CoroutineStart.UNDISPATCHED) { flow.collect() }`.
14 | *
15 | * This differs from [kotlinx.coroutines.flow.launchIn] in that the collection is started immediately
16 | * _in the current thread_
17 | * until the first suspension point, without dispatching to the [CoroutineDispatcher] of the scope context.
18 | * However, when the coroutine is resumed from suspension, it is dispatched to the [CoroutineDispatcher] in its context.
19 | *
20 | * This is useful when collecting a [kotlinx.coroutines.flow.SharedFlow] which does not replay or buffer values,
21 | * and you don't want to miss any values due to the dispatching to the [CoroutineDispatcher].
22 | *
23 | * @see kotlinx.coroutines.flow.launchIn
24 | * @see CoroutineStart.UNDISPATCHED
25 | */
26 | fun Flow.launchNowIn(scope: CoroutineScope): Job =
27 | scope.launch(start = CoroutineStart.UNDISPATCHED) {
28 | collect() // tail-call
29 | }
30 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/desktopMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/common/debugCheckImmediateMainDispatcher.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.common
2 |
3 | actual fun isBuildDebug(): Boolean = true
4 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/composeApp/src/desktopMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/common/identityHashCode.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose.common
2 |
3 | internal actual fun Any?.identityHashCode(): Int = System.identityHashCode(this)
4 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/desktopApp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat
2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
3 |
4 | plugins {
5 | alias(libs.plugins.kotlin.multiplatform)
6 | alias(libs.plugins.jetbrains.compose)
7 | alias(libs.plugins.kotlin.compose)
8 | }
9 |
10 | kotlin {
11 | jvmToolchain {
12 | languageVersion = JavaLanguageVersion.of(libs.versions.java.toolchain.get())
13 | vendor = JvmVendorSpec.AZUL
14 | }
15 |
16 | jvm("desktop") {
17 | compilations.configureEach {
18 | compileTaskProvider.configure {
19 | compilerOptions {
20 | jvmTarget = JvmTarget.fromTarget(libs.versions.java.target.get())
21 | }
22 | }
23 | }
24 | }
25 |
26 | sourceSets {
27 | commonMain.dependencies {
28 | // Compose app
29 | implementation(projects.sample.standaloneComposeMultiplatform.composeApp)
30 |
31 | implementation(compose.runtime)
32 | implementation(compose.foundation)
33 | implementation(compose.material3)
34 | implementation(compose.ui)
35 | implementation(compose.components.resources)
36 | }
37 |
38 | val desktopMain by getting
39 | desktopMain.dependencies {
40 | implementation(compose.desktop.currentOs)
41 |
42 | // Coroutines
43 | implementation(libs.coroutines.swing)
44 | }
45 | }
46 | }
47 |
48 | compose.desktop {
49 | application {
50 | mainClass = "MainKt"
51 |
52 | nativeDistributions {
53 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
54 | packageName = "com.hoc081098.channeleventbus.sample.kmp.compose"
55 | packageVersion = "1.0.0"
56 | }
57 | }
58 | }
59 |
60 | composeCompiler {
61 | featureFlags.addAll(
62 | org.jetbrains.kotlin.compose.compiler.gradle.ComposeFeatureFlag.OptimizeNonSkippingGroups,
63 | )
64 |
65 | val composeCompilerDir = layout.buildDirectory.dir("compose_compiler")
66 | if (project.findProperty("composeCompilerReports") == "true") {
67 | reportsDestination = composeCompilerDir
68 | }
69 | if (project.findProperty("composeCompilerMetrics") == "true") {
70 | metricsDestination = composeCompilerDir
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/sample/standalone-composeMultiplatform/desktopApp/src/desktopMain/kotlin/com/hoc081098/channeleventbus/sample/kmp/compose/main.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.channeleventbus.sample.kmp.compose
2 |
3 | import androidx.compose.desktop.ui.tooling.preview.Preview
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.window.Window
6 | import androidx.compose.ui.window.application
7 | import com.hoc081098.solivagant.lifecycle.LocalLifecycleOwner
8 | import com.hoc081098.solivagant.navigation.ClearOnDispose
9 | import com.hoc081098.solivagant.navigation.ExperimentalSolivagantApi
10 | import com.hoc081098.solivagant.navigation.ProvideCompositionLocals
11 | import com.hoc081098.solivagant.navigation.SavedStateSupport
12 | import com.hoc081098.solivagant.navigation.rememberWindowLifecycleOwner
13 | import org.koin.core.logger.Level
14 |
15 | @OptIn(ExperimentalSolivagantApi::class)
16 | fun main() {
17 | setupNapier()
18 |
19 | startKoinCommon {
20 | printLogger(Level.DEBUG)
21 | }
22 |
23 | val savedStateSupport = SavedStateSupport()
24 | application {
25 | savedStateSupport.ClearOnDispose()
26 |
27 | Window(
28 | onCloseRequest = ::exitApplication,
29 | title = "ChannelEventBus-Multiplatform-Sample",
30 | ) {
31 | val windowLifecycleOwner = rememberWindowLifecycleOwner()!!
32 |
33 | savedStateSupport.ProvideCompositionLocals(LocalLifecycleOwner provides windowLifecycleOwner) {
34 | ChannelEventBusSampleApp()
35 | }
36 | }
37 | }
38 | }
39 |
40 | @Preview
41 | @Composable
42 | private fun AppDesktopPreview() {
43 | ChannelEventBusSampleApp()
44 | }
45 |
--------------------------------------------------------------------------------
/scripts/update_docs_url.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Read input from arguments.
4 | input=$1
5 |
6 | if [[ "$input" == "snapshot" ]]; then
7 | brew install gnu-sed
8 | gsed -i "s/0.x/latest/g" docs/index.md
9 | echo "Updated index.md file with snapshot version."
10 | else
11 | echo "Invalid input: $input"
12 | exit 1
13 | fi
14 |
15 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
2 |
3 | pluginManagement {
4 | repositories {
5 | gradlePluginPortal()
6 | google()
7 | mavenCentral()
8 | }
9 | }
10 |
11 | dependencyResolutionManagement {
12 | repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
13 | repositories {
14 | google()
15 | mavenCentral()
16 | gradlePluginPortal()
17 | maven(url = "https://androidx.dev/storage/compose-compiler/repository/")
18 | }
19 | }
20 |
21 | rootProject.name = "kotlin-channel-event-bus"
22 | include(":channel-event-bus")
23 | include(":sample:standalone-androidApp")
24 | include(
25 | ":sample:standalone-composeMultiplatform:composeApp",
26 | ":sample:standalone-composeMultiplatform:desktopApp",
27 | ":sample:standalone-composeMultiplatform:androidApp",
28 | )
29 |
30 | plugins {
31 | id("org.gradle.toolchains.foojay-resolver-convention") version ("0.8.0")
32 | }
33 |
--------------------------------------------------------------------------------