├── .github
└── workflows
│ └── pull-request-ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── tech
│ └── thdev
│ └── composable
│ └── architecture
│ └── app
│ └── ComposableArchitectureApplication.kt
├── build-logic
├── README.md
├── convention
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ └── kotlin
│ │ ├── AndroidApplicationConventionPlugin.kt
│ │ ├── AndroidLibraryComposeConventionPlugin.kt
│ │ ├── AndroidLibraryConventionPlugin.kt
│ │ ├── AndroidLibraryFeatureComposeApiConventionPlugin.kt
│ │ ├── AndroidLibraryFeatureComposeConventionPlugin.kt
│ │ ├── AndroidLibraryHiltConventionPlugin.kt
│ │ ├── AndroidLibraryNavigationConventionPlugin.kt
│ │ ├── AndroidLibraryUnitTestConventionPlugin.kt
│ │ ├── KotlinLibraryConventionPlugin.kt
│ │ ├── KotlinLibraryHiltConventionPlugin.kt
│ │ ├── KotlinLibraryKspConventionPlugin.kt
│ │ ├── KotlinLibrarySerializationConventionPlugin.kt
│ │ ├── KotlinLibraryVerifyDetektConventionPlugin.kt
│ │ ├── tech.thdev.android.library.publish.gradle.kts
│ │ ├── tech.thdev.kotlin.library.verify.test.gradle.kts
│ │ └── tech
│ │ └── thdev
│ │ └── gradle
│ │ ├── ComposeAndroid.kt
│ │ ├── CoroutineAndroid.kt
│ │ ├── DaggerAndroid.kt
│ │ ├── KotlinAndroid.kt
│ │ ├── KotlinKsp.kt
│ │ ├── VerifyDetekt.kt
│ │ └── extensions
│ │ ├── AppExtension.kt
│ │ ├── ImportExtension.kt
│ │ ├── InternalProjectExtension.kt
│ │ ├── InternalVersionExtension.kt
│ │ └── Publish.kt
├── gradle.properties
└── settings.gradle.kts
├── build.gradle.kts
├── config
└── detekt
│ └── detekt.yml
├── core
└── ui
│ ├── composable-architecture-alert-system
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── tech
│ │ │ └── thdev
│ │ │ └── composable
│ │ │ └── architecture
│ │ │ └── alert
│ │ │ └── system
│ │ │ ├── ActionAlertViewModel.kt
│ │ │ ├── CaAlertAction.kt
│ │ │ ├── CaAlertSideEffect.kt
│ │ │ ├── CaAlertSystem.kt
│ │ │ ├── compose
│ │ │ └── CaDialogScreen.kt
│ │ │ └── model
│ │ │ └── CaAlertUiStateDialogUiState.kt
│ │ └── test
│ │ └── java
│ │ └── tech
│ │ └── thdev
│ │ └── composable
│ │ └── architecture
│ │ └── alert
│ │ └── system
│ │ └── ShowDialogViewModelTest.kt
│ ├── composable-architecture-router-system
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── tech
│ │ │ └── thdev
│ │ │ └── composable
│ │ │ └── architecture
│ │ │ └── router
│ │ │ └── system
│ │ │ ├── LaunchedRouter.kt
│ │ │ ├── Navigator.kt
│ │ │ ├── di
│ │ │ ├── JourneyRouterModule.kt
│ │ │ ├── RouteKey.kt
│ │ │ └── RouterModel.kt
│ │ │ ├── internal
│ │ │ ├── route
│ │ │ │ ├── InternalNavigator.kt
│ │ │ │ ├── InternalNavigatorImpl.kt
│ │ │ │ └── InternalRoute.kt
│ │ │ └── visitor
│ │ │ │ └── InternalActivityRouteMapper.kt
│ │ │ ├── route
│ │ │ ├── ActivityRoute.kt
│ │ │ └── NavigationRoute.kt
│ │ │ └── viewmodel
│ │ │ ├── InternalRouteSideEffect.kt
│ │ │ └── InternalRouteViewModel.kt
│ │ └── test
│ │ └── java
│ │ └── tech
│ │ └── thdev
│ │ └── composable
│ │ └── architecture
│ │ └── router
│ │ └── system
│ │ ├── fake
│ │ ├── FakeActivityRoute.kt
│ │ └── FakeNavigation.kt
│ │ ├── internal
│ │ ├── route
│ │ │ └── InternalNavigatorImplTest.kt
│ │ └── visitor
│ │ │ └── InternalActivityRouteMapperTest.kt
│ │ └── viewmodel
│ │ └── InternalRouteViewModelTest.kt
│ └── composable-architecture-system
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── tech
│ │ └── thdev
│ │ └── composable
│ │ └── architecture
│ │ └── action
│ │ └── system
│ │ ├── Action.kt
│ │ ├── ActionSender.kt
│ │ ├── FlowActionStream.kt
│ │ ├── base
│ │ └── ActionViewModel.kt
│ │ ├── compose
│ │ └── LocalActionSenderOwner.kt
│ │ ├── internal
│ │ ├── InternalActionImpl.kt
│ │ └── di
│ │ │ └── InternalActionModule.kt
│ │ └── lifecycle
│ │ ├── CollectLifecycleEvent.kt
│ │ └── LaunchedLifecycleActionViewModel.kt
│ └── test
│ └── java
│ └── tech
│ └── thdev
│ └── composable
│ └── architecture
│ └── action
│ └── system
│ ├── base
│ └── ActionViewModelTest.kt
│ ├── internal
│ └── InternalActionImplTest.kt
│ └── mock
│ ├── FakeAction.kt
│ └── FakeViewModel.kt
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── sample
├── core
│ └── ui
│ │ └── resource
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── proguard-rules.pro
│ │ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── tech
│ │ │ └── thdev
│ │ │ └── composable
│ │ │ └── architecture
│ │ │ └── sample
│ │ │ └── resource
│ │ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ │ └── res
│ │ ├── drawable
│ │ ├── baseline_info_24.xml
│ │ ├── ic_launcher_background.xml
│ │ └── ic_launcher_foreground.xml
│ │ ├── mipmap-anydpi
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
└── feature
│ ├── detail
│ ├── detail-api
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── proguard-rules.pro
│ │ └── src
│ │ │ └── main
│ │ │ ├── AndroidManifest.xml
│ │ │ └── java
│ │ │ └── tech
│ │ │ └── thdev
│ │ │ └── composable
│ │ │ └── architecture
│ │ │ └── sample
│ │ │ └── feature
│ │ │ └── detail
│ │ │ └── api
│ │ │ ├── DetailActivityRouter.kt
│ │ │ └── model
│ │ │ └── DetailData.kt
│ └── detail
│ │ ├── .gitignore
│ │ ├── build.gradle.kts
│ │ ├── proguard-rules.pro
│ │ └── src
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── tech
│ │ │ └── thdev
│ │ │ └── composable
│ │ │ └── architecture
│ │ │ └── sample
│ │ │ └── feature
│ │ │ └── detail
│ │ │ ├── DetailAction.kt
│ │ │ ├── DetailActivity.kt
│ │ │ ├── DetailActivityRouteImpl.kt
│ │ │ ├── DetailViewModel.kt
│ │ │ ├── compose
│ │ │ └── DetailScreen.kt
│ │ │ ├── di
│ │ │ └── DetailModule.kt
│ │ │ └── model
│ │ │ └── DetailUiState.kt
│ │ └── test
│ │ └── java
│ │ └── tech
│ │ └── thdev
│ │ └── composable
│ │ └── architecture
│ │ └── sample
│ │ └── feature
│ │ └── detail
│ │ └── DetailViewModelTest.kt
│ └── main
│ ├── main-api
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── tech
│ │ └── thdev
│ │ └── composable
│ │ └── architecture
│ │ └── sample
│ │ └── feature
│ │ └── main
│ │ └── api
│ │ └── MainActivityRouter.kt
│ ├── main
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── tech
│ │ └── thdev
│ │ └── composable
│ │ └── architecture
│ │ └── sample
│ │ └── feature
│ │ └── main
│ │ ├── MainActivity.kt
│ │ ├── MainActivityRouteImpl.kt
│ │ └── di
│ │ └── MainModule.kt
│ └── screen
│ ├── screen-navigation
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ │ └── tech
│ │ │ │ └── thdev
│ │ │ │ └── composable
│ │ │ │ └── architecture
│ │ │ │ └── sample
│ │ │ │ └── feature
│ │ │ │ └── main
│ │ │ │ └── screen
│ │ │ │ └── navigation
│ │ │ │ ├── NavigationAction.kt
│ │ │ │ ├── NavigationScreen.kt
│ │ │ │ ├── NavigationViewModel.kt
│ │ │ │ ├── compose
│ │ │ │ └── InternalNavigationScreen.kt
│ │ │ │ └── model
│ │ │ │ └── NavigationUiState.kt
│ │ └── res
│ │ │ └── drawable
│ │ │ ├── img_search.xml
│ │ │ └── img_settings.xml
│ │ └── test
│ │ └── java
│ │ └── tech
│ │ └── thdev
│ │ └── composable
│ │ └── architecture
│ │ └── sample
│ │ └── feature
│ │ └── main
│ │ └── screen
│ │ └── navigation
│ │ └── NavigationViewModelTest.kt
│ ├── screen-search-api
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── tech
│ │ └── thdev
│ │ └── composable
│ │ └── architecture
│ │ └── sample
│ │ └── feature
│ │ └── main
│ │ └── screen
│ │ └── search
│ │ └── api
│ │ └── SearchRoute.kt
│ ├── screen-search
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── tech
│ │ │ └── thdev
│ │ │ └── composable
│ │ │ └── architecture
│ │ │ └── sample
│ │ │ └── feature
│ │ │ └── main
│ │ │ └── screen
│ │ │ └── search
│ │ │ ├── MainAction.kt
│ │ │ ├── SearchScreen.kt
│ │ │ ├── SearchSideEffect.kt
│ │ │ ├── SearchViewModel.kt
│ │ │ └── compose
│ │ │ └── InternalSearchScreen.kt
│ │ └── test
│ │ └── java
│ │ └── tech
│ │ └── thdev
│ │ └── composable
│ │ └── architecture
│ │ └── sample
│ │ └── feature
│ │ └── main
│ │ ├── FlowCacheTestUtil.kt
│ │ └── screen
│ │ └── search
│ │ └── SearchViewModelTest.kt
│ ├── screen-settings-api
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── tech
│ │ └── thdev
│ │ └── composable
│ │ └── architecture
│ │ └── sample
│ │ └── feature
│ │ └── main
│ │ └── screen
│ │ └── setting
│ │ └── api
│ │ └── SettingsRoute.kt
│ └── screen-settings
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ └── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── tech
│ │ │ └── thdev
│ │ │ └── composable
│ │ │ └── architecture
│ │ │ └── sample
│ │ │ └── feature
│ │ │ └── main
│ │ │ └── screen
│ │ │ └── settings
│ │ │ ├── SettingsAction.kt
│ │ │ ├── SettingsScreen.kt
│ │ │ ├── SettingsSideEffect.kt
│ │ │ ├── SettingsViewModel.kt
│ │ │ ├── compose
│ │ │ ├── InternalSettingsScreen.kt
│ │ │ └── component
│ │ │ │ ├── DarkOnOffComponent.kt
│ │ │ │ └── ThemeModeSelectBoxComponent.kt
│ │ │ └── model
│ │ │ └── SettingsUiState.kt
│ └── res
│ │ └── drawable
│ │ ├── img_dark_auto.xml
│ │ ├── img_dark_off.xml
│ │ └── img_dark_on.xml
│ └── test
│ └── java
│ └── tech
│ └── thdev
│ └── composable
│ └── architecture
│ └── sample
│ └── feature
│ └── main
│ └── screen
│ └── settings
│ └── SettingsViewModelTest.kt
├── settings.gradle.kts
└── version.properties
/.github/workflows/pull-request-ci.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: Android Pull Request CI
4 |
5 | on:
6 | pull_request:
7 | branches:
8 | - 'main'
9 | types:
10 | - opened
11 |
12 | jobs:
13 | test:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout the code
18 | uses: actions/checkout@v4
19 |
20 | - name: set up JDK 17
21 | uses: actions/setup-java@v3
22 | with:
23 | distribution: 'corretto'
24 | java-version: '17'
25 |
26 | - name: set up Android SDK
27 | uses: android-actions/setup-android@v3
28 |
29 | - name: Grant execute permission for gradlew
30 | run: chmod +x gradlew
31 |
32 | - name: Clean the project
33 | run: ./gradlew clean
34 |
35 | - name: Test the project
36 | run: ./gradlew test --no-build-cache
37 |
38 | verify:
39 | runs-on: ubuntu-latest
40 |
41 | steps:
42 | - name: Checkout the code
43 | uses: actions/checkout@v4
44 |
45 | - name: set up JDK 17
46 | uses: actions/setup-java@v3
47 | with:
48 | distribution: 'corretto'
49 | java-version: '17'
50 |
51 | - name: set up Android SDK
52 | uses: android-actions/setup-android@v3
53 |
54 | - name: Grant execute permission for gradlew
55 | run: chmod +x gradlew
56 |
57 | - name: Clean the project
58 | run: ./gradlew clean
59 |
60 | - name: Run detekt
61 | run: ./gradlew detekt --no-build-cache
62 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Android template
2 | # Gradle files
3 | .gradle/
4 | build/
5 |
6 | # Local configuration file (sdk path, etc)
7 | local.properties
8 |
9 | # Log/OS Files
10 | *.log
11 |
12 | # Android Studio generated files and folders
13 | captures/
14 | .externalNativeBuild/
15 | .cxx/
16 | *.apk
17 | output.json
18 |
19 | # IntelliJ
20 | *.iml
21 | .idea/
22 | misc.xml
23 | deploymentTargetDropDown.xml
24 | render.experimental.xml
25 |
26 | # Keystore files
27 | *.jks
28 | *.keystore
29 |
30 | # Google Services (e.g. APIs or Firebase)
31 | google-services.json
32 |
33 | # Android Profiling
34 | *.hprof
35 |
36 | .kotlin
37 | .DS_Store
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 thdev.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.application)
3 | alias(libs.plugins.tech.thdev.android.library.hilt)
4 | }
5 |
6 | setNamespace("app")
7 |
8 | android {
9 | val (majorVersion, minorVersion, patchVersion, code) = getVersionInfo()
10 |
11 | defaultConfig {
12 | applicationId = "tech.thdev.imageex"
13 | minSdk = libs.versions.minSdk.get().toInt()
14 | targetSdk = libs.versions.targetSdk.get().toInt()
15 | vectorDrawables.useSupportLibrary = true
16 | versionCode = code
17 | versionName = "$majorVersion.$minorVersion.$patchVersion"
18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
19 | multiDexEnabled = true
20 | }
21 | }
22 |
23 | ksp {
24 | arg("moduleName", project.name)
25 | arg("rootDir", rootDir.absolutePath)
26 | }
27 |
28 | dependencies {
29 | implementation(libs.kotlin.stdlib)
30 |
31 | implementation(libs.androidx.core)
32 |
33 | implementation(libs.androidx.compose.activity)
34 |
35 | rootProject.subprojects.filterProject {
36 | implementation(it)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/java/tech/thdev/composable/architecture/app/ComposableArchitectureApplication.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.app
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class ComposableArchitectureApplication : Application()
8 |
--------------------------------------------------------------------------------
/build-logic/README.md:
--------------------------------------------------------------------------------
1 | # Convention Plugins
2 |
3 | The `build-logic` folder defines project-specific convention plugins, used to keep a single
4 | source of truth for common module configurations.
5 |
6 | This approach is heavily based on
7 | [https://developer.squareup.com/blog/herding-elephants/](https://developer.squareup.com/blog/herding-elephants/)
8 | and
9 | [https://github.com/jjohannes/idiomatic-gradle](https://github.com/jjohannes/idiomatic-gradle).
10 |
11 | By setting up convention plugins in `build-logic`, we can avoid duplicated build script setup,
12 | messy `subproject` configurations, without the pitfalls of the `buildSrc` directory.
13 |
14 | `build-logic` is an included build, as configured in the root
15 | [`settings.gradle.kts`](../settings.gradle.kts).
16 |
17 | Inside `build-logic` is a `convention` module, which defines a set of plugins that all normal
18 | modules can use to configure themselves.
19 |
20 | `build-logic` also includes a set of `Kotlin` files used to share logic between plugins themselves,
21 | which is most useful for configuring Android components (libraries vs applications) with shared
22 | code.
23 |
24 | These plugins are *additive* and *composable*, and try to only accomplish a single responsibility.
25 | Modules can then pick and choose the configurations they need.
26 | If there is one-off logic for a module without shared code, it's preferable to define that directly
27 | in the module's `build.gradle`, as opposed to creating a convention plugin with module-specific
28 | setup.
29 |
30 | Current list of convention plugins:
31 |
32 | - [`nowinandroid.android.application`](convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt),
33 | [`nowinandroid.android.library`](convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt),
34 | [`nowinandroid.android.test`](convention/src/main/kotlin/AndroidTestConventionPlugin.kt):
35 | Configures common Android and Kotlin options.
36 | - [`nowinandroid.android.application.compose`](convention/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt),
37 | [`nowinandroid.android.library.compose`](convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt):
38 | Configures Jetpack Compose options
39 |
--------------------------------------------------------------------------------
/build-logic/convention/.gitignore:
--------------------------------------------------------------------------------
1 | /bin
--------------------------------------------------------------------------------
/build-logic/convention/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
18 |
19 | plugins {
20 | `kotlin-dsl`
21 | `kotlin-dsl-precompiled-script-plugins`
22 | }
23 |
24 | group = "tech.thdev.composable.architecture.buildlogic"
25 |
26 | // Configure the build-logic plugins to target JDK 17
27 | // This matches the JDK used to build the project, and is not related to what is running on device.
28 | java {
29 | sourceCompatibility = JavaVersion.VERSION_17
30 | targetCompatibility = JavaVersion.VERSION_17
31 | }
32 |
33 | kotlin {
34 | compilerOptions {
35 | jvmTarget = JvmTarget.JVM_17
36 | }
37 | }
38 |
39 | dependencies {
40 | compileOnly(libs.plugin.android.gradlePlugin)
41 | compileOnly(libs.plugin.compose.gradlePlugin)
42 | compileOnly(libs.plugin.kotlin.gradlePlugin)
43 | compileOnly(libs.plugin.kotlin.serializationPlugin)
44 | compileOnly(libs.plugin.ksp.gradlePlugin)
45 | compileOnly(libs.plugin.verify.detektPlugin)
46 | }
47 |
48 | tasks {
49 | validatePlugins {
50 | enableStricterValidation = true
51 | failOnWarning = true
52 | }
53 | }
54 |
55 | gradlePlugin {
56 | plugins {
57 | /**
58 | * Application 관련
59 | */
60 | register("androidApplication") {
61 | id = "tech.thdev.android.application"
62 | implementationClass = "AndroidApplicationConventionPlugin"
63 | }
64 |
65 | /**
66 | * Library 관련
67 | */
68 | register("androidLibrary") {
69 | id = "tech.thdev.android.library"
70 | implementationClass = "AndroidLibraryConventionPlugin"
71 | }
72 | register("androidLibraryFeatureComposeApi") {
73 | id = "tech.thdev.android.library.feature.compose.api"
74 | implementationClass = "AndroidLibraryFeatureComposeApiConventionPlugin"
75 | }
76 | register("androidLibraryFeatureCompose") {
77 | id = "tech.thdev.android.library.feature.compose"
78 | implementationClass = "AndroidLibraryFeatureComposeConventionPlugin"
79 | }
80 | register("androidLibraryCompose") {
81 | id = "tech.thdev.android.library.compose"
82 | implementationClass = "AndroidLibraryComposeConventionPlugin"
83 | }
84 | register("androidLibaryHilt") {
85 | id = "tech.thdev.android.library.hilt"
86 | implementationClass = "AndroidLibraryHiltConventionPlugin"
87 | }
88 | register("androidLibraryNavigation") {
89 | id = "tech.thdev.android.library.navigation"
90 | implementationClass = "AndroidLibraryNavigationConventionPlugin"
91 | }
92 | register("androidLibraryUnitTest") {
93 | id = "tech.thdev.android.library.unit.test"
94 | implementationClass = "AndroidLibraryUnitTestConventionPlugin"
95 | }
96 |
97 | /**
98 | * Kotlin 라이브러리
99 | */
100 | register("kotlinLibrary") {
101 | id = "tech.thdev.kotlin.library"
102 | implementationClass = "KotlinLibraryConventionPlugin"
103 | }
104 | register("kotlinLibraryKsp") {
105 | id = "tech.thdev.kotlin.library.ksp"
106 | implementationClass = "KotlinLibraryKspConventionPlugin"
107 | }
108 | register("kotlinLibraryHilt") {
109 | id = "tech.thdev.kotlin.library.hilt"
110 | implementationClass = "KotlinLibraryHiltConventionPlugin"
111 | }
112 | register("kotlinLibrarySerialization") {
113 | id = "tech.thdev.kotlin.library.serialization"
114 | implementationClass = "KotlinLibrarySerializationConventionPlugin"
115 | }
116 | register("kotlinLibraryVerifyDetekt") {
117 | id = "tech.thdev.kotlin.library.verify.detekt"
118 | implementationClass = "KotlinLibraryVerifyDetektConventionPlugin"
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import tech.thdev.gradle.configureComposeAndroid
4 | import tech.thdev.gradle.configureComposeFeature
5 | import tech.thdev.gradle.configureKotlinAndroid
6 |
7 | class AndroidApplicationConventionPlugin : Plugin {
8 |
9 | override fun apply(target: Project) {
10 | with(target) {
11 | with(pluginManager) {
12 | apply("com.android.application")
13 | apply("org.jetbrains.kotlin.android")
14 | apply("org.jetbrains.kotlin.plugin.serialization")
15 |
16 | apply("tech.thdev.android.library.navigation")
17 | apply("tech.thdev.android.library.unit.test")
18 |
19 | apply("tech.thdev.kotlin.library.verify.detekt")
20 | apply("tech.thdev.kotlin.library.verify.test")
21 | }
22 |
23 | configureKotlinAndroid()
24 | configureComposeFeature()
25 | configureComposeAndroid()
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import tech.thdev.gradle.configureComposeAndroid
4 | import tech.thdev.gradle.configureComposeFeature
5 | import tech.thdev.gradle.configureCoroutineAndroid
6 |
7 | class AndroidLibraryComposeConventionPlugin : Plugin {
8 |
9 | override fun apply(target: Project) {
10 | with(target) {
11 | with(pluginManager) {
12 | apply("tech.thdev.android.library")
13 | apply("tech.thdev.android.library.compose")
14 | apply("tech.thdev.android.library.unit.test")
15 | }
16 |
17 | configureComposeFeature()
18 | configureComposeAndroid()
19 | configureCoroutineAndroid()
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import org.gradle.kotlin.dsl.dependencies
4 | import tech.thdev.gradle.configureKotlinAndroid
5 | import tech.thdev.gradle.extensions.findLibrary
6 |
7 | class AndroidLibraryConventionPlugin : Plugin {
8 |
9 | override fun apply(target: Project) {
10 | with(target) {
11 | with(pluginManager) {
12 | apply("com.android.library")
13 | apply("org.jetbrains.kotlin.android")
14 |
15 | apply("tech.thdev.android.library.unit.test")
16 |
17 | apply("tech.thdev.kotlin.library.verify.detekt")
18 | apply("tech.thdev.kotlin.library.verify.test")
19 | }
20 |
21 | configureKotlinAndroid()
22 |
23 | dependencies {
24 | implementation(findLibrary("coroutines-core"))
25 | implementation(findLibrary("androidx-annotation"))
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/AndroidLibraryFeatureComposeApiConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import org.gradle.kotlin.dsl.dependencies
4 |
5 | class AndroidLibraryFeatureComposeApiConventionPlugin : Plugin {
6 |
7 | override fun apply(target: Project) {
8 | with(target) {
9 | with(pluginManager) {
10 | apply("tech.thdev.android.library")
11 | apply("tech.thdev.kotlin.library.serialization")
12 | apply("kotlin-parcelize")
13 | }
14 |
15 | dependencies {
16 | implementation(project(":core:ui:composable-architecture-router-system"))
17 | }
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/AndroidLibraryFeatureComposeConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import org.gradle.kotlin.dsl.dependencies
4 |
5 | class AndroidLibraryFeatureComposeConventionPlugin : Plugin {
6 |
7 | override fun apply(target: Project) {
8 | with(target) {
9 | with(pluginManager) {
10 | apply("tech.thdev.android.library.compose")
11 | apply("tech.thdev.android.library.hilt")
12 | apply("tech.thdev.android.library.navigation")
13 | }
14 |
15 | dependencies {
16 | implementation(project(":core:ui:composable-architecture-system"))
17 | implementation(project(":core:ui:composable-architecture-alert-system"))
18 | implementation(project(":core:ui:composable-architecture-router-system"))
19 |
20 | implementation(project(":sample:core:ui:resource"))
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/AndroidLibraryHiltConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import tech.thdev.gradle.configureDaggerHilt
4 |
5 | class AndroidLibraryHiltConventionPlugin : Plugin {
6 |
7 | override fun apply(target: Project) {
8 | with(target) {
9 | with(pluginManager) {
10 | apply("tech.thdev.kotlin.library.ksp")
11 | apply("com.google.dagger.hilt.android")
12 | }
13 |
14 | configureDaggerHilt()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/AndroidLibraryNavigationConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import org.gradle.kotlin.dsl.dependencies
4 | import tech.thdev.gradle.configureDaggerHilt
5 | import tech.thdev.gradle.extensions.findLibrary
6 |
7 | class AndroidLibraryNavigationConventionPlugin : Plugin {
8 |
9 | override fun apply(target: Project) {
10 | with(target) {
11 | dependencies {
12 | implementation(findLibrary("androidx-compose-navigation"))
13 | implementation(findLibrary("androidx-compose-navigation-hilt"))
14 | }
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/AndroidLibraryUnitTestConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import org.gradle.kotlin.dsl.dependencies
4 | import tech.thdev.gradle.extensions.findLibrary
5 |
6 | class AndroidLibraryUnitTestConventionPlugin : Plugin {
7 |
8 | override fun apply(target: Project) {
9 | with(target) {
10 | dependencies {
11 | implementation(findLibrary("kotlin-stdlib"))
12 |
13 | implementation(findLibrary("coroutines-core"))
14 | testImplementation(findLibrary("test-coroutines"))
15 | testImplementation(findLibrary("test-coroutines-turbine"))
16 |
17 | testImplementation(findLibrary("test-mockito"))
18 | testImplementation(findLibrary("test-mockito-kotlin"))
19 |
20 | testImplementation(findLibrary("test-androidx-core"))
21 | testImplementation(findLibrary("test-androidx-runner"))
22 | testImplementation(findLibrary("test-androidx-junit"))
23 | testImplementation(findLibrary("test-robolectric")) // hilt 사용을 위해 추가
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/KotlinLibraryConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import tech.thdev.gradle.configureKotlinJvm
4 |
5 | class KotlinLibraryConventionPlugin : Plugin {
6 |
7 | override fun apply(target: Project) {
8 | with(target) {
9 | with(pluginManager) {
10 | apply("org.jetbrains.kotlin.jvm")
11 |
12 | apply("tech.thdev.kotlin.library.unit.test")
13 | apply("tech.thdev.kotlin.library.verify.detekt")
14 | apply("tech.thdev.kotlin.library.verify.test")
15 | }
16 |
17 | configureKotlinJvm()
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/KotlinLibraryHiltConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import tech.thdev.gradle.configureDaggerKotlin
4 |
5 | class KotlinLibraryHiltConventionPlugin : Plugin {
6 |
7 | override fun apply(target: Project) {
8 | with(target) {
9 | with(pluginManager) {
10 | apply("tech.thdev.kotlin.library.ksp")
11 | }
12 |
13 | configureDaggerKotlin()
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/KotlinLibraryKspConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import com.google.devtools.ksp.gradle.KspExtension
2 | import org.gradle.api.Plugin
3 | import org.gradle.api.Project
4 | import org.gradle.kotlin.dsl.configure
5 | import tech.thdev.gradle.configureKspSourceSets
6 |
7 | class KotlinLibraryKspConventionPlugin : Plugin {
8 |
9 | override fun apply(target: Project) {
10 | with(target) {
11 | with(pluginManager) {
12 | apply("com.google.devtools.ksp")
13 | }
14 |
15 | configureKspSourceSets()
16 |
17 | extensions.configure {
18 | arg("moduleName", project.name)
19 | arg("rootDir", rootDir.absolutePath)
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/KotlinLibrarySerializationConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import org.gradle.kotlin.dsl.dependencies
4 | import tech.thdev.gradle.extensions.findLibrary
5 |
6 | class KotlinLibrarySerializationConventionPlugin : Plugin {
7 |
8 | override fun apply(target: Project) {
9 | with(target) {
10 | with(pluginManager) {
11 | apply("org.jetbrains.kotlin.plugin.serialization")
12 | }
13 |
14 | dependencies {
15 | implementation(findLibrary("kotlin-serializationJson"))
16 | }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/KotlinLibraryVerifyDetektConventionPlugin.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Plugin
2 | import org.gradle.api.Project
3 | import org.gradle.kotlin.dsl.dependencies
4 | import tech.thdev.gradle.configureVerifyDetekt
5 | import tech.thdev.gradle.extensions.findLibrary
6 |
7 | class KotlinLibraryVerifyDetektConventionPlugin : Plugin {
8 |
9 | override fun apply(target: Project) {
10 | with(target) {
11 | with(pluginManager) {
12 | apply("io.gitlab.arturbosch.detekt")
13 | }
14 |
15 | configureVerifyDetekt()
16 |
17 | dependencies {
18 | "detektPlugins"(findLibrary("verify-detektFormatting"))
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech.thdev.android.library.publish.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.android.build.gradle.api.AndroidSourceSet
2 | import org.gradle.kotlin.dsl.getByName
3 | import org.jetbrains.kotlin.konan.properties.Properties
4 | import tech.thdev.gradle.extensions.androidExtension
5 |
6 | plugins {
7 | `maven-publish`
8 | signing
9 | }
10 |
11 | // Stub secrets to let the project sync and build without the publication values set up
12 | ext["signing.keyId"] = ""
13 | ext["signing.password"] = ""
14 | ext["signing.key"] = ""
15 | ext["ossrhUsername"] = ""
16 | ext["ossrhPassword"] = ""
17 |
18 | val javadocJar by tasks.registering(Jar::class) {
19 | archiveClassifier.set("javadoc")
20 | }
21 |
22 | val androidSourceJar by tasks.registering(Jar::class) {
23 | archiveClassifier.set("sources")
24 | from(androidExtension.sourceSets.getByName("main").java.srcDirs)
25 | }
26 |
27 | fun getExtraString(name: String) =
28 | ext[name]?.toString()
29 |
30 | fun groupId(): String = "tech.thdev"
31 |
32 | afterEvaluate {
33 | // Grabbing secrets from local.properties file or from environment variables, which could be used on CI
34 | val secretPropsFile = project.rootProject.file("local.properties")
35 | if (secretPropsFile.exists()) {
36 | secretPropsFile.reader().use {
37 | Properties().apply {
38 | load(it)
39 | }
40 | }.onEach { (name, value) ->
41 | ext[name.toString()] = value
42 | }
43 | } else {
44 | // Use system environment variables
45 | ext["ossrhUsername"] = System.getenv("OSSRH_USERNAME")
46 | ext["ossrhPassword"] = System.getenv("OSSRH_PASSWORD")
47 | ext["signing.keyId"] = System.getenv("SIGNING_KEY_ID")
48 | ext["signing.password"] = System.getenv("SIGNING_PASSWORD")
49 | ext["signing.key"] = System.getenv("SIGNING_KEY")
50 | }
51 |
52 | // Set up Sonatype repository
53 | publishing {
54 | val artifactName = getExtraString("libraryName") ?: name
55 | val libraryVersion = getExtraString("libraryVersion") ?: "DEV"
56 | val artifactDescription = getExtraString("description") ?: ""
57 | val artifactUrl: String = getExtraString("url") ?: "http://thdev.tech/"
58 |
59 | println("artifactName $artifactName")
60 | println("libraryVersion $libraryVersion")
61 | println("artifactDescription $artifactDescription")
62 | println("artifactUrl $artifactUrl")
63 |
64 | // Configure maven central repository
65 | repositories {
66 | maven {
67 | name = "sonatype"
68 | setUrl("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/")
69 | credentials {
70 | username = getExtraString("ossrhUsername")
71 | password = getExtraString("ossrhPassword")
72 | }
73 | }
74 | }
75 |
76 | // Configure all publications
77 | publications {
78 | create("release") {
79 | groupId = groupId()
80 | artifactId = artifactName
81 | version = libraryVersion
82 |
83 | if (project.plugins.hasPlugin("com.android.library")) {
84 | from(components.getByName("release"))
85 | } else {
86 | from(components.getByName("java"))
87 | }
88 |
89 | // Stub android
90 | artifact(androidSourceJar.get())
91 | // Stub javadoc.jar artifact
92 | artifact(javadocJar.get())
93 |
94 | // Provide artifacts information requited by Maven Central
95 | pom {
96 | name.set(artifactName)
97 | description.set(artifactDescription)
98 | url.set(artifactUrl)
99 |
100 | licenses {
101 | license {
102 | name.set("The Apache License, Version 2.0")
103 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt")
104 | }
105 | }
106 | developers {
107 | developer {
108 | id.set("taehwandev")
109 | name.set("taehwan")
110 | email.set("develop@thdev.tech")
111 | }
112 | }
113 | scm {
114 | url.set(artifactUrl)
115 | }
116 | }
117 | }
118 | }
119 | }
120 |
121 | // Signing artifacts. Signing.* extra properties values will be used
122 | signing {
123 | useInMemoryPgpKeys(
124 | getExtraString("signing.keyId"),
125 | getExtraString("signing.key"),
126 | getExtraString("signing.password"),
127 | )
128 | sign(publishing.publications)
129 | }
130 | }
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech.thdev.kotlin.library.verify.test.gradle.kts:
--------------------------------------------------------------------------------
1 | afterEvaluate {
2 | val tests = mutableListOf()
3 | val lints = mutableListOf()
4 | if (plugins.hasPlugin("java-library") || plugins.hasPlugin("java") || plugins.hasPlugin("kotlin")) {
5 | // test
6 | tests.add("test")
7 | } else {
8 | lints.add("testDebugUnitTest")
9 | lints.add("lintDebug")
10 | }
11 |
12 | tasks.register("testAll") {
13 | dependsOn(*tests.toTypedArray())
14 | }
15 | tasks.register("lintAll") {
16 | if (lints.isNotEmpty()) {
17 | dependsOn(*lints.toTypedArray())
18 | } else {
19 | logger.log(LogLevel.DEBUG, "${project.name} has no lint")
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/ComposeAndroid.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.gradle
2 |
3 | import implementation
4 | import org.gradle.api.Project
5 | import org.gradle.kotlin.dsl.dependencies
6 | import tech.thdev.gradle.extensions.androidExtension
7 | import tech.thdev.gradle.extensions.findLibrary
8 |
9 | fun Project.configureComposeAndroid() {
10 | androidExtension.apply {
11 | dependencies {
12 | implementation(findLibrary("kotlin-collectionsImmutable"))
13 |
14 | implementation(platform(findLibrary("androidx-compose-bom")))
15 | implementation(findLibrary("androidx-compose-ui"))
16 | implementation(findLibrary("androidx-compose-foundation"))
17 | implementation(findLibrary("androidx-compose-material3"))
18 | implementation(findLibrary("androidx-compose-runtime"))
19 | implementation(findLibrary("androidx-compose-ui-tooling-preview"))
20 | implementation(findLibrary("androidx-lifecycleRuntime"))
21 | implementation(findLibrary("androidx-compose-ui-graphics"))
22 |
23 | implementation(findLibrary("androidx-compose-constraintLayout"))
24 |
25 | "debugRuntimeOnly"(findLibrary("androidx-compose-ui-tooling"))
26 | }
27 | }
28 | }
29 |
30 | /**
31 | * Compose Library
32 | */
33 | fun Project.configureComposeFeature() {
34 | androidExtension.apply {
35 | with(plugins) {
36 | apply("org.jetbrains.kotlin.plugin.compose")
37 | }
38 |
39 | buildFeatures {
40 | compose = true
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/CoroutineAndroid.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.gradle
2 |
3 | import implementation
4 | import org.gradle.api.Project
5 | import org.gradle.kotlin.dsl.dependencies
6 | import tech.thdev.gradle.extensions.findLibrary
7 |
8 | internal fun Project.configureCoroutineAndroid() {
9 | dependencies {
10 | implementation(findLibrary("coroutines-android"))
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/DaggerAndroid.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.gradle
2 |
3 | import implementation
4 | import org.gradle.api.Project
5 | import org.gradle.kotlin.dsl.dependencies
6 | import tech.thdev.gradle.extensions.findLibrary
7 |
8 | /**
9 | * Dagger Android 적용 시
10 | */
11 | internal fun Project.configureDaggerHilt() {
12 | configureDaggerKotlin()
13 |
14 | dependencies {
15 | implementation(findLibrary("dagger-hilt-android"))
16 | "ksp"(findLibrary("dagger-hilt-android-compiler"))
17 | }
18 | }
19 |
20 | internal fun Project.configureDaggerKotlin() {
21 | dependencies {
22 | implementation(findLibrary("dagger-hilt-core"))
23 | "ksp"(findLibrary("dagger-hilt-android-compiler"))
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/KotlinKsp.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.gradle
2 |
3 | import kspSourceSet
4 | import org.gradle.api.Project
5 | import tech.thdev.gradle.extensions.androidExtension
6 |
7 | /**
8 | * KSP Source sets
9 | * 모듈 용
10 | */
11 | internal fun Project.configureKspSourceSets() {
12 | androidExtension.apply {
13 | sourceSets.getByName("debug") {
14 | java.srcDir("debug".kspSourceSet)
15 | }
16 | sourceSets.getByName("release") {
17 | java.srcDir("release".kspSourceSet)
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/VerifyDetekt.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.gradle
2 |
3 | import org.gradle.api.Project
4 | import org.gradle.kotlin.dsl.withType
5 |
6 | internal fun Project.configureVerifyDetekt() {
7 | tasks.withType().configureEach {
8 | // Target version of the generated JVM bytecode. It is used for type resolution.
9 | jvmTarget = "17"
10 |
11 | buildUponDefaultConfig = true // preconfigure defaults
12 | allRules = false // activate all available (even unstable) rules.
13 | parallel = true
14 | config.setFrom(listOf(file("$rootDir/config/detekt/detekt.yml"))) // point to your custom config defining rules to run, overwriting default behavior
15 |
16 | reports {
17 | file("$rootDir/build/reports/test/${project.name}/").mkdirs()
18 | html.required.set(true) // observe findings in your browser with structure and code snippets
19 | html.outputLocation.set(file("$rootDir/build/reports/detekt/${project.name}.html"))
20 | xml.required.set(true) // checkstyle like format mainly for integrations like Jenkins
21 | xml.outputLocation.set(file("$rootDir/build/reports/detekt/${project.name}.xml"))
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/extensions/AppExtension.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("PackageDirectoryMismatch")
2 |
3 | import org.gradle.api.Project
4 | import tech.thdev.gradle.extensions.androidExtension
5 |
6 | fun Project.setNamespace(name: String) {
7 | androidExtension.apply {
8 | namespace = "tech.thdev.composable.architecture.$name"
9 | }
10 | }
11 |
12 | val String.kspSourceSet: String
13 | get() = "build/generated/ksp/$this/kotlin"
14 |
15 | fun Set.filterProject(
16 | body: (target: Project) -> Unit,
17 | ) {
18 | forEach { project ->
19 | if (project.name != "app" && project.buildFile.isFile) {
20 | body(project)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/extensions/ImportExtension.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("PackageDirectoryMismatch")
2 |
3 | import org.gradle.kotlin.dsl.DependencyHandlerScope
4 |
5 | internal fun DependencyHandlerScope.api(dependency: Any) {
6 | add("api", dependency)
7 | }
8 |
9 | internal fun DependencyHandlerScope.compileOnly(dependency: Any) {
10 | add("compileOnly", dependency)
11 | }
12 |
13 | internal fun DependencyHandlerScope.implementation(dependency: Any) {
14 | add("implementation", dependency)
15 | }
16 |
17 | internal fun DependencyHandlerScope.testImplementation(dependency: Any) {
18 | add("testImplementation", dependency)
19 | }
20 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/extensions/InternalProjectExtension.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.gradle.extensions
2 |
3 | import com.android.build.api.dsl.ApplicationExtension
4 | import com.android.build.api.dsl.CommonExtension
5 | import com.android.build.gradle.LibraryExtension
6 | import org.gradle.api.Project
7 | import org.gradle.api.artifacts.MinimalExternalModuleDependency
8 | import org.gradle.api.artifacts.VersionCatalog
9 | import org.gradle.api.artifacts.VersionCatalogsExtension
10 | import org.gradle.api.plugins.ExtensionContainer
11 | import org.gradle.api.provider.Provider
12 | import org.gradle.kotlin.dsl.getByType
13 |
14 | internal val Project.applicationExtension: CommonExtension<*, *, *, *, *, *>
15 | get() = extensions.getByType()
16 |
17 | internal val Project.libraryExtension: CommonExtension<*, *, *, *, *, *>
18 | get() = extensions.getByType()
19 |
20 | internal val Project.androidExtension: CommonExtension<*, *, *, *, *, *>
21 | get() = runCatching { libraryExtension }
22 | .recoverCatching { applicationExtension }
23 | .onFailure { println("Could not find Library or Application extension from this project") }
24 | .getOrThrow()
25 |
26 | internal val ExtensionContainer.libs: VersionCatalog
27 | get() = getByType().named("libs")
28 |
29 | internal fun Project.findLibrary(name: String): Provider =
30 | extensions.libs.findLibrary(name).get()
31 |
32 | internal fun Project.findVersion(name: String): String =
33 | extensions.libs.findVersion(name).get().requiredVersion
34 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/extensions/InternalVersionExtension.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("PackageDirectoryMismatch")
2 |
3 | import org.gradle.api.GradleException
4 | import org.gradle.api.Project
5 | import java.util.Properties
6 |
7 | fun Project.getVersionInfo(): VersionInfo {
8 | val versionPropsFile = file("${rootDir.absolutePath}/version.properties")
9 |
10 | val majorVersion: String
11 | val minorVersion: String
12 | val patchVersion: String
13 | val versionCode: Int
14 |
15 | if (versionPropsFile.exists()) {
16 | val versionProps = Properties()
17 | versionProps.load(versionPropsFile.reader())
18 |
19 | versionCode = versionProps["versionCode"].toString().toInt()
20 | majorVersion = versionProps["majorVersion"].toString()
21 | minorVersion = versionProps["minorVersion"].toString()
22 | patchVersion = versionProps["patchVersion"].toString()
23 | return VersionInfo(
24 | majorVersion = majorVersion,
25 | minorVersion = minorVersion,
26 | patchVersion = patchVersion,
27 | versionCode = versionCode,
28 | )
29 | } else {
30 | throw GradleException("version.properties 파일을 찾을 수 없습니다.")
31 | }
32 | }
33 |
34 | data class VersionInfo(
35 | val majorVersion: String,
36 | val minorVersion: String,
37 | val patchVersion: String,
38 | val versionCode: Int,
39 | )
40 |
--------------------------------------------------------------------------------
/build-logic/convention/src/main/kotlin/tech/thdev/gradle/extensions/Publish.kt:
--------------------------------------------------------------------------------
1 | object Publish {
2 |
3 | const val DESCRIPTION = "Android Composable Architecture"
4 |
5 | const val PUBLISH_URL = "https://thdev.tech/TComposableArchitecture/"
6 | }
7 |
--------------------------------------------------------------------------------
/build-logic/gradle.properties:
--------------------------------------------------------------------------------
1 | # Gradle properties are not passed to included builds https://github.com/gradle/gradle/issues/2534
2 | org.gradle.parallel=true
3 | org.gradle.caching=true
4 | org.gradle.configureondemand=true
--------------------------------------------------------------------------------
/build-logic/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | dependencyResolutionManagement {
18 | repositories {
19 | google()
20 | mavenCentral()
21 | }
22 | versionCatalogs {
23 | create("libs") {
24 | from(files("../gradle/libs.versions.toml"))
25 | }
26 | }
27 | }
28 |
29 | rootProject.name = "build-logic"
30 | include(":convention")
31 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.android.library) apply false
5 | alias(libs.plugins.android.test) apply false
6 | alias(libs.plugins.compose.compiler) apply false
7 | alias(libs.plugins.kotlin.jvm) apply false
8 | alias(libs.plugins.kotlin.serialization) apply false
9 | alias(libs.plugins.ksp) apply false
10 | alias(libs.plugins.detekt) apply false
11 | alias(libs.plugins.android.dagger.hilt) apply false
12 | }
13 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-alert-system/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/ui/composable-architecture-alert-system/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import tech.thdev.gradle.configureComposeFeature
2 |
3 | plugins {
4 | alias(libs.plugins.tech.thdev.android.library)
5 | alias(libs.plugins.tech.thdev.android.library.hilt)
6 | alias(libs.plugins.tech.thdev.android.library.publish)
7 | }
8 |
9 | setNamespace("alert.system")
10 |
11 | val (majorVersion, minorVersion, patchVersion, code) = getVersionInfo()
12 |
13 | ext["libraryName"] = "composable-architecture-alert-system"
14 | ext["libraryVersion"] = "$majorVersion.$minorVersion.$patchVersion"
15 | ext["description"] = Publish.DESCRIPTION
16 | ext["url"] = Publish.PUBLISH_URL
17 |
18 | configureComposeFeature()
19 |
20 | android {
21 | buildTypes {
22 | getByName("debug") {
23 | isMinifyEnabled = false
24 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
25 | }
26 |
27 | getByName("release") {
28 | isMinifyEnabled = false
29 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
30 | }
31 | }
32 |
33 | // AGP 8.0
34 | publishing {
35 | multipleVariants("release") {
36 | allVariants()
37 | }
38 | }
39 | }
40 |
41 | dependencies {
42 | implementation(platform(libs.androidx.compose.bom))
43 | implementation(libs.androidx.compose.foundation)
44 | implementation(libs.androidx.compose.material3)
45 | implementation(libs.androidx.compose.ui.tooling.preview)
46 |
47 | implementation(libs.androidx.compose.lifecycle.viewModel)
48 |
49 | implementation(projects.core.ui.composableArchitectureSystem)
50 | }
51 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-alert-system/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/core/ui/composable-architecture-alert-system/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-alert-system/src/main/java/tech/thdev/composable/architecture/alert/system/ActionAlertViewModel.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.alert.system
2 |
3 | import androidx.compose.material3.SnackbarDuration
4 | import dagger.hilt.android.lifecycle.HiltViewModel
5 | import kotlinx.coroutines.channels.Channel
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.asStateFlow
8 | import kotlinx.coroutines.flow.receiveAsFlow
9 | import tech.thdev.composable.architecture.action.system.FlowActionStream
10 | import tech.thdev.composable.architecture.action.system.base.ActionViewModel
11 | import tech.thdev.composable.architecture.alert.system.model.CaAlertUiStateDialogUiState
12 | import javax.inject.Inject
13 |
14 | @HiltViewModel
15 | class ActionAlertViewModel @Inject constructor(
16 | flowActionStream: FlowActionStream,
17 | ) : ActionViewModel(
18 | flowActionStream = flowActionStream,
19 | actionClass = CaAlertAction::class,
20 | ) {
21 |
22 | private val _alertUiStateDialogUiState = MutableStateFlow(CaAlertUiStateDialogUiState.Default)
23 | val alertUiStateDialogUiState = _alertUiStateDialogUiState.asStateFlow()
24 |
25 | private val _sideEffect = Channel(Channel.BUFFERED)
26 | internal val sideEffect = _sideEffect.receiveAsFlow()
27 |
28 | override suspend fun handleAction(action: CaAlertAction) {
29 | when (action) {
30 | is CaAlertAction.ShowDialog -> {
31 | val dialogItem = CaAlertUiStateDialogUiState(
32 | title = action.title,
33 | message = action.message,
34 | confirmButtonText = action.confirmButtonText,
35 | onConfirmButtonAction = action.onConfirmButtonAction,
36 | dismissButtonText = action.dismissButtonText,
37 | onDismissButtonAction = action.onDismissButtonAction,
38 | onDismissRequest = action.onDismissRequest,
39 | icon = action.icon,
40 | dismissOnBackPress = action.dismissOnBackPress,
41 | dismissOnClickOutside = action.dismissOnClickOutside,
42 | )
43 | _alertUiStateDialogUiState.value = dialogItem
44 | _sideEffect.send(CaAlertSideEffect.ShowDialog)
45 | }
46 |
47 | is CaAlertAction.HideDialog -> {
48 | _alertUiStateDialogUiState.value = CaAlertUiStateDialogUiState.Default
49 | _sideEffect.send(CaAlertSideEffect.HideDialog)
50 | }
51 |
52 | is CaAlertAction.ShowSnack -> {
53 | val snackItem = CaAlertSideEffect.ShowSnack(
54 | message = action.message,
55 | actionLabel = action.actionLabel,
56 | onAction = action.onAction,
57 | onDismiss = action.onDismiss,
58 | duration = action.duration.convert(),
59 | )
60 | _sideEffect.send(snackItem)
61 | }
62 |
63 | is CaAlertAction.None -> {}
64 | }
65 | }
66 |
67 | private fun CaAlertAction.ShowSnack.Duration.convert(): SnackbarDuration =
68 | when (this) {
69 | CaAlertAction.ShowSnack.Duration.Short -> SnackbarDuration.Short
70 | CaAlertAction.ShowSnack.Duration.Indefinite -> SnackbarDuration.Indefinite
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-alert-system/src/main/java/tech/thdev/composable/architecture/alert/system/CaAlertAction.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.alert.system
2 |
3 | import android.view.View
4 | import androidx.annotation.DrawableRes
5 | import tech.thdev.composable.architecture.action.system.Action
6 |
7 | sealed interface CaAlertAction : Action {
8 |
9 | data object None : CaAlertAction
10 |
11 | data object HideDialog : CaAlertAction
12 |
13 | data class ShowDialog(
14 | val title: String = "",
15 | val message: String = "",
16 | val confirmButtonText: String = "",
17 | val onConfirmButtonAction: Action = None,
18 | val dismissButtonText: String = "",
19 | val onDismissButtonAction: Action = None,
20 | val onDismissRequest: Action = None,
21 | @DrawableRes val icon: Int = View.NO_ID,
22 | val dismissOnBackPress: Boolean = true,
23 | val dismissOnClickOutside: Boolean = true,
24 | ) : CaAlertAction
25 |
26 | data class ShowSnack(
27 | val message: String = "",
28 | val actionLabel: String = "",
29 | val onAction: Action = None,
30 | val onDismiss: Action = None,
31 | val duration: Duration = if (actionLabel.isNotEmpty()) {
32 | Duration.Indefinite
33 | } else {
34 | Duration.Short
35 | },
36 | ) : CaAlertAction {
37 |
38 | enum class Duration {
39 | Short,
40 | Indefinite,
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-alert-system/src/main/java/tech/thdev/composable/architecture/alert/system/CaAlertSideEffect.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.alert.system
2 |
3 | import androidx.compose.material3.SnackbarDuration
4 | import tech.thdev.composable.architecture.action.system.Action
5 |
6 | internal sealed interface CaAlertSideEffect {
7 |
8 | data object ShowDialog : CaAlertSideEffect
9 |
10 | data object HideDialog : CaAlertSideEffect
11 |
12 | data class ShowSnack(
13 | val message: String,
14 | val actionLabel: String,
15 | val onAction: Action,
16 | val onDismiss: Action,
17 | val duration: SnackbarDuration = if (actionLabel.isNotEmpty()) {
18 | SnackbarDuration.Indefinite
19 | } else {
20 | SnackbarDuration.Short
21 | },
22 | ) : CaAlertSideEffect {
23 |
24 | companion object {
25 |
26 | val Default = ShowSnack(
27 | message = "",
28 | actionLabel = "",
29 | onAction = CaAlertAction.None,
30 | onDismiss = CaAlertAction.None,
31 | duration = SnackbarDuration.Indefinite,
32 | )
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-alert-system/src/main/java/tech/thdev/composable/architecture/alert/system/CaAlertSystem.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.alert.system
2 |
3 | import androidx.compose.material3.SnackbarHostState
4 | import androidx.compose.material3.SnackbarResult
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.runtime.setValue
10 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
11 | import androidx.lifecycle.viewmodel.compose.viewModel
12 | import tech.thdev.composable.architecture.action.system.Action
13 | import tech.thdev.composable.architecture.action.system.compose.LocalActionSenderOwner
14 | import tech.thdev.composable.architecture.action.system.lifecycle.LaunchedLifecycleActionViewModel
15 | import tech.thdev.composable.architecture.action.system.lifecycle.collectLifecycleEvent
16 | import tech.thdev.composable.architecture.action.system.send
17 | import tech.thdev.composable.architecture.alert.system.compose.CaDialogScreen
18 | import tech.thdev.composable.architecture.alert.system.model.CaAlertUiStateDialogUiState
19 |
20 | @Composable
21 | fun CaAlertScreen(
22 | snackbarHostState: SnackbarHostState,
23 | onDialogScreen: (@Composable (caAlertUiStateDialogUiState: CaAlertUiStateDialogUiState, onAction: (nextAction: Action) -> Unit) -> Unit)? = null,
24 | ) {
25 | InternalCaAlertScreen(
26 | snackbarHostState = snackbarHostState,
27 | onDialogScreen = onDialogScreen,
28 | )
29 | }
30 |
31 | @Composable
32 | private fun InternalCaAlertScreen(
33 | snackbarHostState: SnackbarHostState,
34 | onDialogScreen: (@Composable (caAlertUiStateDialogUiState: CaAlertUiStateDialogUiState, onAction: (nextAction: Action) -> Unit) -> Unit)? = null,
35 | caAlertViewModel: ActionAlertViewModel = viewModel(),
36 | ) {
37 | var showDialog by remember { mutableStateOf(false) }
38 | val action = LocalActionSenderOwner.current
39 |
40 | caAlertViewModel.sideEffect.collectLifecycleEvent { event ->
41 | when (event) {
42 | is CaAlertSideEffect.ShowDialog -> {
43 | showDialog = true
44 | }
45 |
46 | is CaAlertSideEffect.HideDialog -> {
47 | showDialog = false
48 | }
49 |
50 | is CaAlertSideEffect.ShowSnack -> {
51 | val result = snackbarHostState
52 | .showSnackbar(
53 | message = event.message,
54 | actionLabel = event.actionLabel.takeIf { it.isNotEmpty() },
55 | duration = event.duration,
56 | )
57 |
58 | when (result) {
59 | SnackbarResult.ActionPerformed -> action.send(event.onAction).invoke()
60 | SnackbarResult.Dismissed -> action.send(event.onDismiss).invoke()
61 | }
62 | }
63 | }
64 | }
65 |
66 | val caAlertUiStateDialogUiState by caAlertViewModel.alertUiStateDialogUiState.collectAsStateWithLifecycle()
67 | if (showDialog) {
68 | val onAction: (nextAction: Action) -> Unit = { nextAction ->
69 | action.send(CaAlertAction.HideDialog).invoke()
70 | action.send(nextAction).invoke()
71 | }
72 | onDialogScreen?.invoke(
73 | caAlertUiStateDialogUiState,
74 | onAction,
75 | ) ?: run {
76 | CaDialogScreen(
77 | caAlertUiStateDialogUiState = caAlertUiStateDialogUiState,
78 | onAction = onAction,
79 | )
80 | }
81 | }
82 |
83 | LaunchedLifecycleActionViewModel(
84 | viewModel = caAlertViewModel,
85 | )
86 | }
87 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-alert-system/src/main/java/tech/thdev/composable/architecture/alert/system/compose/CaDialogScreen.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.alert.system.compose
2 |
3 | import android.view.View
4 | import androidx.compose.material3.AlertDialog
5 | import androidx.compose.material3.Icon
6 | import androidx.compose.material3.Text
7 | import androidx.compose.material3.TextButton
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.res.painterResource
10 | import androidx.compose.ui.tooling.preview.Preview
11 | import androidx.compose.ui.window.DialogProperties
12 | import tech.thdev.composable.architecture.action.system.Action
13 | import tech.thdev.composable.architecture.alert.system.model.CaAlertUiStateDialogUiState
14 |
15 | @Composable
16 | internal fun CaDialogScreen(
17 | caAlertUiStateDialogUiState: CaAlertUiStateDialogUiState,
18 | onAction: (nextAction: Action) -> Unit,
19 | ) {
20 | AlertDialog(
21 | icon = {
22 | if (caAlertUiStateDialogUiState.icon != View.NO_ID) {
23 | Icon(
24 | painter = painterResource(id = caAlertUiStateDialogUiState.icon),
25 | contentDescription = null,
26 | )
27 | }
28 | },
29 | title = {
30 | if (caAlertUiStateDialogUiState.title.isNotEmpty()) {
31 | Text(text = caAlertUiStateDialogUiState.title)
32 | }
33 | },
34 | text = {
35 | if (caAlertUiStateDialogUiState.message.isNotEmpty()) {
36 | Text(text = caAlertUiStateDialogUiState.message)
37 | }
38 | },
39 | onDismissRequest = {
40 | onAction(caAlertUiStateDialogUiState.onDismissRequest)
41 | },
42 | confirmButton = {
43 | if (caAlertUiStateDialogUiState.confirmButtonText.isNotEmpty()) {
44 | TextButton(
45 | onClick = {
46 | onAction(caAlertUiStateDialogUiState.onConfirmButtonAction)
47 | },
48 | ) {
49 | Text(caAlertUiStateDialogUiState.confirmButtonText)
50 | }
51 | }
52 | },
53 | dismissButton = {
54 | if (caAlertUiStateDialogUiState.dismissButtonText.isNotEmpty()) {
55 | TextButton(
56 | onClick = {
57 | onAction(caAlertUiStateDialogUiState.onDismissButtonAction)
58 | }
59 | ) {
60 | Text(caAlertUiStateDialogUiState.dismissButtonText)
61 | }
62 | }
63 | },
64 | properties = DialogProperties(
65 | dismissOnBackPress = true,
66 | dismissOnClickOutside = true,
67 | ),
68 | )
69 | }
70 |
71 | @Preview(
72 | showBackground = true,
73 | )
74 | @Composable
75 | private fun PreviewCaDialogScreen() {
76 | CaDialogScreen(
77 | caAlertUiStateDialogUiState = CaAlertUiStateDialogUiState.Default.copy(
78 | title = "title",
79 | message = "A dialog is a type of modal window that appears in front of app content to provide critical" +
80 | "information, or ask for a decision to be made.",
81 | confirmButtonText = "Confirm",
82 | dismissButtonText = "Dismiss",
83 | ),
84 | onAction = {},
85 | )
86 | }
87 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-alert-system/src/main/java/tech/thdev/composable/architecture/alert/system/model/CaAlertUiStateDialogUiState.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.alert.system.model
2 |
3 | import android.view.View
4 | import androidx.annotation.DrawableRes
5 | import androidx.compose.runtime.Immutable
6 | import tech.thdev.composable.architecture.action.system.Action
7 | import tech.thdev.composable.architecture.alert.system.CaAlertAction
8 |
9 | @Immutable
10 | data class CaAlertUiStateDialogUiState(
11 | val title: String,
12 | val message: String,
13 | val confirmButtonText: String,
14 | val onConfirmButtonAction: Action,
15 | val dismissButtonText: String,
16 | val onDismissButtonAction: Action,
17 | val onDismissRequest: Action,
18 | @DrawableRes val icon: Int,
19 | val dismissOnBackPress: Boolean,
20 | val dismissOnClickOutside: Boolean,
21 | ) {
22 |
23 | companion object {
24 |
25 | val Default = CaAlertUiStateDialogUiState(
26 | title = "",
27 | message = "",
28 | confirmButtonText = "",
29 | onConfirmButtonAction = CaAlertAction.None,
30 | dismissButtonText = "",
31 | onDismissButtonAction = CaAlertAction.None,
32 | onDismissRequest = CaAlertAction.None,
33 | icon = View.NO_ID,
34 | dismissOnBackPress = true,
35 | dismissOnClickOutside = true,
36 | )
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-alert-system/src/test/java/tech/thdev/composable/architecture/alert/system/ShowDialogViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.alert.system
2 |
3 | import app.cash.turbine.test
4 | import kotlinx.coroutines.flow.first
5 | import kotlinx.coroutines.flow.flowOf
6 | import kotlinx.coroutines.test.runTest
7 | import org.junit.Assert
8 | import org.junit.Test
9 | import org.mockito.kotlin.mock
10 | import org.mockito.kotlin.verify
11 | import org.mockito.kotlin.whenever
12 | import tech.thdev.composable.architecture.action.system.FlowActionStream
13 | import tech.thdev.composable.architecture.alert.system.model.CaAlertUiStateDialogUiState
14 |
15 | internal class ShowDialogViewModelTest {
16 |
17 | private val flowActionStream = mock()
18 |
19 | private val caAlertViewModel = ActionAlertViewModel(
20 | flowActionStream = flowActionStream,
21 | )
22 |
23 | @Test
24 | fun `test initData`() {
25 | Assert.assertEquals(CaAlertUiStateDialogUiState.Default, caAlertViewModel.alertUiStateDialogUiState.value)
26 | }
27 |
28 | @Test
29 | fun `test ShowAlert`() = runTest {
30 | val mockItem = CaAlertAction.ShowDialog(
31 | title = "title",
32 | message = "message",
33 | confirmButtonText = "confirmButtonText",
34 | onConfirmButtonAction = CaAlertAction.None,
35 | dismissButtonText = "dismissButtonText",
36 | onDismissButtonAction = CaAlertAction.None,
37 | onDismissRequest = CaAlertAction.None,
38 | )
39 | whenever(flowActionStream.flowAction()).thenReturn(flowOf(mockItem))
40 |
41 | caAlertViewModel.flowAction.test {
42 | val convert = CaAlertUiStateDialogUiState.Default.copy(
43 | title = "title",
44 | message = "message",
45 | confirmButtonText = "confirmButtonText",
46 | dismissButtonText = "dismissButtonText",
47 | )
48 | Assert.assertEquals(mockItem, awaitItem())
49 | Assert.assertEquals(convert, caAlertViewModel.alertUiStateDialogUiState.value)
50 | Assert.assertEquals(CaAlertSideEffect.ShowDialog, caAlertViewModel.sideEffect.first())
51 |
52 | verify(flowActionStream).flowAction()
53 |
54 | cancelAndIgnoreRemainingEvents()
55 | }
56 | }
57 |
58 | @Test
59 | fun `test hideAlert`() = runTest {
60 | whenever(flowActionStream.flowAction()).thenReturn(flowOf(CaAlertAction.HideDialog))
61 |
62 | caAlertViewModel.flowAction.test {
63 | Assert.assertEquals(CaAlertUiStateDialogUiState.Default, caAlertViewModel.alertUiStateDialogUiState.value)
64 | Assert.assertEquals(CaAlertSideEffect.HideDialog, caAlertViewModel.sideEffect.first())
65 |
66 | verify(flowActionStream).flowAction()
67 |
68 | cancelAndIgnoreRemainingEvents()
69 | }
70 | }
71 |
72 | @Test
73 | fun `test Snack`() = runTest {
74 | val mockItem = CaAlertAction.ShowSnack(
75 | message = "message",
76 | actionLabel = "actionLabel",
77 | onAction = CaAlertAction.None,
78 | onDismiss = CaAlertAction.None,
79 | )
80 | whenever(flowActionStream.flowAction()).thenReturn(flowOf(mockItem))
81 |
82 | caAlertViewModel.flowAction.test {
83 | val convert = CaAlertSideEffect.ShowSnack.Default.copy(
84 | message = "message",
85 | actionLabel = "actionLabel",
86 | )
87 | Assert.assertEquals(mockItem, awaitItem())
88 | Assert.assertEquals(convert, caAlertViewModel.sideEffect.first())
89 |
90 | verify(flowActionStream).flowAction()
91 |
92 | cancelAndIgnoreRemainingEvents()
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import tech.thdev.gradle.configureComposeFeature
2 |
3 | plugins {
4 | alias(libs.plugins.tech.thdev.android.library)
5 | alias(libs.plugins.tech.thdev.android.library.hilt)
6 | alias(libs.plugins.tech.thdev.android.library.publish)
7 | }
8 |
9 | setNamespace("router.system")
10 |
11 | val (majorVersion, minorVersion, patchVersion, code) = getVersionInfo()
12 |
13 | ext["libraryName"] = "composable-architecture-router-system"
14 | ext["libraryVersion"] = "$majorVersion.$minorVersion.$patchVersion"
15 | ext["description"] = Publish.DESCRIPTION
16 | ext["url"] = Publish.PUBLISH_URL
17 |
18 | configureComposeFeature()
19 |
20 | android {
21 | buildTypes {
22 | getByName("debug") {
23 | isMinifyEnabled = false
24 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
25 | }
26 |
27 | getByName("release") {
28 | isMinifyEnabled = false
29 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
30 | }
31 | }
32 |
33 | // AGP 8.0
34 | publishing {
35 | multipleVariants("release") {
36 | allVariants()
37 | }
38 | }
39 | }
40 |
41 | dependencies {
42 | implementation(platform(libs.androidx.compose.bom))
43 | implementation(libs.androidx.compose.foundation)
44 | implementation(libs.androidx.compose.navigation)
45 | implementation(libs.androidx.compose.navigation.hilt)
46 |
47 | implementation(libs.androidx.compose.lifecycle.viewModel)
48 | implementation(libs.androidx.compose.activity)
49 | }
50 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/main/java/tech/thdev/composable/architecture/router/system/LaunchedRouter.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.router.system
2 |
3 | import androidx.activity.compose.LocalActivity
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.lifecycle.Lifecycle
7 | import androidx.lifecycle.compose.LocalLifecycleOwner
8 | import androidx.lifecycle.repeatOnLifecycle
9 | import androidx.lifecycle.viewmodel.compose.viewModel
10 | import androidx.navigation.NavGraph.Companion.findStartDestination
11 | import androidx.navigation.NavHostController
12 | import kotlinx.coroutines.flow.collectLatest
13 | import tech.thdev.composable.architecture.router.system.viewmodel.InternalRouteSideEffect
14 | import tech.thdev.composable.architecture.router.system.viewmodel.InternalRouteViewModel
15 |
16 | @Composable
17 | fun LaunchedRouter(
18 | navHostController: NavHostController? = null,
19 | ) {
20 | InternalLaunchedRouter(
21 | navHostController = navHostController,
22 | )
23 | }
24 |
25 | @Composable
26 | private fun InternalLaunchedRouter(
27 | navHostController: NavHostController? = null,
28 | internalRouterViewModel: InternalRouteViewModel = viewModel(),
29 | ) {
30 | val activity = LocalActivity.current
31 | val lifecycleOwner = LocalLifecycleOwner.current
32 | LaunchedEffect(internalRouterViewModel, lifecycleOwner) {
33 | lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
34 | internalRouterViewModel.sideEffect.collectLatest { sideEffect ->
35 | when (sideEffect) {
36 | is InternalRouteSideEffect.NavigateBack -> {
37 | if (navHostController?.previousBackStackEntry != null) {
38 | navHostController.popBackStack()
39 | } else {
40 | activity?.finish()
41 | }
42 | }
43 |
44 | is InternalRouteSideEffect.NavigateActivity -> {
45 | activity?.startActivity(
46 | sideEffect.activityRoute.getActivity(activity).apply {
47 | sideEffect.argumentMap.entries.forEach { (key, value) ->
48 | putExtra(key, value)
49 | }
50 | }
51 | )
52 | }
53 |
54 | is InternalRouteSideEffect.Navigate -> {
55 | navHostController?.let { navigation ->
56 | navigation.navigate(sideEffect.navigationRoute) {
57 | navigation.graph.findStartDestination().route?.let {
58 | popUpTo(it) {
59 | saveState = sideEffect.saveState
60 | }
61 | }
62 | restoreState = sideEffect.saveState
63 | }
64 | }
65 | }
66 | }
67 | }
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/main/java/tech/thdev/composable/architecture/router/system/Navigator.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.router.system
2 |
3 | import android.os.Parcelable
4 | import tech.thdev.composable.architecture.router.system.route.ActivityRoute
5 | import tech.thdev.composable.architecture.router.system.route.NavigationRoute
6 | import kotlin.reflect.KClass
7 |
8 | interface Navigator {
9 |
10 | suspend fun navigate(
11 | activityRoute: KClass,
12 | argumentMap: Map = emptyMap(),
13 | )
14 |
15 | suspend fun navigate(
16 | navigationRoute: NavigationRoute,
17 | saveState: Boolean = false,
18 | )
19 |
20 | suspend fun navigateBack()
21 | }
22 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/main/java/tech/thdev/composable/architecture/router/system/di/JourneyRouterModule.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.router.system.di
2 |
3 | import dagger.Module
4 | import dagger.Provides
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import tech.thdev.composable.architecture.router.system.internal.visitor.InternalActivityRouteMapper
8 | import tech.thdev.composable.architecture.router.system.route.ActivityRoute
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | internal object JourneyRouterModule {
14 |
15 | @Provides
16 | @Singleton
17 | fun providerInternalActivityRouteMapper(
18 | map: Map, @JvmSuppressWildcards ActivityRoute>,
19 | ): InternalActivityRouteMapper =
20 | InternalActivityRouteMapper(map)
21 | }
22 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/main/java/tech/thdev/composable/architecture/router/system/di/RouteKey.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.router.system.di
2 |
3 | import dagger.MapKey
4 | import tech.thdev.composable.architecture.router.system.route.ActivityRoute
5 | import kotlin.reflect.KClass
6 |
7 | /**
8 | * Use with Dagger or Hilt's IntoMap and RouterKey.
9 | *
10 | * Write the following code in your Dagger or Hilt module.
11 | *
12 | * Define the interface for MainActivity.
13 | * ```kotlin
14 | * interface MainActivityRoute : ActivityRoute
15 | * ```
16 | *
17 | * When using Activity, initialize the Intent in the implementation as follows.
18 | *
19 | * ```kotlin
20 | * class MainActivityRouteImpl @Inject internal constructor() : MainActivityRoute {
21 | *
22 | * override fun getActivity(context: Context): Intent =
23 | * Intent(context, MainActivity::class.java)
24 | * }
25 | * ```
26 | *
27 | * And define the Dagger / Hilt module as follows.
28 | *
29 | * ```kotlin
30 | * @Module
31 | * @InstallIn(SingletonComponent::class)
32 | * abstract class MainModule {
33 | *
34 | * @Binds
35 | * @IntoMap
36 | * @RouteKey(MainActivityRoute::class)
37 | * abstract fun bindMainActivityRoute(
38 | * mainActivityRoute: MainActivityRouteImpl,
39 | * ): ActivityRoute
40 | * }
41 | * ```
42 | */
43 | @MapKey
44 | annotation class RouteKey(
45 | val value: KClass,
46 | )
47 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/main/java/tech/thdev/composable/architecture/router/system/di/RouterModel.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.router.system.di
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.android.components.ActivityRetainedComponent
7 | import dagger.hilt.android.scopes.ActivityRetainedScoped
8 | import tech.thdev.composable.architecture.router.system.Navigator
9 | import tech.thdev.composable.architecture.router.system.internal.route.InternalNavigator
10 | import tech.thdev.composable.architecture.router.system.internal.route.InternalNavigatorImpl
11 |
12 | @Module
13 | @InstallIn(ActivityRetainedComponent::class)
14 | internal abstract class RouterModel {
15 |
16 | @Binds
17 | @ActivityRetainedScoped
18 | abstract fun provideNavigator(
19 | navigator: InternalNavigatorImpl
20 | ): Navigator
21 |
22 | @Binds
23 | @ActivityRetainedScoped
24 | abstract fun provideInternalNavigator(
25 | navigator: InternalNavigatorImpl
26 | ): InternalNavigator
27 | }
28 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/main/java/tech/thdev/composable/architecture/router/system/internal/route/InternalNavigator.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.router.system.internal.route
2 |
3 | import kotlinx.coroutines.channels.Channel
4 |
5 | interface InternalNavigator {
6 |
7 | val channel: Channel
8 | }
9 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/main/java/tech/thdev/composable/architecture/router/system/internal/route/InternalNavigatorImpl.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.router.system.internal.route
2 |
3 | import android.os.Parcelable
4 | import dagger.hilt.android.scopes.ActivityRetainedScoped
5 | import kotlinx.coroutines.channels.Channel
6 | import tech.thdev.composable.architecture.router.system.Navigator
7 | import tech.thdev.composable.architecture.router.system.route.ActivityRoute
8 | import tech.thdev.composable.architecture.router.system.route.NavigationRoute
9 | import javax.inject.Inject
10 | import kotlin.reflect.KClass
11 |
12 | @ActivityRetainedScoped
13 | internal class InternalNavigatorImpl @Inject constructor() : Navigator, InternalNavigator {
14 |
15 | override val channel = Channel(Channel.BUFFERED)
16 |
17 | override suspend fun navigate(activityRoute: KClass, argumentMap: Map) {
18 | channel.send(
19 | InternalRoute.Activity(
20 | activityRoute = activityRoute,
21 | argumentMap = argumentMap,
22 | )
23 | )
24 | }
25 |
26 | override suspend fun navigate(navigationRoute: NavigationRoute, saveState: Boolean) {
27 | channel.send(
28 | InternalRoute.Navigation(
29 | navigationRoute = navigationRoute,
30 | saveState = saveState,
31 | )
32 | )
33 | }
34 |
35 | override suspend fun navigateBack() {
36 | channel.send(InternalRoute.NavigateBack)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/main/java/tech/thdev/composable/architecture/router/system/internal/route/InternalRoute.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.router.system.internal.route
2 |
3 | import android.os.Parcelable
4 | import tech.thdev.composable.architecture.router.system.route.ActivityRoute
5 | import tech.thdev.composable.architecture.router.system.route.NavigationRoute
6 | import kotlin.reflect.KClass
7 |
8 | sealed interface InternalRoute {
9 |
10 | data class Activity(
11 | val activityRoute: KClass,
12 | val argumentMap: Map = emptyMap(),
13 | ) : InternalRoute
14 |
15 | data class Navigation(
16 | val navigationRoute: NavigationRoute,
17 | val saveState: Boolean = false,
18 | ) : InternalRoute
19 |
20 | data object NavigateBack : InternalRoute
21 | }
22 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/main/java/tech/thdev/composable/architecture/router/system/internal/visitor/InternalActivityRouteMapper.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.router.system.internal.visitor
2 |
3 | import androidx.annotation.VisibleForTesting
4 | import tech.thdev.composable.architecture.router.system.route.ActivityRoute
5 | import javax.inject.Inject
6 | import javax.inject.Singleton
7 | import kotlin.reflect.KClass
8 |
9 | /**
10 | * Dagger or hilt Map
11 | */
12 | @Singleton
13 | internal class InternalActivityRouteMapper @Inject constructor(
14 | @get:VisibleForTesting val mapper: Map, ActivityRoute>,
15 | ) {
16 |
17 | /**
18 | * Find and return ActivityRoute from the data stored in Mapper.
19 | */
20 | internal fun getJourneyOrNull(journeyKClass: KClass<*>): ActivityRoute? =
21 | synchronized(mapper) {
22 | mapper[journeyKClass.java]
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/main/java/tech/thdev/composable/architecture/router/system/route/ActivityRoute.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.router.system.route
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 |
6 | /**
7 | * Use with Dagger or Hilt's IntoMap and CaRouterKey.
8 | *
9 | * Write the following code in your Dagger or Hilt module.
10 | *
11 | * Define the interface for MainActivity.
12 | * ```kotlin
13 | * interface MainActivityRoute : ActivityRoute
14 | * ```
15 | *
16 | * When using Activity, initialize the Intent in the implementation as follows.
17 | *
18 | * ```kotlin
19 | * class MainActivityRouteImpl @Inject internal constructor() : MainActivityRoute {
20 | *
21 | * override fun getActivity(context: Context): Intent =
22 | * Intent(context, MainActivity::class.java)
23 | * }
24 | * ```
25 | *
26 | * And define the Dagger / Hilt module as follows.
27 | *
28 | * ```kotlin
29 | * @Module
30 | * @InstallIn(SingletonComponent::class)
31 | * abstract class MainModule {
32 | *
33 | * @Binds
34 | * @IntoMap
35 | * @RouterKey(MainActivityRoute::class)
36 | * abstract fun bindMainActivityRoute(
37 | * mainActivityRoute: MainActivityRouteImpl,
38 | * ): ActivityRoute
39 | * }
40 | * ```
41 | */
42 | interface ActivityRoute {
43 |
44 | fun getActivity(context: Context): Intent
45 | }
46 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/main/java/tech/thdev/composable/architecture/router/system/route/NavigationRoute.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.router.system.route
2 |
3 | /**
4 | * CaNavigation is defined as follows.
5 | *
6 | * ```kotlin
7 | * @Serializable
8 | * data object MainRoute : CaNavigationRoute
9 | * ```
10 | */
11 | interface NavigationRoute
12 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/main/java/tech/thdev/composable/architecture/router/system/viewmodel/InternalRouteSideEffect.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.router.system.viewmodel
2 |
3 | import android.os.Parcelable
4 | import tech.thdev.composable.architecture.router.system.route.ActivityRoute
5 | import tech.thdev.composable.architecture.router.system.route.NavigationRoute
6 |
7 | internal sealed interface InternalRouteSideEffect {
8 |
9 | data class Navigate(
10 | val navigationRoute: NavigationRoute,
11 | val saveState: Boolean,
12 | ) : InternalRouteSideEffect
13 |
14 | data class NavigateActivity(
15 | val activityRoute: ActivityRoute,
16 | val argumentMap: Map,
17 | ) : InternalRouteSideEffect
18 |
19 | data object NavigateBack : InternalRouteSideEffect
20 | }
21 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/main/java/tech/thdev/composable/architecture/router/system/viewmodel/InternalRouteViewModel.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.router.system.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import dagger.hilt.android.lifecycle.HiltViewModel
5 | import kotlinx.coroutines.flow.filterNotNull
6 | import kotlinx.coroutines.flow.map
7 | import kotlinx.coroutines.flow.receiveAsFlow
8 | import tech.thdev.composable.architecture.router.system.internal.route.InternalNavigator
9 | import tech.thdev.composable.architecture.router.system.internal.route.InternalRoute
10 | import tech.thdev.composable.architecture.router.system.internal.visitor.InternalActivityRouteMapper
11 | import javax.inject.Inject
12 |
13 | @HiltViewModel
14 | internal class InternalRouteViewModel @Inject internal constructor(
15 | navigator: InternalNavigator,
16 | private val journeyMapper: InternalActivityRouteMapper,
17 | ) : ViewModel() {
18 |
19 | val sideEffect by lazy(LazyThreadSafetyMode.NONE) {
20 | navigator.channel.receiveAsFlow()
21 | .map { router ->
22 | when (router) {
23 | is InternalRoute.Activity<*> -> {
24 | journeyMapper.getJourneyOrNull(router.activityRoute)?.let {
25 | InternalRouteSideEffect.NavigateActivity(
26 | activityRoute = it,
27 | argumentMap = router.argumentMap,
28 | )
29 | }
30 | }
31 |
32 | is InternalRoute.Navigation -> {
33 | InternalRouteSideEffect.Navigate(
34 | navigationRoute = router.navigationRoute,
35 | saveState = router.saveState,
36 | )
37 | }
38 |
39 | is InternalRoute.NavigateBack -> {
40 | InternalRouteSideEffect.NavigateBack
41 | }
42 | }
43 | }
44 | .filterNotNull()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/test/java/tech/thdev/composable/architecture/router/system/fake/FakeActivityRoute.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.router.system.fake
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import org.mockito.kotlin.mock
6 | import tech.thdev.composable.architecture.router.system.route.ActivityRoute
7 |
8 | internal class FakeActivityRoute : ActivityRoute {
9 |
10 | override fun getActivity(context: Context): Intent =
11 | mock()
12 | }
13 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/test/java/tech/thdev/composable/architecture/router/system/fake/FakeNavigation.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.router.system.fake
2 |
3 | import tech.thdev.composable.architecture.router.system.route.NavigationRoute
4 |
5 | object FakeNavigation : NavigationRoute
6 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/test/java/tech/thdev/composable/architecture/router/system/internal/route/InternalNavigatorImplTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.router.system.internal.route
2 |
3 | import app.cash.turbine.test
4 | import kotlinx.coroutines.flow.receiveAsFlow
5 | import kotlinx.coroutines.test.runTest
6 | import org.junit.Assert
7 | import org.junit.Test
8 | import tech.thdev.composable.architecture.router.system.fake.FakeActivityRoute
9 | import tech.thdev.composable.architecture.router.system.fake.FakeNavigation
10 |
11 | internal class InternalNavigatorImplTest {
12 |
13 | private val navigator = InternalNavigatorImpl()
14 |
15 | @Test
16 | fun `test navigate`() = runTest {
17 | navigator.channel.receiveAsFlow()
18 | .test {
19 | // fake route test
20 | navigator.navigate(FakeNavigation)
21 | Assert.assertEquals(InternalRoute.Navigation(FakeNavigation, false), awaitItem())
22 |
23 | // Back test
24 | navigator.navigateBack()
25 | Assert.assertEquals(InternalRoute.NavigateBack, awaitItem())
26 |
27 | // fake route activity
28 | navigator.navigate(FakeActivityRoute::class)
29 | Assert.assertEquals(InternalRoute.Activity(FakeActivityRoute::class), awaitItem())
30 |
31 | cancelAndConsumeRemainingEvents()
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/test/java/tech/thdev/composable/architecture/router/system/internal/visitor/InternalActivityRouteMapperTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.router.system.internal.visitor
2 |
3 | import org.junit.Assert
4 | import org.junit.Test
5 | import tech.thdev.composable.architecture.router.system.fake.FakeActivityRoute
6 |
7 | internal class InternalActivityRouteMapperTest {
8 |
9 | private val fakeActivityRoute = FakeActivityRoute()
10 | private val journeyMapper = InternalActivityRouteMapper(mapper = mapOf(FakeActivityRoute::class.java to fakeActivityRoute))
11 |
12 | @Test
13 | fun `test initData`() {
14 | Assert.assertEquals(mapOf(FakeActivityRoute::class.java to fakeActivityRoute), journeyMapper.mapper)
15 | }
16 |
17 | @Test
18 | fun `test getJourneyOrNull`() {
19 | Assert.assertEquals(fakeActivityRoute, journeyMapper.getJourneyOrNull(FakeActivityRoute::class))
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-router-system/src/test/java/tech/thdev/composable/architecture/router/system/viewmodel/InternalRouteViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.router.system.viewmodel
2 |
3 | import app.cash.turbine.test
4 | import kotlinx.coroutines.channels.Channel
5 | import kotlinx.coroutines.test.runTest
6 | import org.junit.Assert
7 | import org.junit.Test
8 | import org.mockito.kotlin.mock
9 | import org.mockito.kotlin.whenever
10 | import tech.thdev.composable.architecture.router.system.fake.FakeActivityRoute
11 | import tech.thdev.composable.architecture.router.system.fake.FakeNavigation
12 | import tech.thdev.composable.architecture.router.system.internal.route.InternalNavigator
13 | import tech.thdev.composable.architecture.router.system.internal.route.InternalRoute
14 | import tech.thdev.composable.architecture.router.system.internal.visitor.InternalActivityRouteMapper
15 |
16 | internal class InternalRouteViewModelTest {
17 |
18 | private val fakeActivityRoute = FakeActivityRoute()
19 | private val journeyMapper = InternalActivityRouteMapper(mapper = mapOf(FakeActivityRoute::class.java to fakeActivityRoute))
20 |
21 | private val navigator = mock()
22 |
23 | private val viewModel = InternalRouteViewModel(
24 | navigator = navigator,
25 | journeyMapper = journeyMapper,
26 | )
27 |
28 | @Test
29 | fun `test sideEffect`() = runTest {
30 | val mockChannel = Channel(Channel.BUFFERED)
31 | whenever(navigator.channel).thenReturn(mockChannel)
32 |
33 | viewModel.sideEffect
34 | .test {
35 | // Move navigation
36 | mockChannel.send(
37 | InternalRoute.Navigation(
38 | navigationRoute = FakeNavigation,
39 | saveState = true,
40 | )
41 | )
42 | Assert.assertEquals(InternalRouteSideEffect.Navigate(navigationRoute = FakeNavigation, saveState = true), awaitItem())
43 |
44 | // Move activity
45 | mockChannel.send(
46 | InternalRoute.Activity(
47 | activityRoute = FakeActivityRoute::class,
48 | )
49 | )
50 | Assert.assertEquals(InternalRouteSideEffect.NavigateActivity(activityRoute = fakeActivityRoute, argumentMap = emptyMap()), awaitItem())
51 |
52 | // Back test
53 | mockChannel.send(InternalRoute.NavigateBack)
54 | Assert.assertEquals(InternalRouteSideEffect.NavigateBack, awaitItem())
55 |
56 | cancelAndConsumeRemainingEvents()
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-system/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/ui/composable-architecture-system/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import tech.thdev.gradle.configureComposeFeature
2 |
3 | plugins {
4 | alias(libs.plugins.tech.thdev.android.library)
5 | alias(libs.plugins.tech.thdev.android.library.hilt)
6 | alias(libs.plugins.tech.thdev.android.library.publish)
7 | }
8 |
9 | setNamespace("system")
10 |
11 | val (majorVersion, minorVersion, patchVersion, code) = getVersionInfo()
12 |
13 | ext["libraryName"] = "composable-architecture-system"
14 | ext["libraryVersion"] = "$majorVersion.$minorVersion.$patchVersion"
15 | ext["description"] = Publish.DESCRIPTION
16 | ext["url"] = Publish.PUBLISH_URL
17 |
18 | configureComposeFeature()
19 |
20 | android {
21 | buildTypes {
22 | getByName("debug") {
23 | isMinifyEnabled = false
24 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
25 | }
26 |
27 | getByName("release") {
28 | isMinifyEnabled = false
29 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
30 | }
31 | }
32 |
33 | // AGP 8.0
34 | publishing {
35 | multipleVariants("release") {
36 | allVariants()
37 | }
38 | }
39 | }
40 |
41 | dependencies {
42 | implementation(platform(libs.androidx.compose.bom))
43 | implementation(libs.androidx.compose.foundation)
44 |
45 | implementation(libs.androidx.compose.lifecycle.viewModel)
46 | }
47 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-system/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/core/ui/composable-architecture-system/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-system/src/main/java/tech/thdev/composable/architecture/action/system/Action.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.action.system
2 |
3 | /**
4 | * Define an Action by inheriting and implementing [Action].
5 | *
6 | * ```kotlin
7 | * sealed interface Action : CaAction {
8 | *
9 | * data object SomeAction : Action
10 | *
11 | * data object Move : Action
12 | * }
13 | * ```
14 | */
15 | interface Action
16 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-system/src/main/java/tech/thdev/composable/architecture/action/system/ActionSender.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.action.system
2 |
3 | interface ActionSender {
4 |
5 | fun send(action: Action)
6 | }
7 |
8 | fun ActionSender?.send(action: Action): () -> Unit =
9 | { this?.send(action) }
10 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-system/src/main/java/tech/thdev/composable/architecture/action/system/FlowActionStream.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.action.system
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | /**
6 | * Definitions for utilizing [Action] events in a [tech.thdev.composable.architecture.base.ActionViewModel].
7 | *
8 | * ```kotlin
9 | * class MainViewModel constructor(
10 | * flowCaActionStream: FlowCaActionStream,
11 | * ) : ViewModel() {
12 | *
13 | * private val _sideEffect = Channel(Channel.BUFFERED)
14 | * internal val sideEffect = _sideEffect.receiveAsFlow()
15 | *
16 | * // Handle events received through CaActionSender
17 | * @VisibleForTesting
18 | * val flowAction by lazy(LazyThreadSafetyMode.NONE) {
19 | * flowCaActionStream.flowAction()
20 | * .filterIsInstance()
21 | * .flatMapLatest {
22 | * reducer(it)
23 | * }
24 | * .onEach {
25 | * // If the reducer generates an event, pass it on
26 | * flowActionStream.nextAction(it)
27 | * }
28 | * }
29 | *
30 | * // Definition for using CA Action as a reducer
31 | * private fun reducer(action: CaAction) {
32 | * return when (action) {
33 | * is DefaultAction.AppEnd -> {
34 | * _sideEffect.send(SideEffect.End)
35 | * }
36 | * }
37 | * }
38 | * }
39 | * ```
40 | */
41 | interface FlowActionStream {
42 |
43 | fun flowAction(): Flow
44 |
45 | fun nextAction(action: Action)
46 | }
47 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-system/src/main/java/tech/thdev/composable/architecture/action/system/base/ActionViewModel.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.action.system.base
2 |
3 | import androidx.annotation.VisibleForTesting
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import kotlinx.coroutines.Job
7 | import kotlinx.coroutines.flow.filterIsInstance
8 | import kotlinx.coroutines.flow.launchIn
9 | import kotlinx.coroutines.flow.onEach
10 | import tech.thdev.composable.architecture.action.system.Action
11 | import tech.thdev.composable.architecture.action.system.ActionSender
12 | import tech.thdev.composable.architecture.action.system.FlowActionStream
13 | import javax.inject.Inject
14 | import kotlin.reflect.KClass
15 |
16 | /**
17 | * CaViewModel is a class designed to work with [tech.thdev.composable.architecture.action.system.ActionSender].
18 | * It uses a similar structure to a reducer, allowing you to utilize it as shown below. It also supports SideEffect handling.
19 | *
20 | * ```kotlin
21 | * class MainViewModel constructor(
22 | * flowActionStream: FlowActionStream,
23 | * ) : ActionViewModel(flowActionStream, SomeAction::class) {
24 | *
25 | * private val _sideEffect = Channel(Channel.BUFFERED)
26 | * internal val sideEffect = _sideEffect.receiveAsFlow()
27 | *
28 | * override suspend fun handleAction(action: SomeAction) =
29 | * when (action) {
30 | * is SomeAction.Event -> {
31 | * _sideEffect.send(SideEffect.ShowToast)
32 | * // or
33 | * flowCaActionStream.nextAction(SomeAction.ShowToast)
34 | * }
35 | *
36 | * is SomeAction.ShowToast -> {
37 | * // your code
38 | * }
39 | * }
40 | * }
41 | * ```
42 | *
43 | * After inheriting from CaViewModel, you must handle two values in the base class:
44 | * - Required: Call `viewModel.loadAction()`
45 | * - Optional: Collect `viewModel.sideEffect` using `viewModel.sideEffect.collectAsEvent { ... }`
46 | *
47 | * ```kotlin
48 | * @Composable
49 | * fun SomeScreen(
50 | * someViewModel: SomeViewModel = viewModel(), // or, Use navigation hiltViewModel()
51 | * ) {
52 | * ActionSenderCompositionLocalProvider(someViewModel) {
53 | * val uiState by someViewModel.uiState.collectAsStateWithLifecycle()
54 | * SomeScreen(
55 | * uiState = uiState,
56 | * )
57 | * }
58 | * }
59 | * ```
60 | */
61 | abstract class ActionViewModel(
62 | private val flowActionStream: FlowActionStream,
63 | actionClass: KClass,
64 | ) : ViewModel() {
65 |
66 | @Inject
67 | internal lateinit var actionSender: ActionSender
68 |
69 | @VisibleForTesting
70 | var flowActionJob: Job? = null
71 |
72 | @VisibleForTesting
73 | var isFirst = true
74 |
75 | @VisibleForTesting
76 | val flowAction by lazy {
77 | flowActionStream.flowAction()
78 | .filterIsInstance(actionClass)
79 | .onEach {
80 | handleAction(action = it)
81 | }
82 | }
83 |
84 | internal fun loadAction() {
85 | cancelAction()
86 |
87 | flowActionJob = flowAction
88 | .launchIn(viewModelScope)
89 |
90 | if (isFirst) {
91 | onCreated()
92 | isFirst = false
93 | }
94 | }
95 |
96 | /**
97 | * ViewModel created only call once.
98 | */
99 | open fun onCreated() {}
100 |
101 | internal fun cancelAction() {
102 | if (flowActionJob?.isActive == true) {
103 | flowActionJob?.cancel()
104 | flowActionJob = null
105 | }
106 | }
107 |
108 | fun nextAction(action: ACTION) {
109 | flowActionStream.nextAction(action)
110 | }
111 |
112 | abstract suspend fun handleAction(action: ACTION)
113 | }
114 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-system/src/main/java/tech/thdev/composable/architecture/action/system/compose/LocalActionSenderOwner.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.action.system.compose
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.CompositionLocalProvider
5 | import androidx.compose.runtime.ProvidedValue
6 | import androidx.compose.runtime.staticCompositionLocalOf
7 | import tech.thdev.composable.architecture.action.system.ActionSender
8 | import tech.thdev.composable.architecture.action.system.base.ActionViewModel
9 | import tech.thdev.composable.architecture.action.system.lifecycle.LaunchedLifecycleActionViewModel
10 |
11 | /**
12 | * Define [tech.thdev.composable.architecture.action.system.ActionSender] at the beginning of the Compose hierarchy.
13 | * Since Hilt Singleton is used, it's injected as follows:
14 | *
15 | * Apply action - your compose screen
16 | * ```kotlin
17 | * @Composable
18 | * fun SomeScreen(
19 | * someViewModel: SomeViewModel = actionViewModelActivate(), // or, Use navigation actionHiltViewModelActivate()
20 | * ) {
21 | * ActionSenderCompositionLocalProvider(someViewModel) {
22 | * val uiState by someViewModel.uiState.collectAsStateWithLifecycle()
23 | * SomeScreen(
24 | * uiState = uiState,
25 | * )
26 | * }
27 | * }
28 | * ```
29 | *
30 | * Action Define
31 | * ```kotlin
32 | * sealed interface SomeAction : Action {
33 | * data object Move : SomeAction
34 | * }
35 | * ```
36 | *
37 | * Use LocalActionSenderOwner
38 | *
39 | * ```kotlin
40 | * @Composable
41 | * fun SomeScreen(uiState: UiState) {
42 | * val actionSender = LocalActionSenderOwner.current
43 | *
44 | * Column {
45 | * Text(
46 | * text = uiState.text
47 | * )
48 | *
49 | * Button(
50 | * onClick = actionSender.send(Action.Move),
51 | * ) {
52 | * Text(text = "Move")
53 | * }
54 | * }
55 | * }
56 | * ```
57 | */
58 | object LocalActionSenderOwner {
59 |
60 | private val LocalComposition = staticCompositionLocalOf { null }
61 |
62 | val current: ActionSender?
63 | @Composable
64 | get() = LocalComposition.current
65 |
66 | internal infix fun provides(registerOwner: ActionSender): ProvidedValue =
67 | LocalComposition provides registerOwner
68 | }
69 |
70 | @Composable
71 | fun ActionSenderCompositionLocalProvider(
72 | actionViewModel: ActionViewModel<*>,
73 | body: @Composable () -> Unit,
74 | ) {
75 | LaunchedLifecycleActionViewModel(viewModel = actionViewModel)
76 | CompositionLocalProvider(
77 | LocalActionSenderOwner provides actionViewModel.actionSender,
78 | ) {
79 | body()
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-system/src/main/java/tech/thdev/composable/architecture/action/system/internal/InternalActionImpl.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.action.system.internal
2 |
3 | import dagger.hilt.android.scopes.ViewModelScoped
4 | import kotlinx.coroutines.channels.BufferOverflow
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.MutableSharedFlow
7 | import kotlinx.coroutines.flow.asSharedFlow
8 | import tech.thdev.composable.architecture.action.system.Action
9 | import tech.thdev.composable.architecture.action.system.ActionSender
10 | import tech.thdev.composable.architecture.action.system.FlowActionStream
11 | import javax.inject.Inject
12 |
13 | @ViewModelScoped
14 | internal class InternalActionImpl @Inject constructor() : FlowActionStream, ActionSender {
15 |
16 | private val flowCaAction = MutableSharedFlow(
17 | extraBufferCapacity = 1,
18 | onBufferOverflow = BufferOverflow.DROP_OLDEST,
19 | )
20 |
21 | override fun flowAction(): Flow =
22 | flowCaAction.asSharedFlow()
23 |
24 | override fun send(action: Action) {
25 | flowCaAction.tryEmit(action)
26 | }
27 |
28 | override fun nextAction(action: Action) {
29 | send(action)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-system/src/main/java/tech/thdev/composable/architecture/action/system/internal/di/InternalActionModule.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.action.system.internal.di
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.android.components.ViewModelComponent
7 | import dagger.hilt.android.scopes.ViewModelScoped
8 | import tech.thdev.composable.architecture.action.system.ActionSender
9 | import tech.thdev.composable.architecture.action.system.FlowActionStream
10 | import tech.thdev.composable.architecture.action.system.internal.InternalActionImpl
11 |
12 | @Module
13 | @InstallIn(ViewModelComponent::class)
14 | internal abstract class InternalActionModule {
15 |
16 | @Binds
17 | @ViewModelScoped
18 | abstract fun bindFlowActionStream(
19 | internalCaAction: InternalActionImpl,
20 | ): FlowActionStream
21 |
22 | @Binds
23 | @ViewModelScoped
24 | abstract fun bindCaActionSender(
25 | internalCaAction: InternalActionImpl,
26 | ): ActionSender
27 | }
28 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-system/src/main/java/tech/thdev/composable/architecture/action/system/lifecycle/CollectLifecycleEvent.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.action.system.lifecycle
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.remember
9 | import androidx.lifecycle.Lifecycle
10 | import androidx.lifecycle.compose.LocalLifecycleOwner
11 | import androidx.lifecycle.repeatOnLifecycle
12 | import kotlinx.coroutines.flow.Flow
13 | import kotlinx.coroutines.flow.collectLatest
14 |
15 | @SuppressLint("ComposableNaming")
16 | @Composable
17 | fun Flow.collectLifecycleEvent(
18 | state: Lifecycle.State = Lifecycle.State.STARTED,
19 | onBody: suspend (item: T) -> Unit,
20 | ) {
21 | val body by remember { mutableStateOf(onBody) }
22 | val lifecycle = LocalLifecycleOwner.current.lifecycle
23 | LaunchedEffect(body) {
24 | lifecycle.repeatOnLifecycle(state) {
25 | collectLatest {
26 | body(it)
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-system/src/main/java/tech/thdev/composable/architecture/action/system/lifecycle/LaunchedLifecycleActionViewModel.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.action.system.lifecycle
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.DisposableEffect
5 | import androidx.lifecycle.Lifecycle
6 | import androidx.lifecycle.LifecycleEventObserver
7 | import androidx.lifecycle.compose.LocalLifecycleOwner
8 | import tech.thdev.composable.architecture.action.system.base.ActionViewModel
9 |
10 | /**
11 | * This code must be used in conjunction with CaViewModel<*>.
12 | * It can be used as shown below.
13 | *
14 | * ```kotlin
15 | * class MainActivity : CaActionActivity() {
16 | *
17 | * private val mainViewModel by viewModels()
18 | *
19 | * @Composable
20 | * override fun ContentView() {
21 | * TComposableArchitectureTheme {
22 | * LaunchedLifecycleViewModel(viewModel = mainViewModel) // Required
23 | *
24 | * // Your Compose view
25 | * }
26 | * }
27 | * }
28 | * ```
29 | */
30 | @Composable
31 | fun LaunchedLifecycleActionViewModel(viewModel: ActionViewModel<*>) {
32 | val lifecycleOwner = LocalLifecycleOwner.current
33 | DisposableEffect(lifecycleOwner) {
34 | val observer = LifecycleEventObserver { _, event ->
35 | when (event) {
36 | Lifecycle.Event.ON_RESUME -> {
37 | viewModel.loadAction()
38 | }
39 |
40 | Lifecycle.Event.ON_PAUSE -> {
41 | viewModel.cancelAction()
42 | }
43 |
44 | else -> {}
45 | }
46 | }
47 | lifecycleOwner.lifecycle.addObserver(observer)
48 | onDispose {
49 | viewModel.cancelAction()
50 | lifecycleOwner.lifecycle.removeObserver(observer)
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-system/src/test/java/tech/thdev/composable/architecture/action/system/base/ActionViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.action.system.base
2 |
3 | import app.cash.turbine.test
4 | import kotlinx.coroutines.test.runTest
5 | import org.junit.Assert
6 | import org.junit.Test
7 | import tech.thdev.composable.architecture.action.system.mock.FakeAction
8 | import tech.thdev.composable.architecture.action.system.mock.FakeViewModel
9 |
10 | internal class ActionViewModelTest {
11 |
12 | private val viewModel = FakeViewModel()
13 |
14 | @Test
15 | fun `test initData`() {
16 | Assert.assertFalse(viewModel.taskClick)
17 | Assert.assertFalse(viewModel.clickEvent)
18 | Assert.assertTrue(viewModel.isFirst)
19 | Assert.assertNull(viewModel.flowActionJob)
20 | }
21 |
22 | @Test
23 | fun `test Action-Task`() = runTest {
24 | viewModel.flowAction.test {
25 | expectNoEvents()
26 |
27 | viewModel.nextAction(FakeAction.Task)
28 | Assert.assertTrue(viewModel.taskClick)
29 |
30 | cancelAndIgnoreRemainingEvents()
31 | }
32 | }
33 |
34 | @Test
35 | fun `test Action-ClickEvent`() = runTest {
36 | viewModel.flowAction.test {
37 | expectNoEvents()
38 |
39 | viewModel.nextAction(FakeAction.ClickEvent)
40 | Assert.assertTrue(viewModel.clickEvent)
41 |
42 | cancelAndIgnoreRemainingEvents()
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-system/src/test/java/tech/thdev/composable/architecture/action/system/internal/InternalActionImplTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.action.system.internal
2 |
3 | import app.cash.turbine.test
4 | import kotlinx.coroutines.test.runTest
5 | import org.junit.Assert
6 | import org.junit.Test
7 | import tech.thdev.composable.architecture.action.system.Action
8 |
9 | class InternalActionImplTest {
10 |
11 | private val flowAction = InternalActionImpl()
12 |
13 | @Test
14 | fun `test flowAction`() = runTest {
15 | flowAction.flowAction()
16 | .test {
17 | expectNoEvents()
18 |
19 | flowAction.send(SomeAction)
20 | Assert.assertEquals(SomeAction, awaitItem())
21 |
22 | flowAction.nextAction(NextAction)
23 | Assert.assertEquals(NextAction, awaitItem())
24 |
25 | cancelAndIgnoreRemainingEvents()
26 | }
27 | }
28 |
29 | companion object {
30 |
31 | private val SomeAction = object : Action {}
32 |
33 | private val NextAction = object : Action {}
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-system/src/test/java/tech/thdev/composable/architecture/action/system/mock/FakeAction.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.action.system.mock
2 |
3 | import tech.thdev.composable.architecture.action.system.Action
4 |
5 | internal sealed interface FakeAction : Action {
6 |
7 | data object Task : FakeAction
8 |
9 | data object ClickEvent : FakeAction
10 | }
11 |
--------------------------------------------------------------------------------
/core/ui/composable-architecture-system/src/test/java/tech/thdev/composable/architecture/action/system/mock/FakeViewModel.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.action.system.mock
2 |
3 | import tech.thdev.composable.architecture.action.system.base.ActionViewModel
4 | import tech.thdev.composable.architecture.action.system.internal.InternalActionImpl
5 |
6 | internal class FakeViewModel : ActionViewModel(
7 | flowActionStream = InternalActionImpl(),
8 | actionClass = FakeAction::class,
9 | ) {
10 |
11 | var taskClick = false
12 |
13 | var clickEvent = false
14 |
15 | override suspend fun handleAction(action: FakeAction) {
16 | when (action) {
17 | FakeAction.Task -> {
18 | taskClick = true
19 | }
20 |
21 | FakeAction.ClickEvent -> {
22 | clickEvent = true
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 -XX:+EnableDynamicAgentLoading
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/TComposableArchitecture/72937912725e1d90741ae9d97e7d27ae265eea4f/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Feb 01 11:27:00 KST 2025
2 | # https://gradle.org/releases/
3 | distributionBase=GRADLE_USER_HOME
4 | distributionPath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/sample/core/ui/resource/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sample/core/ui/resource/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import tech.thdev.gradle.configureComposeFeature
2 |
3 | plugins {
4 | alias(libs.plugins.tech.thdev.android.library)
5 | }
6 |
7 | setNamespace("sample.resource")
8 |
9 | configureComposeFeature()
10 |
11 | dependencies {
12 | implementation(platform(libs.androidx.compose.bom))
13 | implementation(libs.androidx.compose.runtime)
14 | implementation(libs.androidx.compose.material3)
15 | }
16 |
--------------------------------------------------------------------------------
/sample/core/ui/resource/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/java/tech/thdev/composable/architecture/sample/resource/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.resource.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
12 |
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/java/tech/thdev/composable/architecture/sample/resource/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.resource.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.darkColorScheme
7 | import androidx.compose.material3.dynamicDarkColorScheme
8 | import androidx.compose.material3.dynamicLightColorScheme
9 | import androidx.compose.material3.lightColorScheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.platform.LocalContext
12 |
13 | private val DarkColorScheme = darkColorScheme(
14 | primary = Purple80,
15 | secondary = PurpleGrey80,
16 | tertiary = Pink80
17 | )
18 |
19 | private val LightColorScheme = lightColorScheme(
20 | primary = Purple40,
21 | secondary = PurpleGrey40,
22 | tertiary = Pink40
23 |
24 | /* Other default colors to override
25 | background = Color(0xFFFFFBFE),
26 | surface = Color(0xFFFFFBFE),
27 | onPrimary = Color.White,
28 | onSecondary = Color.White,
29 | onTertiary = Color.White,
30 | onBackground = Color(0xFF1C1B1F),
31 | onSurface = Color(0xFF1C1B1F), */
32 | )
33 |
34 | @Composable
35 | fun TComposableArchitectureTheme(
36 | darkTheme: Boolean = isSystemInDarkTheme(),
37 | // Dynamic color is available on Android 12+
38 | dynamicColor: Boolean = true,
39 | content: @Composable () -> Unit
40 | ) {
41 | val colorScheme = when {
42 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
43 | val context = LocalContext.current
44 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
45 | }
46 |
47 | darkTheme -> DarkColorScheme
48 | else -> LightColorScheme
49 | }
50 |
51 | MaterialTheme(
52 | colorScheme = colorScheme,
53 | typography = Typography,
54 | content = content
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/java/tech/thdev/composable/architecture/sample/resource/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.resource.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | ) */
33 | )
34 |
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/drawable/baseline_info_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/mipmap-anydpi/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/mipmap-anydpi/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/TComposableArchitecture/72937912725e1d90741ae9d97e7d27ae265eea4f/sample/core/ui/resource/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/TComposableArchitecture/72937912725e1d90741ae9d97e7d27ae265eea4f/sample/core/ui/resource/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/TComposableArchitecture/72937912725e1d90741ae9d97e7d27ae265eea4f/sample/core/ui/resource/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/TComposableArchitecture/72937912725e1d90741ae9d97e7d27ae265eea4f/sample/core/ui/resource/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/TComposableArchitecture/72937912725e1d90741ae9d97e7d27ae265eea4f/sample/core/ui/resource/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/TComposableArchitecture/72937912725e1d90741ae9d97e7d27ae265eea4f/sample/core/ui/resource/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/TComposableArchitecture/72937912725e1d90741ae9d97e7d27ae265eea4f/sample/core/ui/resource/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/TComposableArchitecture/72937912725e1d90741ae9d97e7d27ae265eea4f/sample/core/ui/resource/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/TComposableArchitecture/72937912725e1d90741ae9d97e7d27ae265eea4f/sample/core/ui/resource/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/taehwandev/TComposableArchitecture/72937912725e1d90741ae9d97e7d27ae265eea4f/sample/core/ui/resource/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | TComposableArchitecture
3 |
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/sample/core/ui/resource/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/sample/feature/detail/detail-api/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sample/feature/detail/detail-api/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.library.feature.compose.api)
3 | }
4 |
5 | setNamespace("sample.feature.detail.api")
6 |
--------------------------------------------------------------------------------
/sample/feature/detail/detail-api/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/sample/feature/detail/detail-api/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sample/feature/detail/detail-api/src/main/java/tech/thdev/composable/architecture/sample/feature/detail/api/DetailActivityRouter.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.detail.api
2 |
3 | import tech.thdev.composable.architecture.router.system.route.ActivityRoute
4 |
5 | interface DetailActivityRouter : ActivityRoute {
6 |
7 | companion object {
8 |
9 | const val PUT_DATA = "put-data"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/sample/feature/detail/detail-api/src/main/java/tech/thdev/composable/architecture/sample/feature/detail/api/model/DetailData.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.detail.api.model
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | @Parcelize
7 | data class DetailData(
8 | val text: String,
9 | ) : Parcelable
10 |
--------------------------------------------------------------------------------
/sample/feature/detail/detail/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sample/feature/detail/detail/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.library.feature.compose)
3 | }
4 |
5 | setNamespace("sample.feature.detail")
6 |
7 | dependencies {
8 | implementation(projects.sample.feature.detail.detailApi)
9 | }
10 |
--------------------------------------------------------------------------------
/sample/feature/detail/detail/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/sample/feature/detail/detail/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
--------------------------------------------------------------------------------
/sample/feature/detail/detail/src/main/java/tech/thdev/composable/architecture/sample/feature/detail/DetailAction.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.detail
2 |
3 | import tech.thdev.composable.architecture.action.system.Action
4 |
5 | internal sealed interface DetailAction : Action {
6 |
7 | data object Task : DetailAction
8 |
9 | data object MoveBack : DetailAction
10 | }
11 |
--------------------------------------------------------------------------------
/sample/feature/detail/detail/src/main/java/tech/thdev/composable/architecture/sample/feature/detail/DetailActivity.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.detail
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.activity.viewModels
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.Icon
12 | import androidx.compose.material3.IconButton
13 | import androidx.compose.material3.Scaffold
14 | import androidx.compose.material3.SnackbarHost
15 | import androidx.compose.material3.SnackbarHostState
16 | import androidx.compose.material3.Text
17 | import androidx.compose.material3.TopAppBar
18 | import androidx.compose.runtime.remember
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.unit.dp
21 | import dagger.hilt.android.AndroidEntryPoint
22 | import tech.thdev.composable.architecture.action.system.compose.ActionSenderCompositionLocalProvider
23 | import tech.thdev.composable.architecture.action.system.compose.LocalActionSenderOwner
24 | import tech.thdev.composable.architecture.action.system.send
25 | import tech.thdev.composable.architecture.alert.system.CaAlertScreen
26 | import tech.thdev.composable.architecture.router.system.LaunchedRouter
27 | import tech.thdev.composable.architecture.sample.feature.detail.compose.DetailScreen
28 | import tech.thdev.composable.architecture.sample.resource.theme.TComposableArchitectureTheme
29 |
30 | @AndroidEntryPoint
31 | class DetailActivity : ComponentActivity() {
32 |
33 | private val detailViewModel by viewModels()
34 |
35 | override fun onCreate(savedInstanceState: Bundle?) {
36 | super.onCreate(savedInstanceState)
37 | setContent {
38 | TComposableArchitectureTheme {
39 | val snackbarHostState = remember { SnackbarHostState() }
40 |
41 | CaAlertScreen(
42 | snackbarHostState = snackbarHostState,
43 | )
44 | LaunchedRouter()
45 |
46 | ActionSenderCompositionLocalProvider(detailViewModel) {
47 | val action = LocalActionSenderOwner.current
48 | Scaffold(
49 | topBar = {
50 | TopAppBar(
51 | navigationIcon = {
52 | IconButton(
53 | onClick = action.send(DetailAction.MoveBack),
54 | ) {
55 | Icon(
56 | imageVector = Icons.AutoMirrored.Filled.ArrowBack,
57 | contentDescription = "back",
58 | )
59 | }
60 | },
61 | title = {
62 | Text("Detail view")
63 | }
64 | )
65 | },
66 | snackbarHost = {
67 | SnackbarHost(hostState = snackbarHostState)
68 | },
69 | modifier = Modifier
70 | .fillMaxSize()
71 | ) { innerPadding ->
72 | DetailScreen(
73 | modifier = Modifier
74 | .padding(innerPadding)
75 | .padding(horizontal = 10.dp)
76 | )
77 | }
78 | }
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/sample/feature/detail/detail/src/main/java/tech/thdev/composable/architecture/sample/feature/detail/DetailActivityRouteImpl.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.detail
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import tech.thdev.composable.architecture.sample.feature.detail.api.DetailActivityRouter
6 | import javax.inject.Inject
7 |
8 | internal class DetailActivityRouteImpl @Inject internal constructor() : DetailActivityRouter {
9 |
10 | override fun getActivity(context: Context): Intent =
11 | Intent(context, DetailActivity::class.java)
12 | }
13 |
--------------------------------------------------------------------------------
/sample/feature/detail/detail/src/main/java/tech/thdev/composable/architecture/sample/feature/detail/DetailViewModel.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.detail
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import dagger.hilt.android.lifecycle.HiltViewModel
5 | import kotlinx.coroutines.flow.MutableStateFlow
6 | import kotlinx.coroutines.flow.asStateFlow
7 | import tech.thdev.composable.architecture.action.system.FlowActionStream
8 | import tech.thdev.composable.architecture.action.system.base.ActionViewModel
9 | import tech.thdev.composable.architecture.router.system.Navigator
10 | import tech.thdev.composable.architecture.sample.feature.detail.api.DetailActivityRouter
11 | import tech.thdev.composable.architecture.sample.feature.detail.api.model.DetailData
12 | import tech.thdev.composable.architecture.sample.feature.detail.model.DetailUiState
13 | import javax.inject.Inject
14 |
15 | @HiltViewModel
16 | internal class DetailViewModel @Inject constructor(
17 | flowActionStream: FlowActionStream,
18 | private val savedStateHandle: SavedStateHandle,
19 | private val navigator: Navigator,
20 | ) : ActionViewModel(flowActionStream, DetailAction::class) {
21 |
22 | private val _detailUiState = MutableStateFlow(DetailUiState.Default)
23 | val detailUiState = _detailUiState.asStateFlow()
24 |
25 | override fun onCreated() {
26 | nextAction(DetailAction.Task)
27 | }
28 |
29 | override suspend fun handleAction(action: DetailAction) {
30 | when (action) {
31 | is DetailAction.Task -> {
32 | _detailUiState.value = DetailUiState(
33 | message = savedStateHandle.get(DetailActivityRouter.PUT_DATA)?.text ?: "",
34 | )
35 | }
36 |
37 | is DetailAction.MoveBack -> {
38 | navigator.navigateBack()
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/sample/feature/detail/detail/src/main/java/tech/thdev/composable/architecture/sample/feature/detail/compose/DetailScreen.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.detail.compose
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.tooling.preview.Preview
13 | import androidx.compose.ui.unit.dp
14 | import androidx.hilt.navigation.compose.hiltViewModel
15 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
16 | import tech.thdev.composable.architecture.sample.feature.detail.DetailViewModel
17 | import tech.thdev.composable.architecture.sample.feature.detail.model.DetailUiState
18 |
19 | @Composable
20 | internal fun DetailScreen(
21 | modifier: Modifier = Modifier,
22 | detailViewModel: DetailViewModel = hiltViewModel(),
23 | ) {
24 | val detailUiState by detailViewModel.detailUiState.collectAsStateWithLifecycle()
25 |
26 | DetailScreen(
27 | detailUiState = detailUiState,
28 | modifier = modifier,
29 | )
30 | }
31 |
32 | @Composable
33 | private fun DetailScreen(
34 | detailUiState: DetailUiState,
35 | modifier: Modifier = Modifier,
36 | ) {
37 | Column(
38 | modifier = modifier
39 | ) {
40 | Text(
41 | text = detailUiState.message,
42 | style = MaterialTheme.typography.bodyLarge,
43 | modifier = Modifier
44 | .fillMaxWidth()
45 | .padding(20.dp)
46 | )
47 | }
48 | }
49 |
50 | @Preview(showBackground = true)
51 | @Composable
52 | private fun PreviewMainScreen() {
53 | DetailScreen(
54 | detailUiState = DetailUiState(
55 | message = "next message",
56 | ),
57 | modifier = Modifier
58 | .fillMaxSize()
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/sample/feature/detail/detail/src/main/java/tech/thdev/composable/architecture/sample/feature/detail/di/DetailModule.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.detail.di
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import dagger.multibindings.IntoMap
8 | import tech.thdev.composable.architecture.router.system.di.RouteKey
9 | import tech.thdev.composable.architecture.router.system.route.ActivityRoute
10 | import tech.thdev.composable.architecture.sample.feature.detail.DetailActivityRouteImpl
11 | import tech.thdev.composable.architecture.sample.feature.detail.api.DetailActivityRouter
12 |
13 | @Module
14 | @InstallIn(SingletonComponent::class)
15 | internal abstract class DetailModule {
16 |
17 | @Binds
18 | @IntoMap
19 | @RouteKey(DetailActivityRouter::class)
20 | abstract fun bindDetailActivityRoute(
21 | mainActivityRoute: DetailActivityRouteImpl,
22 | ): ActivityRoute
23 | }
24 |
--------------------------------------------------------------------------------
/sample/feature/detail/detail/src/main/java/tech/thdev/composable/architecture/sample/feature/detail/model/DetailUiState.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.detail.model
2 |
3 | import androidx.compose.runtime.Immutable
4 |
5 | @Immutable
6 | internal data class DetailUiState(
7 | val message: String,
8 | ) {
9 |
10 | companion object {
11 |
12 | val Default = DetailUiState(
13 | message = "",
14 | )
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/sample/feature/detail/detail/src/test/java/tech/thdev/composable/architecture/sample/feature/detail/DetailViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.detail
2 |
3 | import androidx.lifecycle.SavedStateHandle
4 | import app.cash.turbine.test
5 | import kotlinx.coroutines.channels.BufferOverflow
6 | import kotlinx.coroutines.flow.MutableSharedFlow
7 | import kotlinx.coroutines.flow.flowOf
8 | import kotlinx.coroutines.test.runTest
9 | import org.junit.Assert
10 | import org.junit.Before
11 | import org.junit.Test
12 | import org.mockito.kotlin.mock
13 | import org.mockito.kotlin.verify
14 | import org.mockito.kotlin.whenever
15 | import tech.thdev.composable.architecture.action.system.FlowActionStream
16 | import tech.thdev.composable.architecture.router.system.Navigator
17 | import tech.thdev.composable.architecture.sample.feature.detail.api.DetailActivityRouter
18 | import tech.thdev.composable.architecture.sample.feature.detail.api.model.DetailData
19 | import tech.thdev.composable.architecture.sample.feature.detail.model.DetailUiState
20 |
21 | class DetailViewModelTest {
22 |
23 | private val flowActionStream = mock()
24 | private val savedStateHandle = mock()
25 | private val navigator = mock()
26 |
27 | private val viewModel = DetailViewModel(
28 | flowActionStream = flowActionStream,
29 | savedStateHandle = savedStateHandle,
30 | navigator = navigator,
31 | )
32 |
33 | private val action = MutableSharedFlow(
34 | extraBufferCapacity = 1,
35 | onBufferOverflow = BufferOverflow.DROP_OLDEST,
36 | )
37 |
38 | @Before
39 | fun setUp() {
40 | whenever(flowActionStream.flowAction()).thenReturn(action)
41 | }
42 |
43 | @Test
44 | fun `test initData`() {
45 | Assert.assertEquals(DetailUiState.Default, viewModel.detailUiState.value)
46 | }
47 |
48 | @Test
49 | fun `test Task`() = runTest {
50 | viewModel.flowAction.test {
51 | expectNoEvents()
52 |
53 | // Task 실행
54 | whenever(savedStateHandle.get(DetailActivityRouter.PUT_DATA)).thenReturn(DetailData(text = "message"))
55 |
56 | action.tryEmit(DetailAction.Task)
57 |
58 | val convert = DetailUiState(message = "message")
59 | Assert.assertEquals(convert, viewModel.detailUiState.value)
60 |
61 | verify(savedStateHandle).get(DetailActivityRouter.PUT_DATA)
62 |
63 | cancelAndIgnoreRemainingEvents()
64 | }
65 | }
66 |
67 | @Test
68 | fun `test MoveBack`() = runTest {
69 | val mockItem = DetailAction.MoveBack
70 | whenever(flowActionStream.flowAction()).thenReturn(flowOf(mockItem))
71 |
72 | viewModel.flowAction.test {
73 | Assert.assertEquals(mockItem, awaitItem())
74 | verify(navigator).navigateBack()
75 |
76 | cancelAndIgnoreRemainingEvents()
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/sample/feature/main/main-api/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sample/feature/main/main-api/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.library.feature.compose.api)
3 | }
4 |
5 | setNamespace("sample.feature.main.api")
6 |
--------------------------------------------------------------------------------
/sample/feature/main/main-api/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/sample/feature/main/main-api/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sample/feature/main/main-api/src/main/java/tech/thdev/composable/architecture/sample/feature/main/api/MainActivityRouter.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.api
2 |
3 | import tech.thdev.composable.architecture.router.system.route.ActivityRoute
4 |
5 | interface MainActivityRouter : ActivityRoute
6 |
--------------------------------------------------------------------------------
/sample/feature/main/main/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sample/feature/main/main/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.library.feature.compose)
3 | }
4 |
5 | setNamespace("sample.feature.main")
6 |
7 | dependencies {
8 | implementation(projects.sample.feature.main.mainApi)
9 |
10 | implementation(projects.sample.feature.main.screen.screenNavigation)
11 | implementation(projects.sample.feature.main.screen.screenSearch)
12 | implementation(projects.sample.feature.main.screen.screenSettings)
13 | }
14 |
--------------------------------------------------------------------------------
/sample/feature/main/main/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/sample/feature/main/main/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/sample/feature/main/main/src/main/java/tech/thdev/composable/architecture/sample/feature/main/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.navigation.compose.rememberNavController
7 | import dagger.hilt.android.AndroidEntryPoint
8 | import tech.thdev.composable.architecture.router.system.LaunchedRouter
9 | import tech.thdev.composable.architecture.sample.feature.main.screen.navigation.NavigationScreen
10 |
11 | @AndroidEntryPoint
12 | class MainActivity : ComponentActivity() {
13 |
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 |
17 | setContent {
18 | val navHostController = rememberNavController()
19 | LaunchedRouter(navHostController)
20 | NavigationScreen(navHostController)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/sample/feature/main/main/src/main/java/tech/thdev/composable/architecture/sample/feature/main/MainActivityRouteImpl.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import tech.thdev.composable.architecture.sample.feature.main.api.MainActivityRouter
6 | import javax.inject.Inject
7 |
8 | internal class MainActivityRouteImpl @Inject constructor() : MainActivityRouter {
9 |
10 | override fun getActivity(context: Context): Intent =
11 | Intent(context, MainActivity::class.java)
12 | }
13 |
--------------------------------------------------------------------------------
/sample/feature/main/main/src/main/java/tech/thdev/composable/architecture/sample/feature/main/di/MainModule.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.di
2 |
3 | import dagger.Binds
4 | import dagger.Module
5 | import dagger.hilt.InstallIn
6 | import dagger.hilt.components.SingletonComponent
7 | import dagger.multibindings.IntoMap
8 | import tech.thdev.composable.architecture.router.system.di.RouteKey
9 | import tech.thdev.composable.architecture.router.system.route.ActivityRoute
10 | import tech.thdev.composable.architecture.sample.feature.main.MainActivityRouteImpl
11 | import tech.thdev.composable.architecture.sample.feature.main.api.MainActivityRouter
12 |
13 | @Module
14 | @InstallIn(SingletonComponent::class)
15 | internal abstract class MainModule {
16 |
17 | @Binds
18 | @IntoMap
19 | @RouteKey(MainActivityRouter::class)
20 | abstract fun bindMainActivityRoute(
21 | mainActivityRoute: MainActivityRouteImpl,
22 | ): ActivityRoute
23 | }
24 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-navigation/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-navigation/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.library.feature.compose)
3 | }
4 |
5 | setNamespace("sample.feature.main.screen.navigation")
6 |
7 | dependencies {
8 | implementation(projects.sample.feature.main.screen.screenSearch)
9 | implementation(projects.sample.feature.main.screen.screenSearchApi)
10 | implementation(projects.sample.feature.main.screen.screenSettings)
11 | implementation(projects.sample.feature.main.screen.screenSettingsApi)
12 | }
13 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-navigation/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-navigation/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-navigation/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/navigation/NavigationAction.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.navigation
2 |
3 | import tech.thdev.composable.architecture.action.system.Action
4 | import tech.thdev.composable.architecture.sample.feature.main.screen.navigation.model.NavigationUiState
5 |
6 | internal sealed interface NavigationAction : Action {
7 |
8 | data class SwitchNavigation(val navItem: NavigationUiState.NavItem) : NavigationAction
9 | }
10 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-navigation/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/navigation/NavigationScreen.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.navigation
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.navigation.NavHostController
5 | import tech.thdev.composable.architecture.sample.feature.main.screen.navigation.compose.InternalNavigationScaffold
6 |
7 | @Composable
8 | fun NavigationScreen(
9 | navController: NavHostController,
10 | ) {
11 | InternalNavigationScaffold(
12 | navController = navController,
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-navigation/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/navigation/NavigationViewModel.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.navigation
2 |
3 | import dagger.hilt.android.lifecycle.HiltViewModel
4 | import kotlinx.coroutines.flow.MutableStateFlow
5 | import kotlinx.coroutines.flow.asStateFlow
6 | import kotlinx.coroutines.flow.update
7 | import tech.thdev.composable.architecture.action.system.FlowActionStream
8 | import tech.thdev.composable.architecture.action.system.base.ActionViewModel
9 | import tech.thdev.composable.architecture.router.system.Navigator
10 | import tech.thdev.composable.architecture.sample.feature.main.screen.navigation.model.NavigationUiState
11 | import javax.inject.Inject
12 |
13 | @HiltViewModel
14 | internal class NavigationViewModel @Inject constructor(
15 | flowActionStream: FlowActionStream,
16 | private val navigator: Navigator,
17 | ) : ActionViewModel(flowActionStream, NavigationAction::class) {
18 |
19 | private val _navigationUiState = MutableStateFlow(NavigationUiState.Default)
20 | val navigationUiState = _navigationUiState.asStateFlow()
21 |
22 | override suspend fun handleAction(action: NavigationAction) {
23 | when (action) {
24 | is NavigationAction.SwitchNavigation -> {
25 | navigator.navigate(
26 | navigationRoute = action.navItem.route,
27 | saveState = true,
28 | )
29 |
30 | _navigationUiState.update {
31 | it.copy(
32 | selectNav = action.navItem,
33 | )
34 | }
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-navigation/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/navigation/compose/InternalNavigationScreen.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.navigation.compose
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.material3.Icon
7 | import androidx.compose.material3.NavigationBar
8 | import androidx.compose.material3.NavigationBarItem
9 | import androidx.compose.material3.Scaffold
10 | import androidx.compose.material3.Text
11 | import androidx.compose.material3.TopAppBar
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.getValue
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.res.painterResource
16 | import androidx.compose.ui.tooling.preview.Preview
17 | import androidx.compose.ui.unit.dp
18 | import androidx.hilt.navigation.compose.hiltViewModel
19 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
20 | import androidx.navigation.NavHostController
21 | import androidx.navigation.compose.NavHost
22 | import androidx.navigation.compose.rememberNavController
23 | import tech.thdev.composable.architecture.action.system.compose.ActionSenderCompositionLocalProvider
24 | import tech.thdev.composable.architecture.action.system.compose.LocalActionSenderOwner
25 | import tech.thdev.composable.architecture.sample.feature.main.screen.navigation.NavigationAction
26 | import tech.thdev.composable.architecture.sample.feature.main.screen.navigation.NavigationViewModel
27 | import tech.thdev.composable.architecture.sample.feature.main.screen.navigation.model.NavigationUiState
28 | import tech.thdev.composable.architecture.sample.feature.main.screen.search.searchNavGraph
29 | import tech.thdev.composable.architecture.sample.feature.main.screen.settings.settingsNavGraph
30 |
31 | @Composable
32 | internal fun InternalNavigationScaffold(
33 | navController: NavHostController,
34 | navigationViewModel: NavigationViewModel = hiltViewModel(),
35 | ) {
36 | ActionSenderCompositionLocalProvider(navigationViewModel) {
37 | val actionSender = LocalActionSenderOwner.current
38 | val navigationUiState by navigationViewModel.navigationUiState.collectAsStateWithLifecycle()
39 |
40 | InternalNavigationScreen(
41 | navigationUiState = navigationUiState,
42 | navController = navController,
43 | onClick = { navItem ->
44 | actionSender?.send(NavigationAction.SwitchNavigation(navItem))
45 | },
46 | modifier = Modifier
47 | .fillMaxSize()
48 | )
49 | }
50 | }
51 |
52 | @Composable
53 | private fun InternalNavigationScreen(
54 | navigationUiState: NavigationUiState,
55 | navController: NavHostController,
56 | onClick: (navItem: NavigationUiState.NavItem) -> Unit,
57 | modifier: Modifier = Modifier,
58 | ) {
59 | Scaffold(
60 | topBar = {
61 | TopAppBar(
62 | title = {
63 | Text("Composable Architecture example")
64 | }
65 | )
66 | },
67 | bottomBar = {
68 | NavigationBar {
69 | navigationUiState.navigation.forEach { navItem ->
70 | NavigationBarItem(
71 | selected = navigationUiState.selectNav == navItem,
72 | onClick = {
73 | onClick(navItem)
74 | },
75 | label = {
76 | Text(navItem.title)
77 | },
78 | icon = {
79 | Icon(
80 | painter = painterResource(navItem.icon),
81 | contentDescription = navItem.title,
82 | )
83 | },
84 | )
85 | }
86 | }
87 | },
88 | modifier = modifier
89 | ) { innerPadding ->
90 | Box(
91 | modifier = Modifier
92 | .padding(innerPadding)
93 | .padding(horizontal = 10.dp)
94 | ) {
95 | NavHost(
96 | navController = navController,
97 | startDestination = navigationUiState.selectNav.route,
98 | ) {
99 | searchNavGraph()
100 | settingsNavGraph()
101 | }
102 | }
103 | }
104 | }
105 |
106 | @Preview(showBackground = true)
107 | @Composable
108 | private fun PreviewInternalNavigationScreen() {
109 | InternalNavigationScreen(
110 | navigationUiState = NavigationUiState.Default,
111 | navController = rememberNavController(),
112 | onClick = {},
113 | modifier = Modifier
114 | .fillMaxSize()
115 | )
116 | }
117 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-navigation/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/navigation/model/NavigationUiState.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.navigation.model
2 |
3 | import androidx.compose.runtime.Immutable
4 | import kotlinx.collections.immutable.ImmutableList
5 | import kotlinx.collections.immutable.persistentListOf
6 | import tech.thdev.composable.architecture.router.system.route.NavigationRoute
7 | import tech.thdev.composable.architecture.sample.feature.main.screen.navigation.R
8 | import tech.thdev.composable.architecture.sample.feature.main.screen.search.api.SearchRoute
9 | import tech.thdev.composable.architecture.sample.feature.main.screen.setting.api.SettingsRoute
10 |
11 | @Immutable
12 | internal data class NavigationUiState(
13 | val selectNav: NavItem,
14 | val navigation: ImmutableList,
15 | ) {
16 |
17 | @Immutable
18 | internal sealed class NavItem(
19 | val title: String,
20 | val icon: Int,
21 | val route: NavigationRoute,
22 | ) {
23 |
24 | @Immutable
25 | data object Search : NavItem(
26 | title = "Search",
27 | icon = R.drawable.img_search,
28 | route = SearchRoute,
29 | )
30 |
31 | @Immutable
32 | data object Setting : NavItem(
33 | title = "Setting",
34 | icon = R.drawable.img_settings,
35 | route = SettingsRoute,
36 | )
37 | }
38 |
39 | companion object {
40 |
41 | val Default = NavigationUiState(
42 | selectNav = NavItem.Search,
43 | navigation = persistentListOf(
44 | NavItem.Search,
45 | NavItem.Setting,
46 | ),
47 | )
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-navigation/src/main/res/drawable/img_search.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-navigation/src/main/res/drawable/img_settings.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-navigation/src/test/java/tech/thdev/composable/architecture/sample/feature/main/screen/navigation/NavigationViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.navigation
2 |
3 | import app.cash.turbine.test
4 | import kotlinx.coroutines.flow.flowOf
5 | import kotlinx.coroutines.test.runTest
6 | import org.junit.Assert
7 | import org.junit.Test
8 | import org.mockito.kotlin.mock
9 | import org.mockito.kotlin.verify
10 | import org.mockito.kotlin.whenever
11 | import tech.thdev.composable.architecture.action.system.FlowActionStream
12 | import tech.thdev.composable.architecture.router.system.Navigator
13 | import tech.thdev.composable.architecture.sample.feature.main.screen.navigation.model.NavigationUiState
14 | import tech.thdev.composable.architecture.sample.feature.main.screen.setting.api.SettingsRoute
15 |
16 | internal class NavigationViewModelTest {
17 |
18 | private val flowActionStream = mock()
19 | private val navigator = mock()
20 |
21 | private val viewModel = NavigationViewModel(
22 | flowActionStream = flowActionStream,
23 | navigator = navigator,
24 | )
25 |
26 | @Test
27 | fun `test initData`() {
28 | Assert.assertEquals(NavigationUiState.Default, viewModel.navigationUiState.value)
29 | }
30 |
31 | @Test
32 | fun `test SwitchNavigation`() = runTest {
33 | val mock = NavigationAction.SwitchNavigation(
34 | navItem = NavigationUiState.NavItem.Setting,
35 | )
36 | whenever(flowActionStream.flowAction()).thenReturn(flowOf(mock))
37 |
38 | viewModel.flowAction
39 | .test {
40 | Assert.assertEquals(
41 | NavigationUiState.NavItem.Setting,
42 | viewModel.navigationUiState.value.selectNav
43 | )
44 | verify(navigator).navigate(
45 | navigationRoute = SettingsRoute,
46 | saveState = true,
47 | )
48 |
49 | verify(flowActionStream).flowAction()
50 |
51 | cancelAndIgnoreRemainingEvents()
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-search-api/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-search-api/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.library.feature.compose.api)
3 | }
4 |
5 | setNamespace("sample.feature.main.screen.search.api")
6 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-search-api/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-search-api/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-search-api/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/search/api/SearchRoute.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.search.api
2 |
3 | import kotlinx.serialization.Serializable
4 | import tech.thdev.composable.architecture.router.system.route.NavigationRoute
5 |
6 | @Serializable
7 | object SearchRoute : NavigationRoute
8 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-search/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-search/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.library.feature.compose)
3 | alias(libs.plugins.tech.thdev.android.library.navigation)
4 | }
5 |
6 | setNamespace("sample.feature.main.screen.search")
7 |
8 | dependencies {
9 | implementation(projects.sample.feature.main.screen.screenSearchApi)
10 | implementation(projects.sample.feature.detail.detailApi)
11 | }
12 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-search/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-search/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-search/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/search/MainAction.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.search
2 |
3 | import tech.thdev.composable.architecture.action.system.Action
4 |
5 | sealed interface MainAction : Action {
6 |
7 | data object ShowToast : MainAction
8 |
9 | data class ShowDetail(
10 | val message: String,
11 | ) : MainAction
12 |
13 | data class ShowAlert(
14 | val icon: Int,
15 | val title: String,
16 | val message: String,
17 | val confirmButtonText: String,
18 | val dismissButtonText: String,
19 | ) : MainAction
20 | }
21 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-search/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/search/SearchScreen.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.search
2 |
3 | import androidx.navigation.NavGraphBuilder
4 | import androidx.navigation.compose.composable
5 | import tech.thdev.composable.architecture.sample.feature.main.screen.search.api.SearchRoute
6 | import tech.thdev.composable.architecture.sample.feature.main.screen.search.compose.InternalSearchScreen
7 |
8 | fun NavGraphBuilder.searchNavGraph() {
9 | composable {
10 | InternalSearchScreen()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-search/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/search/SearchSideEffect.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.search
2 |
3 | internal sealed interface SearchSideEffect {
4 |
5 | data object ShowToast : SearchSideEffect
6 | }
7 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-search/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/search/SearchViewModel.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.search
2 |
3 | import dagger.hilt.android.lifecycle.HiltViewModel
4 | import kotlinx.coroutines.channels.Channel
5 | import kotlinx.coroutines.flow.receiveAsFlow
6 | import tech.thdev.composable.architecture.action.system.FlowActionStream
7 | import tech.thdev.composable.architecture.action.system.base.ActionViewModel
8 | import tech.thdev.composable.architecture.router.system.Navigator
9 | import tech.thdev.composable.architecture.sample.feature.detail.api.DetailActivityRouter
10 | import tech.thdev.composable.architecture.sample.feature.detail.api.model.DetailData
11 | import javax.inject.Inject
12 |
13 | @HiltViewModel
14 | internal class SearchViewModel @Inject constructor(
15 | flowActionStream: FlowActionStream,
16 | private val navigator: Navigator,
17 | ) : ActionViewModel(flowActionStream, MainAction::class) {
18 |
19 | private val _sideEffect = Channel(Channel.BUFFERED)
20 | val sideEffect = _sideEffect.receiveAsFlow()
21 |
22 | override suspend fun handleAction(action: MainAction) {
23 | when (action) {
24 | is MainAction.ShowToast -> {
25 | _sideEffect.send(SearchSideEffect.ShowToast)
26 | }
27 |
28 | is MainAction.ShowAlert -> {
29 | // nextAction( // todo
30 | // CaAlertAction.ShowDialog(
31 | // icon = action.icon,
32 | // title = action.title,
33 | // message = action.message,
34 | // confirmButtonText = action.confirmButtonText,
35 | // onConfirmButtonAction = CaAlertAction.ShowSnack(
36 | // message = "Confirm",
37 | // ),
38 | // dismissButtonText = action.dismissButtonText,
39 | // onDismissButtonAction = CaAlertAction.ShowSnack(
40 | // message = "Dismiss",
41 | // ),
42 | // )
43 | // )
44 | }
45 |
46 | is MainAction.ShowDetail -> {
47 | navigator.navigate(
48 | activityRoute = DetailActivityRouter::class,
49 | argumentMap = mapOf(
50 | DetailActivityRouter.PUT_DATA to DetailData(text = action.message),
51 | ),
52 | )
53 | }
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-search/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/search/compose/InternalSearchScreen.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.search.compose
2 |
3 | import android.widget.Toast
4 | import androidx.activity.compose.LocalActivity
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.material3.Button
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.tooling.preview.Preview
13 | import androidx.compose.ui.unit.dp
14 | import androidx.hilt.navigation.compose.hiltViewModel
15 | import tech.thdev.composable.architecture.action.system.compose.ActionSenderCompositionLocalProvider
16 | import tech.thdev.composable.architecture.action.system.compose.LocalActionSenderOwner
17 | import tech.thdev.composable.architecture.action.system.lifecycle.collectLifecycleEvent
18 | import tech.thdev.composable.architecture.action.system.send
19 | import tech.thdev.composable.architecture.sample.feature.main.screen.search.MainAction
20 | import tech.thdev.composable.architecture.sample.feature.main.screen.search.SearchSideEffect
21 | import tech.thdev.composable.architecture.sample.feature.main.screen.search.SearchViewModel
22 | import tech.thdev.composable.architecture.sample.resource.R
23 |
24 | @Composable
25 | internal fun InternalSearchScreen(
26 | searchViewModel: SearchViewModel = hiltViewModel(),
27 | ) {
28 | ActionSenderCompositionLocalProvider(searchViewModel) {
29 | val activity = LocalActivity.current
30 | searchViewModel.sideEffect.collectLifecycleEvent {
31 | when (it) {
32 | SearchSideEffect.ShowToast -> {
33 | Toast.makeText(activity, "message", Toast.LENGTH_SHORT).show()
34 | }
35 | }
36 | }
37 |
38 | InternalSearchScreen(
39 | modifier = Modifier
40 | .padding(horizontal = 10.dp)
41 | )
42 | }
43 | }
44 |
45 | @Composable
46 | private fun InternalSearchScreen(
47 | modifier: Modifier = Modifier,
48 | ) {
49 | val actionSender = LocalActionSenderOwner.current
50 |
51 | Column(
52 | modifier = modifier
53 | ) {
54 | Button(
55 | onClick = actionSender.send(MainAction.ShowToast),
56 | ) {
57 | Text(
58 | text = "ShowToast",
59 | )
60 | }
61 |
62 | Button(
63 | onClick = actionSender.send(
64 | MainAction.ShowAlert(
65 | icon = R.drawable.baseline_info_24,
66 | title = "Info",
67 | message = "A dialog is a type of modal window that appears in front of app content to provide critical" +
68 | "information, or ask for a decision to be made.",
69 | confirmButtonText = "Confirm",
70 | dismissButtonText = "Dismiss",
71 | )
72 | ),
73 | ) {
74 | Text(
75 | text = "ShowAlert",
76 | )
77 | }
78 |
79 | Button(
80 | onClick = actionSender.send(MainAction.ShowDetail(message = "Show detail activity")),
81 | ) {
82 | Text(
83 | text = "Show Detail activity",
84 | )
85 | }
86 | }
87 | }
88 |
89 | @Preview(showBackground = true)
90 | @Composable
91 | private fun PreviewInternalSearchScreen() {
92 | InternalSearchScreen(
93 | modifier = Modifier
94 | .fillMaxSize()
95 | )
96 | }
97 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-search/src/test/java/tech/thdev/composable/architecture/sample/feature/main/FlowCacheTestUtil.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.Job
5 | import kotlinx.coroutines.cancel
6 | import kotlinx.coroutines.flow.Flow
7 | import kotlinx.coroutines.flow.first
8 | import kotlinx.coroutines.launch
9 | import kotlinx.coroutines.test.UnconfinedTestDispatcher
10 |
11 | internal fun Flow.awaitTest(onCallback: (item: T) -> Unit): Job =
12 | CoroutineScope(UnconfinedTestDispatcher()).launch {
13 | first().also {
14 | onCallback(it)
15 | cancel()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-search/src/test/java/tech/thdev/composable/architecture/sample/feature/main/screen/search/SearchViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.search
2 |
3 | import app.cash.turbine.test
4 | import kotlinx.coroutines.flow.flowOf
5 | import kotlinx.coroutines.test.runTest
6 | import org.junit.Assert
7 | import org.junit.Test
8 | import org.mockito.kotlin.mock
9 | import org.mockito.kotlin.verify
10 | import org.mockito.kotlin.whenever
11 | import tech.thdev.composable.architecture.action.system.FlowActionStream
12 | import tech.thdev.composable.architecture.router.system.Navigator
13 | import tech.thdev.composable.architecture.sample.feature.detail.api.DetailActivityRouter
14 | import tech.thdev.composable.architecture.sample.feature.detail.api.model.DetailData
15 | import tech.thdev.composable.architecture.sample.feature.main.awaitTest
16 | import tech.thdev.composable.architecture.sample.resource.R
17 |
18 | internal class SearchViewModelTest {
19 |
20 | private val flowActionStream = mock()
21 | private val navigator = mock()
22 |
23 | private val viewModel = SearchViewModel(
24 | flowActionStream = flowActionStream,
25 | navigator = navigator,
26 | )
27 |
28 | @Test
29 | fun `test ShowToast`() = runTest {
30 | viewModel.sideEffect.awaitTest {
31 | Assert.assertEquals(SearchSideEffect.ShowToast, it)
32 | }
33 |
34 | val mockItem = MainAction.ShowToast
35 | whenever(flowActionStream.flowAction()).thenReturn(flowOf(mockItem))
36 |
37 | viewModel.flowAction.test {
38 | Assert.assertEquals(mockItem, awaitItem())
39 |
40 | cancelAndIgnoreRemainingEvents()
41 | }
42 | }
43 |
44 | @Test
45 | fun `test ShowAlert`() = runTest {
46 | val mockItem = MainAction.ShowAlert(
47 | icon = R.drawable.baseline_info_24,
48 | title = "title",
49 | message = "message",
50 | confirmButtonText = "confirmButtonText",
51 | dismissButtonText = "dismissButtonText",
52 | )
53 | whenever(flowActionStream.flowAction()).thenReturn(flowOf(mockItem))
54 |
55 | viewModel.flowAction.test {
56 | Assert.assertEquals(mockItem, awaitItem())
57 |
58 | // verify(flowActionStream).nextAction(
59 | // CaAlertAction.ShowDialog(
60 | // icon = R.drawable.baseline_info_24,
61 | // title = "title",
62 | // message = "message",
63 | // confirmButtonText = "confirmButtonText",
64 | // onConfirmButtonAction = CaAlertAction.ShowSnack(
65 | // message = "Confirm",
66 | // ),
67 | // dismissButtonText = "dismissButtonText",
68 | // onDismissButtonAction = CaAlertAction.ShowSnack(
69 | // message = "Dismiss",
70 | // ),
71 | // )
72 | // )
73 |
74 | cancelAndIgnoreRemainingEvents()
75 | }
76 | }
77 |
78 | @Test
79 | fun `test ShowDetail`() = runTest {
80 | val mockItem = MainAction.ShowDetail(
81 | message = "Show detail activity",
82 | )
83 | whenever(flowActionStream.flowAction()).thenReturn(flowOf(mockItem))
84 |
85 | viewModel.flowAction.test {
86 | Assert.assertEquals(mockItem, awaitItem())
87 |
88 | verify(navigator).navigate(
89 | activityRoute = DetailActivityRouter::class,
90 | argumentMap = mapOf(
91 | DetailActivityRouter.PUT_DATA to DetailData(text = "Show detail activity")
92 | ),
93 | )
94 |
95 | cancelAndIgnoreRemainingEvents()
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings-api/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings-api/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.library.feature.compose.api)
3 | }
4 |
5 | setNamespace("sample.feature.main.screen.settings.api")
6 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings-api/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings-api/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings-api/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/setting/api/SettingsRoute.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.setting.api
2 |
3 | import kotlinx.serialization.Serializable
4 | import tech.thdev.composable.architecture.router.system.route.NavigationRoute
5 |
6 | @Serializable
7 | object SettingsRoute : NavigationRoute
8 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.tech.thdev.android.library.feature.compose)
3 | }
4 |
5 | setNamespace("sample.feature.main.screen.settings")
6 |
7 | dependencies {
8 | implementation(projects.sample.feature.main.screen.screenSettingsApi)
9 | }
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/settings/SettingsAction.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.settings
2 |
3 | import tech.thdev.composable.architecture.action.system.Action
4 | import tech.thdev.composable.architecture.sample.feature.main.screen.settings.model.SettingsUiState
5 |
6 | internal sealed interface SettingsAction : Action {
7 |
8 | data class ThemeModeChange(
9 | val mode: SettingsUiState.Mode,
10 | ) : SettingsAction
11 | }
12 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/settings/SettingsScreen.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.settings
2 |
3 | import androidx.navigation.NavGraphBuilder
4 | import androidx.navigation.compose.composable
5 | import tech.thdev.composable.architecture.sample.feature.main.screen.setting.api.SettingsRoute
6 | import tech.thdev.composable.architecture.sample.feature.main.screen.settings.compose.InternalSettingsScreen
7 |
8 | fun NavGraphBuilder.settingsNavGraph() {
9 | composable {
10 | InternalSettingsScreen()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/settings/SettingsSideEffect.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.settings
2 |
3 | sealed interface SettingsSideEffect {
4 |
5 | data object ShowToast : SettingsSideEffect
6 | }
7 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/settings/SettingsViewModel.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.settings
2 |
3 | import dagger.hilt.android.lifecycle.HiltViewModel
4 | import kotlinx.coroutines.flow.MutableStateFlow
5 | import kotlinx.coroutines.flow.asStateFlow
6 | import kotlinx.coroutines.flow.update
7 | import tech.thdev.composable.architecture.action.system.FlowActionStream
8 | import tech.thdev.composable.architecture.action.system.base.ActionViewModel
9 | import tech.thdev.composable.architecture.sample.feature.main.screen.settings.model.SettingsUiState
10 | import javax.inject.Inject
11 |
12 | @HiltViewModel
13 | internal class SettingsViewModel @Inject constructor(
14 | flowActionStream: FlowActionStream,
15 | ) : ActionViewModel(flowActionStream, SettingsAction::class) {
16 |
17 | private val _settingsUiState = MutableStateFlow(SettingsUiState.Default)
18 | val settingsUiState = _settingsUiState.asStateFlow()
19 |
20 | override suspend fun handleAction(action: SettingsAction) {
21 | when (action) {
22 | is SettingsAction.ThemeModeChange -> {
23 | _settingsUiState.update {
24 | it.copy(
25 | mode = action.mode,
26 | )
27 | }
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/settings/compose/InternalSettingsScreen.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.settings.compose
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.runtime.setValue
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.tooling.preview.Preview
14 | import androidx.compose.ui.unit.dp
15 | import androidx.hilt.navigation.compose.hiltViewModel
16 | import androidx.lifecycle.compose.collectAsStateWithLifecycle
17 | import tech.thdev.composable.architecture.action.system.compose.ActionSenderCompositionLocalProvider
18 | import tech.thdev.composable.architecture.sample.feature.main.screen.settings.SettingsViewModel
19 | import tech.thdev.composable.architecture.sample.feature.main.screen.settings.compose.component.ThemeModeSelectBox
20 | import tech.thdev.composable.architecture.sample.feature.main.screen.settings.model.SettingsUiState
21 |
22 | @Composable
23 | internal fun InternalSettingsScreen(
24 | settingsViewModel: SettingsViewModel = hiltViewModel(),
25 | ) {
26 | ActionSenderCompositionLocalProvider(settingsViewModel) {
27 | val settingsUiState by settingsViewModel.settingsUiState.collectAsStateWithLifecycle()
28 |
29 | InternalSettingsScreen(
30 | onThemeModeSelectBox = { modifier ->
31 | ThemeModeSelectBox(
32 | mode = settingsUiState.mode,
33 | modifier = modifier
34 | )
35 | },
36 | )
37 | }
38 | }
39 |
40 | @Composable
41 | private fun InternalSettingsScreen(
42 | onThemeModeSelectBox: @Composable (modifier: Modifier) -> Unit,
43 | modifier: Modifier = Modifier,
44 | ) {
45 | Column(
46 | modifier = modifier
47 | .fillMaxSize()
48 | ) {
49 | onThemeModeSelectBox(
50 | Modifier
51 | .fillMaxWidth()
52 | .padding(20.dp)
53 | )
54 | }
55 | }
56 |
57 | @Preview(showBackground = true)
58 | @Composable
59 | private fun PreviewInternalSettingsScreen() {
60 | var uiState by remember { mutableStateOf(SettingsUiState.Default) }
61 | InternalSettingsScreen(
62 | onThemeModeSelectBox = { modifier ->
63 | ThemeModeSelectBox(
64 | mode = uiState.mode,
65 | onClick = { mode ->
66 | uiState = SettingsUiState(mode = mode)
67 | },
68 | modifier = modifier
69 | )
70 | },
71 | modifier = Modifier
72 | .fillMaxSize()
73 | )
74 | }
75 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/settings/compose/component/DarkOnOffComponent.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.settings.compose.component
2 |
3 | import androidx.compose.foundation.BorderStroke
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.shape.RoundedCornerShape
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.RadioButton
12 | import androidx.compose.material3.Surface
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.graphics.Color
18 | import androidx.compose.ui.res.painterResource
19 | import androidx.compose.ui.tooling.preview.Preview
20 | import androidx.compose.ui.unit.dp
21 | import tech.thdev.composable.architecture.sample.feature.main.screen.settings.R
22 |
23 | @Composable
24 | internal fun DarkOnOff(
25 | selected: Boolean,
26 | title: String,
27 | res: Int,
28 | onClick: () -> Unit,
29 | modifier: Modifier = Modifier,
30 | ) {
31 | Surface(
32 | onClick = onClick,
33 | shape = RoundedCornerShape(12.dp),
34 | border = BorderStroke(1.dp, Color.DarkGray).takeIf { selected },
35 | modifier = modifier
36 | ) {
37 | Column(
38 | verticalArrangement = Arrangement.spacedBy(8.dp),
39 | horizontalAlignment = Alignment.CenterHorizontally,
40 | modifier = Modifier
41 | .padding(12.dp)
42 | ) {
43 | Image(
44 | painter = painterResource(res),
45 | contentDescription = null,
46 | )
47 |
48 | Text(
49 | text = title,
50 | style = MaterialTheme.typography.bodyLarge,
51 | )
52 |
53 | RadioButton(
54 | selected = selected,
55 | onClick = onClick,
56 | )
57 | }
58 | }
59 | }
60 |
61 | @Preview(
62 | showBackground = true,
63 | )
64 | @Composable
65 | private fun PreviewDarkOnOff() {
66 | Row {
67 | DarkOnOff(
68 | selected = true,
69 | title = "Auto",
70 | onClick = {},
71 | res = R.drawable.img_dark_auto,
72 | modifier = Modifier
73 | .weight(1f)
74 | )
75 | DarkOnOff(
76 | selected = false,
77 | title = "Dark",
78 | onClick = {},
79 | res = R.drawable.img_dark_on,
80 | modifier = Modifier
81 | .weight(1f)
82 | )
83 | DarkOnOff(
84 | selected = false,
85 | title = "Light",
86 | onClick = {},
87 | res = R.drawable.img_dark_off,
88 | modifier = Modifier
89 | .weight(1f)
90 | )
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/settings/compose/component/ThemeModeSelectBoxComponent.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.settings.compose.component
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.Card
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.getValue
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.compose.runtime.remember
12 | import androidx.compose.runtime.rememberUpdatedState
13 | import androidx.compose.runtime.setValue
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.tooling.preview.Preview
16 | import androidx.compose.ui.unit.dp
17 | import tech.thdev.composable.architecture.action.system.compose.LocalActionSenderOwner
18 | import tech.thdev.composable.architecture.sample.feature.main.screen.settings.R
19 | import tech.thdev.composable.architecture.sample.feature.main.screen.settings.SettingsAction
20 | import tech.thdev.composable.architecture.sample.feature.main.screen.settings.model.SettingsUiState
21 |
22 | @Composable
23 | internal fun ThemeModeSelectBox(
24 | mode: SettingsUiState.Mode,
25 | modifier: Modifier = Modifier,
26 | ) {
27 | val actionSender = LocalActionSenderOwner.current
28 |
29 | ThemeModeSelectBox(
30 | mode = mode,
31 | onClick = {
32 | actionSender?.send(SettingsAction.ThemeModeChange(it))
33 | },
34 | modifier = modifier
35 | )
36 | }
37 |
38 | @Composable
39 | internal fun ThemeModeSelectBox(
40 | mode: SettingsUiState.Mode,
41 | onClick: (mode: SettingsUiState.Mode) -> Unit,
42 | modifier: Modifier = Modifier,
43 | ) {
44 | Card(
45 | modifier = modifier
46 | .fillMaxWidth()
47 | .padding(horizontal = 20.dp, vertical = 20.dp)
48 | ) {
49 | Row(
50 | horizontalArrangement = Arrangement.spacedBy(12.dp),
51 | modifier = Modifier
52 | .fillMaxWidth()
53 | .padding(12.dp)
54 | ) {
55 | DarkOnOff(
56 | selected = rememberUpdatedState(mode == SettingsUiState.Mode.AUTO).value,
57 | title = "Auto",
58 | onClick = {
59 | onClick(SettingsUiState.Mode.AUTO)
60 | },
61 | res = R.drawable.img_dark_auto,
62 | modifier = Modifier
63 | .weight(1f)
64 | )
65 | DarkOnOff(
66 | selected = rememberUpdatedState(mode == SettingsUiState.Mode.DARK).value,
67 | title = "Dark",
68 | onClick = {
69 | onClick(SettingsUiState.Mode.DARK)
70 | },
71 | res = R.drawable.img_dark_on,
72 | modifier = Modifier
73 | .weight(1f)
74 | )
75 | DarkOnOff(
76 | selected = rememberUpdatedState(mode == SettingsUiState.Mode.LIGHT).value,
77 | title = "Light",
78 | onClick = {
79 | onClick(SettingsUiState.Mode.LIGHT)
80 | },
81 | res = R.drawable.img_dark_off,
82 | modifier = Modifier
83 | .weight(1f)
84 | )
85 | }
86 | }
87 | }
88 |
89 | @Preview(
90 | showBackground = true,
91 | )
92 | @Composable
93 | private fun PreviewThemeModeSelectBox() {
94 | var mode by remember { mutableStateOf(SettingsUiState.Mode.AUTO) }
95 | ThemeModeSelectBox(
96 | mode = mode,
97 | onClick = {
98 | mode = it
99 | },
100 | modifier = Modifier
101 | .fillMaxWidth()
102 | )
103 | }
104 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings/src/main/java/tech/thdev/composable/architecture/sample/feature/main/screen/settings/model/SettingsUiState.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.settings.model
2 |
3 | import androidx.compose.runtime.Immutable
4 |
5 | @Immutable
6 | internal data class SettingsUiState(
7 | val mode: Mode,
8 | ) {
9 |
10 | enum class Mode {
11 | LIGHT,
12 | DARK,
13 | AUTO,
14 | }
15 |
16 | companion object {
17 |
18 | val Default = SettingsUiState(
19 | mode = Mode.AUTO,
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings/src/main/res/drawable/img_dark_auto.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings/src/main/res/drawable/img_dark_off.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings/src/main/res/drawable/img_dark_on.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/sample/feature/main/screen/screen-settings/src/test/java/tech/thdev/composable/architecture/sample/feature/main/screen/settings/SettingsViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package tech.thdev.composable.architecture.sample.feature.main.screen.settings
2 |
3 | import app.cash.turbine.test
4 | import kotlinx.coroutines.flow.flowOf
5 | import kotlinx.coroutines.test.runTest
6 | import org.junit.Assert
7 | import org.junit.Test
8 | import org.mockito.kotlin.mock
9 | import org.mockito.kotlin.verify
10 | import org.mockito.kotlin.whenever
11 | import tech.thdev.composable.architecture.action.system.FlowActionStream
12 | import tech.thdev.composable.architecture.sample.feature.main.screen.settings.model.SettingsUiState
13 |
14 | internal class SettingsViewModelTest {
15 |
16 | private val flowActionStream = mock()
17 |
18 | private val viewModel = SettingsViewModel(
19 | flowActionStream = flowActionStream,
20 | )
21 |
22 | @Test
23 | fun `test initData`() {
24 | Assert.assertEquals(SettingsUiState.Default, viewModel.settingsUiState.value)
25 | }
26 |
27 | @Test
28 | fun `test ThemeModeChange`() = runTest {
29 | val mockItem = SettingsAction.ThemeModeChange(mode = SettingsUiState.Mode.DARK)
30 | whenever(flowActionStream.flowAction()).thenReturn(flowOf(mockItem))
31 |
32 | viewModel.flowAction
33 | .test {
34 | Assert.assertEquals(
35 | SettingsAction.ThemeModeChange(mode = SettingsUiState.Mode.DARK),
36 | awaitItem()
37 | )
38 |
39 | Assert.assertEquals(
40 | SettingsUiState(
41 | mode = SettingsUiState.Mode.DARK,
42 | ),
43 | viewModel.settingsUiState.value
44 | )
45 |
46 | verify(flowActionStream).flowAction()
47 |
48 | cancelAndIgnoreRemainingEvents()
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | pluginManagement {
4 | includeBuild("build-logic")
5 | repositories {
6 | google {
7 | content {
8 | includeGroupByRegex("com\\.android.*")
9 | includeGroupByRegex("com\\.google.*")
10 | includeGroupByRegex("androidx.*")
11 | }
12 | }
13 | mavenCentral()
14 | gradlePluginPortal()
15 | }
16 | }
17 | dependencyResolutionManagement {
18 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
19 | repositories {
20 | google()
21 | mavenCentral()
22 | }
23 | }
24 |
25 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
26 |
27 | rootProject.name = "TComposableArchitecture"
28 |
29 | include(":app")
30 |
31 | // core
32 | include(
33 | ":core:ui:composable-architecture-router-system",
34 | ":core:ui:composable-architecture-system",
35 | ":core:ui:composable-architecture-alert-system",
36 | )
37 |
38 | include(
39 | ":sample:core:ui:resource",
40 | )
41 |
42 | include(
43 | ":sample:feature:detail:detail",
44 | ":sample:feature:detail:detail-api",
45 | ":sample:feature:main:main",
46 | ":sample:feature:main:main-api",
47 | )
48 |
49 | include(
50 | ":sample:feature:main:screen:screen-navigation",
51 | ":sample:feature:main:screen:screen-search",
52 | ":sample:feature:main:screen:screen-search-api",
53 | ":sample:feature:main:screen:screen-settings",
54 | ":sample:feature:main:screen:screen-settings-api",
55 | )
56 |
--------------------------------------------------------------------------------
/version.properties:
--------------------------------------------------------------------------------
1 | majorVersion=25
2 | minorVersion=2
3 | patchVersion=0
4 | versionCode=1
--------------------------------------------------------------------------------