diff --git a/.gitignore b/.gitignore index 5429390..4ac0ff3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ *.iml .gradle .idea +.kotlin +.fleet .DS_Store build captures diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 390f5a8..fea293c 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -7,8 +7,8 @@ plugins { id(libs.plugins.kotlin.android.get().pluginId) id(libs.plugins.conventions.lint.get().pluginId) - alias(libs.plugins.compose.compiler) alias(libs.plugins.androidx.baselineprofile) + alias(libs.plugins.compose.compiler) // TODO enable after providing google-services.json // alias(libs.plugins.google.services) alias(libs.plugins.firebase.distribution) @@ -103,7 +103,7 @@ dependencies { implementation(projects.shared.app) implementation(projects.shared.feature) implementation(projects.shared.platform) - implementation(projects.shared.util) + implementation(projects.shared.util.tools) implementation(projects.shared.resources) implementation(platform(libs.androidx.compose.bom)) diff --git a/androidApp/src/main/kotlin/app/futured/kmptemplate/android/tools/arch/EventsEffect.kt b/androidApp/src/main/kotlin/app/futured/kmptemplate/android/tools/arch/EventsEffect.kt index 964f022..06a9a01 100644 --- a/androidApp/src/main/kotlin/app/futured/kmptemplate/android/tools/arch/EventsEffect.kt +++ b/androidApp/src/main/kotlin/app/futured/kmptemplate/android/tools/arch/EventsEffect.kt @@ -2,7 +2,7 @@ package app.futured.kmptemplate.android.tools.arch import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import app.futured.kmptemplate.util.arch.UiEvent +import app.futured.kmptemplate.util.arch.Event import kotlinx.coroutines.flow.Flow /** @@ -24,7 +24,7 @@ import kotlinx.coroutines.flow.Flow * @param observer Event receiver lambda. */ @Composable -inline fun > EventsEffect( +inline fun > EventsEffect( eventsFlow: Flow, crossinline observer: suspend E.() -> Unit, ) { @@ -35,7 +35,7 @@ inline fun > EventsEffect( } } -inline fun > UiEvent<*>.onEvent(action: (E) -> Unit) { +inline fun > Event<*>.onEvent(action: (E) -> Unit) { if (this is E) { action(this) } diff --git a/androidApp/src/main/kotlin/app/futured/kmptemplate/android/ui/screen/FirstScreenUi.kt b/androidApp/src/main/kotlin/app/futured/kmptemplate/android/ui/screen/FirstScreenUi.kt index 5c7fdff..951a9c1 100644 --- a/androidApp/src/main/kotlin/app/futured/kmptemplate/android/ui/screen/FirstScreenUi.kt +++ b/androidApp/src/main/kotlin/app/futured/kmptemplate/android/ui/screen/FirstScreenUi.kt @@ -34,7 +34,7 @@ import app.futured.kmptemplate.android.MyApplicationTheme import app.futured.kmptemplate.android.tools.arch.EventsEffect import app.futured.kmptemplate.android.tools.arch.onEvent import app.futured.kmptemplate.feature.ui.first.FirstScreen -import app.futured.kmptemplate.feature.ui.first.FirstUiEvent +import app.futured.kmptemplate.feature.ui.first.FirstEvent import app.futured.kmptemplate.feature.ui.first.FirstViewState import app.futured.kmptemplate.resources.MR import app.futured.kmptemplate.resources.kmpStringResource @@ -53,7 +53,7 @@ fun FirstScreenUi( Content(viewState = viewState, actions = actions, modifier = modifier) EventsEffect(eventsFlow = screen.events) { - onEvent { event -> + onEvent { event -> Toast.makeText(context, event.text, Toast.LENGTH_SHORT).show() } } diff --git a/buildSrc/src/main/kotlin/app/futured/kmptemplate/gradle/ext/KotlinTargetExt.kt b/buildSrc/src/main/kotlin/app/futured/kmptemplate/gradle/ext/KotlinTargetExt.kt index 2f6c085..d300cff 100644 --- a/buildSrc/src/main/kotlin/app/futured/kmptemplate/gradle/ext/KotlinTargetExt.kt +++ b/buildSrc/src/main/kotlin/app/futured/kmptemplate/gradle/ext/KotlinTargetExt.kt @@ -1,12 +1,12 @@ package app.futured.kmptemplate.gradle.ext -import org.jetbrains.kotlin.gradle.dsl.KotlinTargetContainerWithNativeShortcuts +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget /** * Configures supported native iOS targets all at once. */ -fun KotlinTargetContainerWithNativeShortcuts.iosTargets( +fun KotlinMultiplatformExtension.iosTargets( targets: List = listOf( iosX64(), iosArm64(), diff --git a/convention-plugins/src/main/kotlin/koin-annotations.gradle.kts b/convention-plugins/src/main/kotlin/koin-annotations.gradle.kts index 7eebb9c..e5b2fb3 100644 --- a/convention-plugins/src/main/kotlin/koin-annotations.gradle.kts +++ b/convention-plugins/src/main/kotlin/koin-annotations.gradle.kts @@ -12,6 +12,10 @@ ksp { arg("KOIN_CONFIG_CHECK","false") // disable default module generation arg("KOIN_DEFAULT_MODULE","false") + + // setup for component processor + arg("appComponentContext", "app.futured.kmptemplate.util.arch.AppComponentContext") + arg("viewModelExt", "app.futured.kmptemplate.util.arch.viewModel") } // https://github.com/gradle/gradle/issues/15383 @@ -19,6 +23,7 @@ val libs = the() // Enable source generation by KSP to commonMain only dependencies { add("kspCommonMainMetadata", libs.koin.ksp.compiler) + add("kspCommonMainMetadata", project(":shared:util:component-processor")) // DO NOT add bellow dependencies // add("kspAndroid", Deps.Koin.kspCompiler) // add("kspIosX64", Deps.Koin.kspCompiler) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 20036a2..06a96b3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,6 +40,7 @@ composeRuntime = "1.6.11" dokkaVersion = "1.9.20" google-servicesPlugin = "4.4.2" google-firebaseAppDistributionPlugin = "5.0.0" +poet = "1.18.1" # Android Namespaces project-android-namespace = "app.futured.kmptemplate.android" @@ -133,6 +134,10 @@ network-ktor-client-engine-darwin = { module = "io.ktor:ktor-client-darwin", ver network-ktor-http = { module = "io.ktor:ktor-http", version.ref = "ktor" } network-ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } +# KSP api +ksp-lib = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp"} +poet-interop = { module = "com.squareup:kotlinpoet-ksp", version.ref = "poet"} + # Datastore androidx-datastore-preferences-core = { group = "androidx.datastore", name = "datastore-preferences-core", version.ref = "datastore" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2617362..8d9d3f8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +# https://kotlinlang.org/docs/multiplatform-compatibility-guide.html#version-compatibility +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/init_template.kts b/init_template.kts index 92da0f8..e0028d4 100755 --- a/init_template.kts +++ b/init_template.kts @@ -171,7 +171,9 @@ fun renamePackagesInShared(packageName: String) { "persistence", "platform", "resources", - "util", + "util/tools", + "util/component-annotation", + "util/component-processor", ) modules.forEach { moduleName -> sourceSets.forEach { targetName -> diff --git a/iosApp/iosApp/Views/Screen/First/FirstViewModel.swift b/iosApp/iosApp/Views/Screen/First/FirstViewModel.swift index aa5aece..33077bd 100644 --- a/iosApp/iosApp/Views/Screen/First/FirstViewModel.swift +++ b/iosApp/iosApp/Views/Screen/First/FirstViewModel.swift @@ -3,20 +3,20 @@ import SwiftUI protocol FirstViewModelProtocol: DynamicProperty { var text: String { get } - var events: SkieSwiftFlow { get } + var events: SkieSwiftFlow { get } var isAlertVisible: Binding { get } var alertText: String { get } func onNext() func onBack() - func showToast(event: FirstUiEvent.ShowToast) + func showToast(event: FirstEvent.ShowToast) func hideToast() } struct FirstViewModel { @StateObject @KotlinStateFlow private var viewState: FirstViewState private let actions: FirstScreenActions - let events: SkieSwiftFlow + let events: SkieSwiftFlow @State private var alertVisible: Bool = false @State private(set) var alertText: String = "" @@ -48,7 +48,7 @@ extension FirstViewModel: FirstViewModelProtocol { actions.onBack() } - func showToast(event: FirstUiEvent.ShowToast) { + func showToast(event: FirstEvent.ShowToast) { alertText = event.text alertVisible = true } diff --git a/settings.gradle.kts b/settings.gradle.kts index f261aff..28f5d02 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,7 +25,9 @@ include(":shared:network:rest") include(":shared:feature") include(":shared:persistence") include(":shared:platform") -include(":shared:util") +include(":shared:util:tools") +include(":shared:util:component-annotation") +include(":shared:util:component-processor") include(":shared:resources") includeBuild("convention-plugins") diff --git a/shared/app/build.gradle.kts b/shared/app/build.gradle.kts index df7e779..2677248 100644 --- a/shared/app/build.gradle.kts +++ b/shared/app/build.gradle.kts @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalKotlinGradlePluginApi::class) + import app.futured.kmptemplate.gradle.configuration.ProjectSettings import app.futured.kmptemplate.gradle.ext.iosTargets import co.touchlab.skie.configuration.DefaultArgumentInterop @@ -7,6 +9,7 @@ import co.touchlab.skie.configuration.SealedInterop import co.touchlab.skie.configuration.SuppressSkieWarning import co.touchlab.skie.configuration.SuspendInterop import dev.icerock.gradle.MRVisibility +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { id(libs.plugins.com.android.library.get().pluginId) @@ -35,7 +38,7 @@ kotlin { isStatic = true export(projects.shared.platform) - export(projects.shared.util) + export(projects.shared.util.tools) export(projects.shared.feature) export(projects.shared.resources) @@ -74,7 +77,7 @@ kotlin { iosMain { dependencies { api(projects.shared.platform) - api(projects.shared.util) + api(projects.shared.util.tools) api(projects.shared.feature) api(projects.shared.resources) diff --git a/shared/feature/build.gradle.kts b/shared/feature/build.gradle.kts index 066ea7f..fcecf0b 100644 --- a/shared/feature/build.gradle.kts +++ b/shared/feature/build.gradle.kts @@ -1,5 +1,8 @@ +@file:OptIn(ExperimentalKotlinGradlePluginApi::class) + import app.futured.kmptemplate.gradle.configuration.ProjectSettings import app.futured.kmptemplate.gradle.ext.iosTargets +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.dokka.gradle.DokkaTask plugins { @@ -32,7 +35,7 @@ kotlin { sourceSets { androidMain { dependencies { - implementation(libs.androidx.compose.runtime) + implementation(libs.jetbrains.compose.runtime) } } commonMain { @@ -49,7 +52,8 @@ kotlin { implementation(projects.shared.network.graphql) implementation(projects.shared.network.rest) implementation(projects.shared.persistence) - implementation(projects.shared.util) + implementation(projects.shared.util.tools) + implementation(projects.shared.util.componentAnnotation) implementation(projects.shared.resources) implementation(libs.logging.kermit) implementation(libs.skie.annotations) diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/data/model/ui/args/SecondScreenArgs.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/data/model/ui/args/SecondScreenArgs.kt new file mode 100644 index 0000000..5d87e2b --- /dev/null +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/data/model/ui/args/SecondScreenArgs.kt @@ -0,0 +1,6 @@ +package app.futured.kmptemplate.feature.data.model.ui.args + +data class SecondScreenArgs( + val id: Int = 0, + val name: String = "" +) diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/signedin/tab/b/TabBDestinations.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/signedin/tab/b/TabBDestinations.kt index 8e8e942..bf9a2a8 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/signedin/tab/b/TabBDestinations.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/navigation/signedin/tab/b/TabBDestinations.kt @@ -1,5 +1,6 @@ package app.futured.kmptemplate.feature.navigation.signedin.tab.b +import app.futured.kmptemplate.feature.data.model.ui.args.SecondScreenArgs import app.futured.kmptemplate.feature.ui.first.FirstComponent import app.futured.kmptemplate.feature.ui.first.FirstScreen import app.futured.kmptemplate.feature.ui.second.SecondComponent @@ -24,7 +25,7 @@ sealed class TabBDestination : Destination { @Serializable data object Second : TabBDestination() { override fun createComponent(componentContext: AppComponentContext): TabBNavEntry { - return TabBNavEntry.Second(SecondComponent(componentContext)) + return TabBNavEntry.Second(SecondComponent(SecondScreenArgs(), componentContext)) } } diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/first/FirstComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/first/FirstComponent.kt index 12b820b..a53cb55 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/first/FirstComponent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/first/FirstComponent.kt @@ -13,5 +13,5 @@ internal class FirstComponent( override val viewState: StateFlow = viewModel.viewState override val actions: FirstScreen.Actions = viewModel - override val events: Flow = viewModel.uiEvents + override val events: Flow = viewModel.uiEvents } diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/first/FirstEvent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/first/FirstEvent.kt index 917b64d..9351b07 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/first/FirstEvent.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/first/FirstEvent.kt @@ -1,7 +1,7 @@ package app.futured.kmptemplate.feature.ui.first -import app.futured.kmptemplate.util.arch.UiEvent +import app.futured.kmptemplate.util.arch.Event -sealed class FirstUiEvent : UiEvent { - data class ShowToast(val text: String) : FirstUiEvent() +sealed class FirstEvent : Event { + data class ShowToast(val text: String) : FirstEvent() } diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/first/FirstScreen.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/first/FirstScreen.kt index 6ade45e..61eb620 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/first/FirstScreen.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/first/FirstScreen.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.StateFlow interface FirstScreen { val viewState: StateFlow val actions: Actions - val events: Flow + val events: Flow interface Actions { fun onBack() diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/first/FirstViewModel.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/first/FirstViewModel.kt index 40ac22e..fdb3c79 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/first/FirstViewModel.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/first/FirstViewModel.kt @@ -19,7 +19,7 @@ internal class FirstViewModel( private val tabBNavigator: TabBNavigator, private val syncDataUseCase: SyncDataUseCase, private val counterUseCase: CounterUseCase, -) : SharedViewModel(), +) : SharedViewModel(), FirstScreen.Actions { private val logger: Logger = Logger.withTag("FirstViewModel") @@ -43,7 +43,7 @@ internal class FirstViewModel( if (count == 10L) { logger.d { "Conter reached 10" } - sendUiEvent(FirstUiEvent.ShowToast("Counter reached 10 🎉")) + sendUiEvent(FirstEvent.ShowToast("Counter reached 10 🎉")) } } onError { error -> diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/second/SecondComponent.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/second/SecondComponent.kt deleted file mode 100644 index 6a553cb..0000000 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/second/SecondComponent.kt +++ /dev/null @@ -1,14 +0,0 @@ -package app.futured.kmptemplate.feature.ui.second - -import app.futured.kmptemplate.util.arch.AppComponentContext -import app.futured.kmptemplate.util.arch.viewModel -import kotlinx.coroutines.flow.StateFlow - -internal class SecondComponent( - componentContext: AppComponentContext, -) : AppComponentContext by componentContext, SecondScreen { - - private val viewModel: SecondViewModel by viewModel() - override val viewState: StateFlow = viewModel.viewState - override val actions: SecondScreen.Actions = viewModel -} diff --git a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/second/SecondScreen.kt b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/second/SecondScreen.kt index be68a01..770dcff 100644 --- a/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/second/SecondScreen.kt +++ b/shared/feature/src/commonMain/kotlin/app/futured/kmptemplate/feature/ui/second/SecondScreen.kt @@ -1,7 +1,10 @@ package app.futured.kmptemplate.feature.ui.second +import app.futured.kmptemplate.feature.data.model.ui.args.SecondScreenArgs +import com.rudolfhladik.annotation.Component import kotlinx.coroutines.flow.StateFlow +@Component(argType = SecondScreenArgs::class) interface SecondScreen { val viewState: StateFlow val actions: Actions diff --git a/shared/network/graphql/build.gradle.kts b/shared/network/graphql/build.gradle.kts index 0f00cda..db9bffc 100644 --- a/shared/network/graphql/build.gradle.kts +++ b/shared/network/graphql/build.gradle.kts @@ -1,7 +1,10 @@ +@file:OptIn(ExperimentalKotlinGradlePluginApi::class) + import app.futured.kmptemplate.gradle.configuration.ProductFlavors import app.futured.kmptemplate.gradle.configuration.ProjectSettings import app.futured.kmptemplate.gradle.ext.iosTargets import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { id(libs.plugins.com.android.library.get().pluginId) diff --git a/shared/network/rest/build.gradle.kts b/shared/network/rest/build.gradle.kts index b5cb86f..3c73bf3 100644 --- a/shared/network/rest/build.gradle.kts +++ b/shared/network/rest/build.gradle.kts @@ -1,7 +1,10 @@ +@file:OptIn(ExperimentalKotlinGradlePluginApi::class) + import app.futured.kmptemplate.gradle.configuration.ProductFlavors import app.futured.kmptemplate.gradle.configuration.ProjectSettings import app.futured.kmptemplate.gradle.ext.iosTargets import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { id(libs.plugins.com.android.library.get().pluginId) diff --git a/shared/persistence/build.gradle.kts b/shared/persistence/build.gradle.kts index 57ab5b2..580d562 100644 --- a/shared/persistence/build.gradle.kts +++ b/shared/persistence/build.gradle.kts @@ -1,5 +1,8 @@ +@file:OptIn(ExperimentalKotlinGradlePluginApi::class) + import app.futured.kmptemplate.gradle.configuration.ProjectSettings import app.futured.kmptemplate.gradle.ext.iosTargets +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { id(libs.plugins.com.android.library.get().pluginId) diff --git a/shared/platform/build.gradle.kts b/shared/platform/build.gradle.kts index 42a9b7e..e2a4077 100644 --- a/shared/platform/build.gradle.kts +++ b/shared/platform/build.gradle.kts @@ -1,5 +1,8 @@ +@file:OptIn(ExperimentalKotlinGradlePluginApi::class) + import app.futured.kmptemplate.gradle.configuration.ProjectSettings import app.futured.kmptemplate.gradle.ext.iosTargets +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { id(libs.plugins.com.android.library.get().pluginId) diff --git a/shared/resources/build.gradle.kts b/shared/resources/build.gradle.kts index 400afc2..dbfaaf7 100644 --- a/shared/resources/build.gradle.kts +++ b/shared/resources/build.gradle.kts @@ -1,5 +1,8 @@ +@file:OptIn(ExperimentalKotlinGradlePluginApi::class) + import app.futured.kmptemplate.gradle.configuration.ProjectSettings import app.futured.kmptemplate.gradle.ext.iosTargets +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { id(libs.plugins.com.android.library.get().pluginId) @@ -17,6 +20,8 @@ dependencies { kotlin { jvmToolchain(ProjectSettings.Kotlin.JvmToolchainVersion) + applyDefaultHierarchyTemplate() + androidTarget { compilerOptions { jvmTarget.set(ProjectSettings.Android.KotlinJvmTarget) diff --git a/shared/util/component-annotation/build.gradle.kts b/shared/util/component-annotation/build.gradle.kts new file mode 100644 index 0000000..49c701b --- /dev/null +++ b/shared/util/component-annotation/build.gradle.kts @@ -0,0 +1,23 @@ +import app.futured.kmptemplate.gradle.configuration.ProjectSettings + +plugins { + id(libs.plugins.kotlin.multiplatform.get().pluginId) +} + +kotlin { + jvmToolchain(ProjectSettings.Kotlin.JvmToolchainVersion) + + applyDefaultHierarchyTemplate() + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain { + dependencies { } + } + } +} diff --git a/shared/util/component-annotation/src/commonMain/kotlin/com/rudolfhladik/annotation/ComponentAnnotation.kt b/shared/util/component-annotation/src/commonMain/kotlin/com/rudolfhladik/annotation/ComponentAnnotation.kt new file mode 100644 index 0000000..a4cc44a --- /dev/null +++ b/shared/util/component-annotation/src/commonMain/kotlin/com/rudolfhladik/annotation/ComponentAnnotation.kt @@ -0,0 +1,12 @@ +package com.rudolfhladik.annotation + +import kotlin.reflect.KClass + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +annotation class Component(val argType: KClass<*> = Unit::class) + + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +annotation class NavigationComponent() diff --git a/shared/util/component-processor/build.gradle.kts b/shared/util/component-processor/build.gradle.kts new file mode 100644 index 0000000..b3c702b --- /dev/null +++ b/shared/util/component-processor/build.gradle.kts @@ -0,0 +1,34 @@ +import app.futured.kmptemplate.gradle.configuration.ProjectSettings + +plugins { + id(libs.plugins.kotlin.multiplatform.get().pluginId) +} + +kotlin { + jvmToolchain(ProjectSettings.Kotlin.JvmToolchainVersion) + + applyDefaultHierarchyTemplate() + + jvm() + + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain { + dependencies { + implementation(projects.shared.util.componentAnnotation) + } + } + + jvmMain { + dependencies { + implementation(libs.ksp.lib) + implementation(libs.poet.interop) + } + kotlin.srcDir("src/main/kotlin") + resources.srcDir("src/main/resources") + } + } +} diff --git a/shared/util/component-processor/src/commonMain/kotlin/com/rudolfhladik/componentprocessor/Deps.kt b/shared/util/component-processor/src/commonMain/kotlin/com/rudolfhladik/componentprocessor/Deps.kt new file mode 100644 index 0000000..7a6628d --- /dev/null +++ b/shared/util/component-processor/src/commonMain/kotlin/com/rudolfhladik/componentprocessor/Deps.kt @@ -0,0 +1,5 @@ +package com.rudolfhladik.componentprocessor + +object Deps { + const val packageName = "com.rudolfhladik.processor" +} diff --git a/shared/util/component-processor/src/jvmMain/kotlin/com/rudolfhladik/componentprocessor/ComponentProcessor.kt b/shared/util/component-processor/src/jvmMain/kotlin/com/rudolfhladik/componentprocessor/ComponentProcessor.kt new file mode 100644 index 0000000..aad355c --- /dev/null +++ b/shared/util/component-processor/src/jvmMain/kotlin/com/rudolfhladik/componentprocessor/ComponentProcessor.kt @@ -0,0 +1,45 @@ +package com.rudolfhladik.componentprocessor + +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.rudolfhladik.annotation.Component +import com.rudolfhladik.componentprocessor.content.ComponentPoetGenerator +import kotlin.reflect.KClass + +class ComponentProcessor( + private val codeGenerator: CodeGenerator, + private val logger: KSPLogger, + private val args: Map, +) : SymbolProcessor { + + override fun process(resolver: Resolver): List { + val components: Sequence = resolver.findAnnotationsForClass(Component::class) + + components.forEach { + generateComponent(it) + } + + return emptyList() + } + + private fun generateComponent(component: KSClassDeclaration) { + val appComponentContextPackage = args.get("appComponentContext") ?: error("specify ViewModelComponent path") + val viewModelExtPackage = args.get("viewModelExt") ?: error("specify viewModel extension path") + + // don't use old hard coded generation +// val componentContentGenerator = ComponentContentGenerator() +// componentContentGenerator.generateContents(component, args, codeGenerator) + + // use poet generation + val poet = ComponentPoetGenerator(logger) + poet.tryPoet(appComponentContextPackage, viewModelExtPackage, component, codeGenerator) + } + + private fun Resolver.findAnnotationsForClass(kClass: KClass<*>): Sequence = + this.getSymbolsWithAnnotation(kClass.qualifiedName.toString()) + .filterIsInstance() +} diff --git a/shared/util/component-processor/src/jvmMain/kotlin/com/rudolfhladik/componentprocessor/ComponentProcessorProvider.kt b/shared/util/component-processor/src/jvmMain/kotlin/com/rudolfhladik/componentprocessor/ComponentProcessorProvider.kt new file mode 100644 index 0000000..3ec5ed2 --- /dev/null +++ b/shared/util/component-processor/src/jvmMain/kotlin/com/rudolfhladik/componentprocessor/ComponentProcessorProvider.kt @@ -0,0 +1,15 @@ +package com.rudolfhladik.componentprocessor + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +class ComponentProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return ComponentProcessor( + codeGenerator = environment.codeGenerator, + logger = environment.logger, + args = environment.options + ) + } +} diff --git a/shared/util/component-processor/src/jvmMain/kotlin/com/rudolfhladik/componentprocessor/content/ComponentContentGenerator.kt b/shared/util/component-processor/src/jvmMain/kotlin/com/rudolfhladik/componentprocessor/content/ComponentContentGenerator.kt new file mode 100644 index 0000000..f9baafb --- /dev/null +++ b/shared/util/component-processor/src/jvmMain/kotlin/com/rudolfhladik/componentprocessor/content/ComponentContentGenerator.kt @@ -0,0 +1,150 @@ +package com.rudolfhladik.componentprocessor.content + +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.rudolfhladik.annotation.Component +import java.io.OutputStream + +internal class ComponentContentGenerator { + internal companion object { + private const val viewStateName = "viewState" + private const val actionsName = "Actions" + private const val suspendActionsName = "SuspendActions" + private const val eventsName = "events" + } + + fun generateContents( + component: KSClassDeclaration, + args: Map, + codeGenerator: CodeGenerator, + ) { + val fileName = getFileName(component) + + val file = codeGenerator.createNewFile( + dependencies = Dependencies(false), + packageName = component.packageName.asString(), + fileName = fileName, + ) + val annotation: KSAnnotation = component.annotations.first { + it.shortName.asString() == Component::class.simpleName + } + // Getting the 'argType' argument object from the @Component. + val nameArgument: String? = annotation.arguments + .firstOrNull { arg -> arg.name?.asString() == "argType" } + ?.value?.toString() + + val contents = mutableListOf() + + val packageLine = "package ${component.packageName.asString()}\n\n" + contents.add(packageLine) + + val imports = getImports(component, nameArgument, args) + contents.addAll(imports) + + val classContents = getClassAndBody(component, fileName, nameArgument) + classContents.forEach { + contents.add(it) + } + + file.use { + it.writeAll(contents) + } + } + + private fun getFileName(component: KSClassDeclaration): String = + getComponentName(component).plus("Component") + + private fun getClassAndBody(component: KSClassDeclaration, fileName: String, nameArgument: String?): List { + val classContents = mutableListOf() + + val declarations = component.declarations + .map { it.simpleName.asString() } + + val componentName = getComponentName(component) + val screenInterfaceName = getScreenInterfaceName(component) + + val classLine = "internal class $fileName(\n" + val argsLine = nameArgument?.let { " arg: $nameArgument,\n" } + val constructorParams = " componentContext: ComponentContext,\n" + val superTypeLine = + ") : ViewModelComponent<${componentName}ViewModel>(componentContext), $screenInterfaceName {\n" + val endClassLine = "}\n" + + val parametersForVM = nameArgument?.let { "parameters = { parametersOf(arg) }" } ?: "" + val bodyViewModel = + " override val viewModel: ${componentName}ViewModel by viewModel($parametersForVM)\n" + + val bodyViewState = " override val viewState: StateFlow<${componentName}ViewState> = viewModel.viewState\n" + + val bodyEvents = " override val events: Flow<${componentName}Event> = viewModel.uiEvents\n" + + val bodyActions = " override val actions: ${screenInterfaceName}.$actionsName = viewModel\n" + + val bodySuspendActions = + " override val suspendActions: ${screenInterfaceName}.$suspendActionsName = viewModel\n" + + classContents.addAll(listOfNotNull(classLine, argsLine, constructorParams, superTypeLine)) + + classContents.add(bodyViewModel) + if (declarations.any { it == viewStateName }) { + classContents.add(bodyViewState) + } + if (declarations.any { it == eventsName }) { + classContents.add(bodyEvents) + } + if (declarations.any { it == actionsName }) { + classContents.add(bodyActions) + } + if (declarations.any { it == suspendActionsName }) { + classContents.add(bodySuspendActions) + } + + classContents.add(endClassLine) + + return classContents + } + + private fun getImports(component: KSClassDeclaration, nameArgument: String?, args: Map): List { + val declarations = component.declarations + .map { it.simpleName.asString() } + +// val viewModelComponentPackage = args.get("viewModel") ?: error("specify ViewModelComponent path") + val viewModelExtPackage = args.get("viewModelExt") ?: error("specify viewModel extension path") + val viewModelExtImport = "import $viewModelExtPackage\n" + + val componentContextImport = "import com.arkivanov.decompose.ComponentContext\n" + val flowImport = if (declarations.any { it == eventsName }) { + "import kotlinx.coroutines.flow.Flow\n" + } else null + val stateFlowImport = if (declarations.any { it == viewStateName }) { + "import kotlinx.coroutines.flow.StateFlow\n" + } else null + val parametersOfImport = if (nameArgument != null) "import org.koin.core.parameter.parametersOf\n" else "" + + return listOfNotNull( + componentContextImport, + flowImport, + stateFlowImport, + viewModelExtImport, + parametersOfImport, + "\n", + ) + } + + private fun getComponentName(component: KSClassDeclaration) = + getScreenInterfaceName(component) + .removeSuffix("Screen") + + private fun getScreenInterfaceName(component: KSClassDeclaration) = component + .qualifiedName + ?.asString() + ?.substringAfterLast(".") ?: "Error" + + private fun OutputStream.writeAll(contents: List) { + contents.forEach { content -> + this.write(content.toByteArray()) + } + } +} diff --git a/shared/util/component-processor/src/jvmMain/kotlin/com/rudolfhladik/componentprocessor/content/ComponentPoetGenerator.kt b/shared/util/component-processor/src/jvmMain/kotlin/com/rudolfhladik/componentprocessor/content/ComponentPoetGenerator.kt new file mode 100644 index 0000000..6935ded --- /dev/null +++ b/shared/util/component-processor/src/jvmMain/kotlin/com/rudolfhladik/componentprocessor/content/ComponentPoetGenerator.kt @@ -0,0 +1,124 @@ +package com.rudolfhladik.componentprocessor.content + +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSType +import com.rudolfhladik.annotation.Component +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.ksp.toTypeName +import com.squareup.kotlinpoet.ksp.writeTo + +internal class ComponentPoetGenerator( + private val logger: KSPLogger, +) { + fun tryPoet( + appComponentContextPackage: String, + viewModelExtPackage: String, + component: KSClassDeclaration, + codeGenerator: CodeGenerator, + ) { + val annotation: KSAnnotation = component.annotations.first { + it.shortName.asString() == Component::class.simpleName + } + // Getting the 'argType' argument object from the @Component. + val arg: KSType? = annotation.arguments.first().value as? KSType + + /// + val baseName = component.qualifiedName?.asString()?.removeSuffix("Screen") ?: error("Unable to get base name") + val classname = ClassName( + packageName = component.packageName.asString(), + simpleNames = listOf(baseName.plus("Component")), + ) + + val appComponentContext = ClassName( + packageName = component.packageName.asString(), + simpleNames = listOf("AppComponentContext"), + ) + + val properties = component.getAllProperties() + + val viewStateType = properties + .find { it.type.toTypeName().toString().contains("ViewState") } + + val actionsType = properties + .find { it.type.toTypeName().toString().contains("Actions") } + + logger.warn("$viewStateType") + + + val vMClass = ClassName( + packageName = component.packageName.asString(), + simpleNames = listOf(baseName.plus("ViewModel")), + ) + val constructorBuilder = FunSpec.constructorBuilder() + if (arg != null) { + constructorBuilder.addParameter("arg", (arg.declaration as KSClassDeclaration).asStarProjectedType().toTypeName()) + } + + constructorBuilder.addParameter("componentContext", appComponentContext) + + val vmStatement = if (arg != null) { + "viewModel { parametersOf(arg) }" + } else { + "viewModel()" + } + + val componentBuilder = TypeSpec.classBuilder(baseName.substringAfterLast('.').plus("Component")) + .addModifiers(KModifier.INTERNAL) + .primaryConstructor(constructorBuilder.build()) + .addSuperinterface( + delegate = CodeBlock.builder().addStatement("componentContext").build(), + superinterface = appComponentContext, + ) + .addSuperinterface( + component.asType(emptyList()).toTypeName(), + ) + .addProperty( + PropertySpec.builder("viewModel", vMClass) + .delegate( + CodeBlock.builder() + .addStatement(vmStatement).build(), + ) + .addModifiers(KModifier.PRIVATE) + .build(), + ) + + if (viewStateType != null) { + componentBuilder + .addProperty( + PropertySpec.builder(viewStateType.simpleName.getShortName(), viewStateType.type.toTypeName()) + .initializer(CodeBlock.builder().addStatement("viewModel.viewState").build()) + .addModifiers(KModifier.OVERRIDE) + .build(), + ) + } + + if (actionsType != null) { + componentBuilder + .addProperty( + PropertySpec.builder(actionsType.simpleName.getShortName(), actionsType.type.toTypeName()) + .initializer(CodeBlock.builder().addStatement("viewModel").build()) + .addModifiers(KModifier.OVERRIDE) + .build(), + ) + } + + val spec = FileSpec.builder(classname) + if (arg != null) { + spec.addImport("org.koin.core.parameter", "parametersOf") + } + spec.addImport(viewModelExtPackage.removeSuffix("viewModel"), "viewModel") + .addImport(appComponentContextPackage.removeSuffix("AppComponentContext"), "AppComponentContext") + .addType(componentBuilder.build()) + + spec.build().writeTo(codeGenerator, aggregating = true) + } +} diff --git a/shared/util/component-processor/src/jvmMain/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/shared/util/component-processor/src/jvmMain/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 0000000..2335102 --- /dev/null +++ b/shared/util/component-processor/src/jvmMain/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +com.rudolfhladik.componentprocessor.ComponentProcessorProvider \ No newline at end of file diff --git a/shared/util/build.gradle.kts b/shared/util/tools/build.gradle.kts similarity index 92% rename from shared/util/build.gradle.kts rename to shared/util/tools/build.gradle.kts index 30e1659..c4f50e7 100644 --- a/shared/util/build.gradle.kts +++ b/shared/util/tools/build.gradle.kts @@ -1,5 +1,8 @@ +@file:OptIn(ExperimentalKotlinGradlePluginApi::class) + import app.futured.kmptemplate.gradle.configuration.ProjectSettings import app.futured.kmptemplate.gradle.ext.iosTargets +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi plugins { id(libs.plugins.com.android.library.get().pluginId) diff --git a/shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/AppComponentContext.kt b/shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/AppComponentContext.kt similarity index 100% rename from shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/AppComponentContext.kt rename to shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/AppComponentContext.kt diff --git a/shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/Event.kt b/shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/Event.kt similarity index 86% rename from shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/Event.kt rename to shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/Event.kt index 1b72752..c6df705 100644 --- a/shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/Event.kt +++ b/shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/Event.kt @@ -6,4 +6,4 @@ package app.futured.kmptemplate.util.arch * Event is guaranteed to be delivered just once even if screen rotation or a similar * operation is in progress. */ -interface UiEvent +interface Event diff --git a/shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/Navigation.kt b/shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/Navigation.kt similarity index 100% rename from shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/Navigation.kt rename to shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/Navigation.kt diff --git a/shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/SharedViewModel.kt b/shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/SharedViewModel.kt similarity index 91% rename from shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/SharedViewModel.kt rename to shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/SharedViewModel.kt index bfadbf0..f6ade6e 100644 --- a/shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/SharedViewModel.kt +++ b/shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/SharedViewModel.kt @@ -23,7 +23,7 @@ import org.koin.core.component.KoinComponent * It implements the [UseCaseExecutionScope] interface, so KMM UseCases * can be executed in it's [CoroutineScope] tied to retained instance lifecycle. */ -abstract class SharedViewModel> : +abstract class SharedViewModel> : InstanceKeeper.Instance, UseCaseExecutionScope, KoinComponent { @@ -52,7 +52,7 @@ abstract class SharedViewModel> : .shareIn(viewModelScope, SharingStarted.Lazily) /** - * Sends an [UiEvent] to event channel that can be consumed using [uiEvents] flow. + * Sends an [Event] to event channel that can be consumed using [uiEvents] flow. */ fun sendUiEvent(event: UI_EVENT) { viewModelScope.launch { diff --git a/shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/ViewState.kt b/shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/ViewState.kt similarity index 100% rename from shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/ViewState.kt rename to shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/arch/ViewState.kt diff --git a/shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/FlowUseCase.kt b/shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/FlowUseCase.kt similarity index 100% rename from shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/FlowUseCase.kt rename to shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/FlowUseCase.kt diff --git a/shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/UseCase.kt b/shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/UseCase.kt similarity index 100% rename from shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/UseCase.kt rename to shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/UseCase.kt diff --git a/shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/error/UseCaseErrorHandler.kt b/shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/error/UseCaseErrorHandler.kt similarity index 100% rename from shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/error/UseCaseErrorHandler.kt rename to shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/error/UseCaseErrorHandler.kt diff --git a/shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/scope/CoroutineScopeOwner.kt b/shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/scope/CoroutineScopeOwner.kt similarity index 100% rename from shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/scope/CoroutineScopeOwner.kt rename to shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/scope/CoroutineScopeOwner.kt diff --git a/shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/scope/FlowUseCaseExecutionScope.kt b/shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/scope/FlowUseCaseExecutionScope.kt similarity index 100% rename from shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/scope/FlowUseCaseExecutionScope.kt rename to shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/scope/FlowUseCaseExecutionScope.kt diff --git a/shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/scope/SingleUseCaseExecutionScope.kt b/shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/scope/SingleUseCaseExecutionScope.kt similarity index 100% rename from shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/scope/SingleUseCaseExecutionScope.kt rename to shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/scope/SingleUseCaseExecutionScope.kt diff --git a/shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/scope/UseCaseExecutionScope.kt b/shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/scope/UseCaseExecutionScope.kt similarity index 100% rename from shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/scope/UseCaseExecutionScope.kt rename to shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/domain/scope/UseCaseExecutionScope.kt diff --git a/shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/ext/DecomposeValueExt.kt b/shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/ext/DecomposeValueExt.kt similarity index 100% rename from shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/ext/DecomposeValueExt.kt rename to shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/ext/DecomposeValueExt.kt diff --git a/shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/ext/FlowExt.kt b/shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/ext/FlowExt.kt similarity index 100% rename from shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/ext/FlowExt.kt rename to shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/ext/FlowExt.kt diff --git a/shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/ext/LifecycleOwnerExt.kt b/shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/ext/LifecycleOwnerExt.kt similarity index 100% rename from shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/ext/LifecycleOwnerExt.kt rename to shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/ext/LifecycleOwnerExt.kt diff --git a/shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/ext/ResultExt.kt b/shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/ext/ResultExt.kt similarity index 100% rename from shared/util/src/commonMain/kotlin/app/futured/kmptemplate/util/ext/ResultExt.kt rename to shared/util/tools/src/commonMain/kotlin/app/futured/kmptemplate/util/ext/ResultExt.kt diff --git a/shared/util/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/UseCaseExecutionScopeTest.kt b/shared/util/tools/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/UseCaseExecutionScopeTest.kt similarity index 100% rename from shared/util/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/UseCaseExecutionScopeTest.kt rename to shared/util/tools/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/UseCaseExecutionScopeTest.kt diff --git a/shared/util/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/base/BaseUseCaseExecutionScopeTest.kt b/shared/util/tools/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/base/BaseUseCaseExecutionScopeTest.kt similarity index 100% rename from shared/util/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/base/BaseUseCaseExecutionScopeTest.kt rename to shared/util/tools/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/base/BaseUseCaseExecutionScopeTest.kt diff --git a/shared/util/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/usecases/TestFailureFlowUseCase.kt b/shared/util/tools/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/usecases/TestFailureFlowUseCase.kt similarity index 100% rename from shared/util/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/usecases/TestFailureFlowUseCase.kt rename to shared/util/tools/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/usecases/TestFailureFlowUseCase.kt diff --git a/shared/util/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/usecases/TestFailureUseCase.kt b/shared/util/tools/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/usecases/TestFailureUseCase.kt similarity index 100% rename from shared/util/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/usecases/TestFailureUseCase.kt rename to shared/util/tools/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/usecases/TestFailureUseCase.kt diff --git a/shared/util/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/usecases/TestFlowUseCase.kt b/shared/util/tools/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/usecases/TestFlowUseCase.kt similarity index 100% rename from shared/util/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/usecases/TestFlowUseCase.kt rename to shared/util/tools/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/usecases/TestFlowUseCase.kt diff --git a/shared/util/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/usecases/TestUseCase.kt b/shared/util/tools/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/usecases/TestUseCase.kt similarity index 100% rename from shared/util/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/usecases/TestUseCase.kt rename to shared/util/tools/src/commonTest/kotlin/app/futured/kmptemplate/util/domain/usecases/TestUseCase.kt