├── .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 |